mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
becca entities enriched with functionality from repository entities
This commit is contained in:
parent
7494491560
commit
8d8d654fe8
5
.idea/dataSources.xml
generated
5
.idea/dataSources.xml
generated
@ -1,11 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="document.db" uuid="4e69c96a-8a2b-43f5-9b40-d1608f75f7a4">
|
||||
<data-source source="LOCAL" name="SQLite - document.db" uuid="30cef30d-e704-484d-a4ca-5d3bfc2ece63">
|
||||
<driver-ref>sqlite.xerial</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||
<jdbc-url>jdbc:sqlite:$USER_HOME$/trilium-data/document.db</jdbc-url>
|
||||
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/../trilium-data/document.db</jdbc-url>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
2
db/TODO.txt
Normal file
2
db/TODO.txt
Normal file
@ -0,0 +1,2 @@
|
||||
- drop branches.utcDateCreated - not used for anything
|
||||
- drop options.utcDateCreated - not used for anything
|
@ -17,7 +17,7 @@ function load() {
|
||||
const start = Date.now();
|
||||
becca.reset();
|
||||
|
||||
for (const row of sql.iterateRows(`SELECT noteId, title, type, mime, isProtected, dateCreated, dateModified, utcDateCreated, utcDateModified FROM notes WHERE isDeleted = 0`, [])) {
|
||||
for (const row of sql.iterateRows(`SELECT noteId, title, type, mime, isProtected, dateCreated, dateModified, utcDateCreated, utcDateModified FROM notes`, [])) {
|
||||
new Note(becca, row);
|
||||
}
|
||||
|
||||
|
43
src/services/becca/entities/abstract_entity.js
Normal file
43
src/services/becca/entities/abstract_entity.js
Normal file
@ -0,0 +1,43 @@
|
||||
"use strict";
|
||||
|
||||
class AbstractEntity {
|
||||
beforeSaving() {
|
||||
this.generateIdIfNecessary();
|
||||
}
|
||||
|
||||
generateIdIfNecessary() {
|
||||
if (!this[this.constructor.primaryKeyName]) {
|
||||
this[this.constructor.primaryKeyName] = utils.newEntityId();
|
||||
}
|
||||
}
|
||||
|
||||
generateHash() {
|
||||
let contentToHash = "";
|
||||
|
||||
for (const propertyName of this.constructor.hashedProperties) {
|
||||
contentToHash += "|" + this[propertyName];
|
||||
}
|
||||
|
||||
return utils.hash(contentToHash).substr(0, 10);
|
||||
}
|
||||
|
||||
getUtcDateChanged() {
|
||||
return this.utcDateModified || this.utcDateCreated;
|
||||
}
|
||||
|
||||
get repository() {
|
||||
if (!repo) {
|
||||
repo = require('../services/repository');
|
||||
}
|
||||
|
||||
return repo;
|
||||
}
|
||||
|
||||
save() {
|
||||
this.repository.updateEntity(this);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AbstractEntity;
|
@ -1,9 +1,19 @@
|
||||
"use strict";
|
||||
|
||||
const Note = require('./note.js');
|
||||
const AbstractEntity = require("./abstract_entity.js");
|
||||
const sql = require("../../sql.js");
|
||||
const dateUtils = require("../../date_utils.js");
|
||||
const promotedAttributeDefinitionParser = require("../../promoted_attribute_definition_parser");
|
||||
|
||||
class Attribute extends AbstractEntity {
|
||||
static get entityName() { return "attributes"; }
|
||||
static get primaryKeyName() { return "attributeId"; }
|
||||
static get hashedProperties() { return ["attributeId", "noteId", "type", "name", "value", "isInheritable"]; }
|
||||
|
||||
class Attribute {
|
||||
constructor(becca, row) {
|
||||
super();
|
||||
|
||||
/** @param {Becca} */
|
||||
this.becca = becca;
|
||||
/** @param {string} */
|
||||
@ -60,13 +70,99 @@ class Attribute {
|
||||
}
|
||||
}
|
||||
|
||||
// for logging etc
|
||||
/**
|
||||
* @returns {Note|null}
|
||||
*/
|
||||
getNote() {
|
||||
return this.repository.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.repository.getNote(this.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isDefinition() {
|
||||
return this.type === 'label' && (this.name.startsWith('label:') || this.name.startsWith('relation:'));
|
||||
}
|
||||
|
||||
getDefinition() {
|
||||
return promotedAttributeDefinitionParser.parse(this.value);
|
||||
}
|
||||
|
||||
getDefinedName() {
|
||||
if (this.type === 'label' && this.name.startsWith('label:')) {
|
||||
return this.name.substr(6);
|
||||
} else if (this.type === 'label' && this.name.startsWith('relation:')) {
|
||||
return this.name.substr(9);
|
||||
} else {
|
||||
return this.name;
|
||||
}
|
||||
}
|
||||
|
||||
get pojo() {
|
||||
const pojo = {...this};
|
||||
return {
|
||||
attributeId: this.attributeId,
|
||||
noteId: this.noteId,
|
||||
type: this.type,
|
||||
name: this.name,
|
||||
position: this.position,
|
||||
value: this.value,
|
||||
isInheritable: this.isInheritable
|
||||
};
|
||||
}
|
||||
|
||||
delete pojo.becca;
|
||||
beforeSaving() {
|
||||
if (!this.value) {
|
||||
if (this.type === 'relation') {
|
||||
throw new Error(`Cannot save relation ${this.name} since it does not target any note.`);
|
||||
}
|
||||
|
||||
return pojo;
|
||||
// null value isn't allowed
|
||||
this.value = "";
|
||||
}
|
||||
|
||||
if (this.position === undefined) {
|
||||
this.position = 1 + sql.getValue(`SELECT COALESCE(MAX(position), 0) FROM attributes WHERE noteId = ?`, [this.noteId]);
|
||||
}
|
||||
|
||||
if (!this.isInheritable) {
|
||||
this.isInheritable = false;
|
||||
}
|
||||
|
||||
if (!this.isDeleted) {
|
||||
this.isDeleted = false;
|
||||
}
|
||||
|
||||
super.beforeSaving();
|
||||
|
||||
this.utcDateModified = dateUtils.utcNowDateTime();
|
||||
}
|
||||
|
||||
createClone(type, name, value, isInheritable) {
|
||||
return new Attribute({
|
||||
noteId: this.noteId,
|
||||
type: type,
|
||||
name: name,
|
||||
value: value,
|
||||
position: this.position,
|
||||
isInheritable: isInheritable,
|
||||
isDeleted: false,
|
||||
utcDateModified: this.utcDateModified
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,19 @@
|
||||
"use strict";
|
||||
|
||||
const Note = require('./note.js');
|
||||
const AbstractEntity = require("./abstract_entity.js");
|
||||
const sql = require("../../sql.js");
|
||||
const dateUtils = require("../../date_utils.js");
|
||||
|
||||
class Branch extends AbstractEntity {
|
||||
static get entityName() { return "branches"; }
|
||||
static get primaryKeyName() { return "branchId"; }
|
||||
// notePosition is not part of hash because it would produce a lot of updates in case of reordering
|
||||
static get hashedProperties() { return ["branchId", "noteId", "parentNoteId", "prefix"]; }
|
||||
|
||||
class Branch {
|
||||
constructor(becca, row) {
|
||||
super();
|
||||
|
||||
/** @param {Becca} */
|
||||
this.becca = becca;
|
||||
/** @param {string} */
|
||||
@ -55,13 +65,44 @@ class Branch {
|
||||
return this.becca.notes[this.parentNoteId];
|
||||
}
|
||||
|
||||
// for logging etc
|
||||
get pojo() {
|
||||
const pojo = {...this};
|
||||
return {
|
||||
branchId: this.branchId,
|
||||
noteId: this.noteId,
|
||||
parentNoteId: this.parentNoteId,
|
||||
prefix: this.prefix,
|
||||
notePosition: this.notePosition,
|
||||
isExpanded: this.isExpanded
|
||||
};
|
||||
}
|
||||
|
||||
delete pojo.becca;
|
||||
createClone(parentNoteId, notePosition) {
|
||||
return new Branch({
|
||||
noteId: this.noteId,
|
||||
parentNoteId: parentNoteId,
|
||||
notePosition: notePosition,
|
||||
prefix: this.prefix,
|
||||
isExpanded: this.isExpanded
|
||||
});
|
||||
}
|
||||
|
||||
return pojo;
|
||||
beforeSaving() {
|
||||
if (this.notePosition === undefined || this.notePosition === null) {
|
||||
const maxNotePos = sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [this.parentNoteId]);
|
||||
this.notePosition = maxNotePos === null ? 0 : maxNotePos + 10;
|
||||
}
|
||||
|
||||
if (!this.isExpanded) {
|
||||
this.isExpanded = false;
|
||||
}
|
||||
|
||||
if (!this.branchId) {
|
||||
this.utcDateCreated = dateUtils.utcNowDateTime();
|
||||
}
|
||||
|
||||
super.beforeSaving();
|
||||
|
||||
this.utcDateModified = dateUtils.utcNowDateTime();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,12 +2,23 @@
|
||||
|
||||
const protectedSessionService = require('../../protected_session');
|
||||
const log = require('../../log');
|
||||
const sql = require('../../sql');
|
||||
const utils = require('../../utils');
|
||||
const dateUtils = require('../../date_utils');
|
||||
const entityChangesService = require('../../entity_changes.js');
|
||||
const AbstractEntity = require("./abstract_entity.js");
|
||||
|
||||
const LABEL = 'label';
|
||||
const RELATION = 'relation';
|
||||
|
||||
class Note {
|
||||
class Note extends AbstractEntity {
|
||||
static get entityName() { return "notes"; }
|
||||
static get primaryKeyName() { return "noteId"; }
|
||||
static get hashedProperties() { return ["noteId", "title", "isProtected", "type", "mime"]; }
|
||||
|
||||
constructor(becca, row) {
|
||||
super();
|
||||
|
||||
/** @param {Becca} */
|
||||
this.becca = becca;
|
||||
|
||||
@ -46,10 +57,14 @@ class Note {
|
||||
}
|
||||
|
||||
update(row) {
|
||||
// ------ Database persisted attributes ------
|
||||
|
||||
/** @param {string} */
|
||||
this.noteId = row.noteId;
|
||||
/** @param {string} */
|
||||
this.title = row.title;
|
||||
/** @param {boolean} */
|
||||
this.isProtected = !!row.isProtected;
|
||||
/** @param {string} */
|
||||
this.type = row.type;
|
||||
/** @param {string} */
|
||||
@ -62,8 +77,9 @@ class Note {
|
||||
this.utcDateCreated = row.utcDateCreated;
|
||||
/** @param {string} */
|
||||
this.utcDateModified = row.utcDateModified;
|
||||
/** @param {boolean} */
|
||||
this.isProtected = !!row.isProtected;
|
||||
|
||||
// ------ Derived attributes ------
|
||||
|
||||
/** @param {boolean} */
|
||||
this.isDecrypted = !row.isProtected || !!row.isContentAvailable;
|
||||
|
||||
@ -73,6 +89,162 @@ class Note {
|
||||
this.flatTextCache = null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Note content has quite special handling - it's not a separate entity, but a lazily loaded
|
||||
* part of Note entity with it's own sync. Reasons behind this hybrid design has been:
|
||||
*
|
||||
* - content can be quite large and it's not necessary to load it / fill memory for any note access even if we don't need a content, especially for bulk operations like search
|
||||
* - changes in the note metadata or title should not trigger note content sync (so we keep separate utcDateModified and entity changes records)
|
||||
* - but to the user note content and title changes are one and the same - single dateModified (so all changes must go through Note and content is not a separate entity)
|
||||
*/
|
||||
|
||||
/** @returns {*} */
|
||||
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.isProtected) {
|
||||
if (protectedSessionService.isProtectedSessionAvailable()) {
|
||||
content = content === null ? null : protectedSessionService.decrypt(content);
|
||||
}
|
||||
else {
|
||||
content = "";
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isStringNote()) {
|
||||
return content === null
|
||||
? ""
|
||||
: content.toString("UTF-8");
|
||||
}
|
||||
else {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
/** @returns {{contentLength, dateModified, utcDateModified}} */
|
||||
getContentMetadata() {
|
||||
return sql.getRow(`
|
||||
SELECT
|
||||
LENGTH(content) AS contentLength,
|
||||
dateModified,
|
||||
utcDateModified
|
||||
FROM note_contents
|
||||
WHERE noteId = ?`, [this.noteId]);
|
||||
}
|
||||
|
||||
/** @returns {*} */
|
||||
getJsonContent() {
|
||||
const content = this.getContent();
|
||||
|
||||
if (!content || !content.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
setContent(content) {
|
||||
if (content === null || content === undefined) {
|
||||
throw new Error(`Cannot set null content to note ${this.noteId}`);
|
||||
}
|
||||
|
||||
if (this.isStringNote()) {
|
||||
content = content.toString();
|
||||
}
|
||||
else {
|
||||
content = Buffer.isBuffer(content) ? content : Buffer.from(content);
|
||||
}
|
||||
|
||||
const pojo = {
|
||||
noteId: this.noteId,
|
||||
content: content,
|
||||
dateModified: dateUtils.localNowDateTime(),
|
||||
utcDateModified: dateUtils.utcNowDateTime()
|
||||
};
|
||||
|
||||
if (this.isProtected) {
|
||||
if (protectedSessionService.isProtectedSessionAvailable()) {
|
||||
pojo.content = protectedSessionService.encrypt(pojo.content);
|
||||
}
|
||||
else {
|
||||
throw new Error(`Cannot update content of noteId=${this.noteId} since we're out of protected session.`);
|
||||
}
|
||||
}
|
||||
|
||||
sql.upsert("note_contents", "noteId", pojo);
|
||||
|
||||
const hash = utils.hash(this.noteId + "|" + pojo.content.toString());
|
||||
|
||||
entityChangesService.addEntityChange({
|
||||
entityName: 'note_contents',
|
||||
entityId: this.noteId,
|
||||
hash: hash,
|
||||
isErased: false,
|
||||
utcDateChanged: pojo.utcDateModified
|
||||
}, null);
|
||||
}
|
||||
|
||||
setJsonContent(content) {
|
||||
this.setContent(JSON.stringify(content, null, '\t'));
|
||||
}
|
||||
|
||||
/** @returns {boolean} true if this note is the root of the note tree. Root note has "root" noteId */
|
||||
isRoot() {
|
||||
return this.noteId === 'root';
|
||||
}
|
||||
|
||||
/** @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);
|
||||
}
|
||||
|
||||
/** @returns {string|null} JS script environment - either "frontend" or "backend" */
|
||||
getScriptEnv() {
|
||||
if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith('env=frontend'))) {
|
||||
return "frontend";
|
||||
}
|
||||
|
||||
if (this.type === 'render') {
|
||||
return "frontend";
|
||||
}
|
||||
|
||||
if (this.isJavaScript() && this.mime.endsWith('env=backend')) {
|
||||
return "backend";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return {Attribute[]} */
|
||||
get attributes() {
|
||||
return this.__getAttributes([]);
|
||||
@ -543,19 +715,45 @@ class Note {
|
||||
}
|
||||
}
|
||||
|
||||
// for logging etc
|
||||
get pojo() {
|
||||
const pojo = {...this};
|
||||
return {
|
||||
noteId: this.noteId,
|
||||
title: this.title,
|
||||
isProtected: this.isProtected,
|
||||
type: this.type,
|
||||
mime: this.mime,
|
||||
dateCreated: this.dateCreated,
|
||||
dateModified: this.dateModified,
|
||||
utcDateCreated: this.utcDateCreated,
|
||||
utcDateModified: this.utcDateModified
|
||||
};
|
||||
}
|
||||
|
||||
delete pojo.becca;
|
||||
delete pojo.ancestorCache;
|
||||
delete pojo.attributeCache;
|
||||
delete pojo.flatTextCache;
|
||||
delete pojo.children;
|
||||
delete pojo.parents;
|
||||
delete pojo.parentBranches;
|
||||
beforeSaving() {
|
||||
if (!this.dateCreated) {
|
||||
this.dateCreated = dateUtils.localNowDateTime();
|
||||
}
|
||||
|
||||
return pojo;
|
||||
if (!this.utcDateCreated) {
|
||||
this.utcDateCreated = dateUtils.utcNowDateTime();
|
||||
}
|
||||
|
||||
super.beforeSaving();
|
||||
|
||||
this.dateModified = dateUtils.localNowDateTime();
|
||||
this.utcDateModified = dateUtils.utcNowDateTime();
|
||||
}
|
||||
|
||||
updatePojo(pojo) {
|
||||
if (pojo.isProtected) {
|
||||
if (this.isDecrypted) {
|
||||
pojo.title = protectedSessionService.encrypt(pojo.title);
|
||||
}
|
||||
else {
|
||||
// updating protected note outside of protected session means we will keep original ciphertexts
|
||||
delete pojo.title;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user