unify .setContent() .getContent() handling across notes, revisions, attachments

This commit is contained in:
zadam 2023-03-16 15:19:26 +01:00
parent bb45c67e60
commit 2b84f1be00
7 changed files with 151 additions and 249 deletions

View file

@ -7,6 +7,7 @@ CREATE TABLE IF NOT EXISTS "attachments"
title TEXT not null,
isProtected INT not null DEFAULT 0,
blobId TEXT not null,
utcDateScheduledForDeletionSince TEXT DEFAULT NULL,
utcDateModified TEXT not null,
isDeleted INT not null,
deleteId TEXT DEFAULT NULL);

View file

@ -7,6 +7,7 @@ const eventService = require("../../services/events");
const dateUtils = require("../../services/date_utils");
const cls = require("../../services/cls");
const log = require("../../services/log");
const protectedSessionService = require("../../services/protected_session.js");
let becca = null;
@ -118,6 +119,113 @@ class AbstractBeccaEntity {
return this;
}
/** @protected */
_isHot() {
return false;
}
/** @protected */
_setContent(content) {
if (content === null || content === undefined) {
throw new Error(`Cannot set null content to ${this.constructor.primaryKeyName} '${this[this.constructor.primaryKeyName]}'`);
}
if (this.isStringNote()) {
content = content.toString();
}
else {
content = Buffer.isBuffer(content) ? content : Buffer.from(content);
}
if (this.isProtected) {
if (protectedSessionService.isProtectedSessionAvailable()) {
content = protectedSessionService.encrypt(content);
}
else {
throw new Error(`Cannot update content of blob since we're out of protected session.`);
}
}
sql.transactional(() => {
let newBlobId = this._saveBlob(content);
if (newBlobId !== this.blobId) {
this.blobId = newBlobId;
this.save();
}
});
}
/** @protected */
_saveBlob(content) {
let newBlobId;
let blobNeedsInsert;
if (this._isHot()) {
newBlobId = this.blobId || utils.randomBlobId();
blobNeedsInsert = true;
} else {
newBlobId = utils.hashedBlobId(content);
blobNeedsInsert = !sql.getValue('SELECT 1 FROM blobs WHERE blobId = ?', [newBlobId]);
}
if (blobNeedsInsert) {
const pojo = {
blobId: newBlobId,
content: content,
dateModified: dateUtils.localNowDateTime(),
utcDateModified: dateUtils.utcNowDateTime()
};
sql.upsert("blobs", "blobId", pojo);
const hash = utils.hash(`${newBlobId}|${pojo.content.toString()}`);
entityChangesService.addEntityChange({
entityName: 'blobs',
entityId: newBlobId,
hash: hash,
isErased: false,
utcDateChanged: pojo.utcDateModified,
isSynced: true
});
eventService.emit(eventService.ENTITY_CHANGED, {
entityName: 'blobs',
entity: this
});
}
return newBlobId;
}
/** @protected */
_getContent() {
const row = sql.getRow(`SELECT content FROM blobs WHERE blobId = ?`, [this.blobId]);
if (!row) {
throw new Error(`Cannot find content for ${this.constructor.primaryKeyName} '${this[this.constructor.primaryKeyName]}', blobId '${this.blobId}'`);
}
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;
}
}
/**
* Mark the entity as (soft) deleted. It will be completely erased later.
*

View file

@ -17,7 +17,8 @@ const AbstractBeccaEntity = require("./abstract_becca_entity");
class BAttachment extends AbstractBeccaEntity {
static get entityName() { return "attachments"; }
static get primaryKeyName() { return "attachmentId"; }
static get hashedProperties() { return ["attachmentId", "parentId", "role", "mime", "title", "utcDateModified"]; }
static get hashedProperties() { return ["attachmentId", "parentId", "role", "mime", "title", "blobId",
"utcDateScheduledForDeletionSince", "utcDateModified"]; }
constructor(row) {
super();
@ -32,7 +33,7 @@ class BAttachment extends AbstractBeccaEntity {
throw new Error("'title' must be given to initialize a Attachment entity");
}
/** @type {string} needs to be set at the initialization time since it's used in the .setContent() */
/** @type {string} */
this.attachmentId = row.attachmentId || `${this.noteId}_${this.name}`; // FIXME
/** @type {string} either noteId or noteRevisionId to which this attachment belongs */
this.parentId = row.parentId;
@ -45,6 +46,8 @@ class BAttachment extends AbstractBeccaEntity {
/** @type {boolean} */
this.isProtected = !!row.isProtected;
/** @type {string} */
this.utcDateScheduledForDeletionSince = row.utcDateScheduledForDeletionSince;
/** @type {string} */
this.utcDateModified = row.utcDateModified;
}
@ -58,68 +61,12 @@ class BAttachment extends AbstractBeccaEntity {
}
/** @returns {*} */
getContent(silentNotFoundError = false) {
const res = sql.getRow(`SELECT content FROM attachment_contents WHERE attachmentId = ?`, [this.attachmentId]);
if (!res) {
if (silentNotFoundError) {
return undefined;
}
else {
throw new Error(`Cannot find note attachment content for attachmentId=${this.attachmentId}`);
}
}
let content = res.content;
if (this.isProtected) {
if (protectedSessionService.isProtectedSessionAvailable()) {
content = protectedSessionService.decrypt(content);
}
else {
content = "";
}
}
if (this.isStringNote()) {
return content === null
? ""
: content.toString("UTF-8");
}
else {
return content;
}
getContent() {
return this._getContent();
}
setContent(content) {
sql.transactional(() => {
this.save(); // also explicitly save attachment to update contentCheckSum
const pojo = {
attachmentId: this.attachmentId,
content: content,
utcDateModified: dateUtils.utcNowDateTime()
};
if (this.isProtected) {
if (protectedSessionService.isProtectedSessionAvailable()) {
pojo.content = protectedSessionService.encrypt(pojo.content);
} else {
throw new Error(`Cannot update content of attachmentId=${this.attachmentId} since we're out of protected session.`);
}
}
sql.upsert("attachment_contents", "attachmentId", pojo);
entityChangesService.addEntityChange({
entityName: 'attachment_contents',
entityId: this.attachmentId,
hash: this.contentCheckSum, // FIXME
isErased: false,
utcDateChanged: pojo.utcDateModified,
isSynced: true
});
});
this._setContent(content);
}
calculateCheckSum(content) {

View file

@ -208,37 +208,8 @@ class BNote extends AbstractBeccaEntity {
*/
/** @returns {*} */
getContent(silentNotFoundError = false) {
const row = sql.getRow(`SELECT content FROM blobs WHERE blobId = ?`, [this.blobId]);
if (!row) {
if (silentNotFoundError) {
return undefined;
}
else {
throw new Error(`Cannot find note content for noteId '${this.noteId}', blobId '${this.blobId}'.`);
}
}
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;
}
getContent() {
return this._getContent();
}
/** @returns {{contentLength, dateModified, utcDateModified}} */
@ -252,6 +223,29 @@ class BNote extends AbstractBeccaEntity {
WHERE blobId = ?`, [this.blobId]);
}
/** @returns {*} */
getJsonContent() {
const content = this.getContent();
if (!content || !content.trim()) {
return null;
}
return JSON.parse(content);
}
_isHot() {
return ['text', 'code', 'relationMap', 'canvas', 'mermaid'].includes(this.type);
}
setContent(content) {
this._setContent(content)
}
setJsonContent(content) {
this.setContent(JSON.stringify(content, null, '\t'));
}
get dateCreatedObj() {
return this.dateCreated === null ? null : dayjs(this.dateCreated);
}
@ -268,90 +262,6 @@ class BNote extends AbstractBeccaEntity {
return this.utcDateModified === null ? null : dayjs.utc(this.utcDateModified);
}
/** @returns {*} */
getJsonContent() {
const content = this.getContent();
if (!content || !content.trim()) {
return null;
}
return JSON.parse(content);
}
isHot() {
return ['text', 'code', 'relationMap', 'canvas', 'mermaid'].includes(this.type);
}
setContent(content, ignoreMissingProtectedSession = false) {
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);
}
if (this.isProtected) {
if (protectedSessionService.isProtectedSessionAvailable()) {
content = protectedSessionService.encrypt(content);
}
else if (!ignoreMissingProtectedSession) {
throw new Error(`Cannot update content of noteId '${this.noteId}' since we're out of protected session.`);
}
}
let newBlobId;
let blobNeedsInsert;
if (this.isHot()) {
newBlobId = this.blobId || utils.randomBlobId();
blobNeedsInsert = true;
} else {
newBlobId = utils.hashedBlobId(content);
blobNeedsInsert = !sql.getValue('SELECT 1 FROM blobs WHERE blobId = ?', [newBlobId]);
}
if (blobNeedsInsert) {
const pojo = {
blobId: this.blobId,
content: content,
dateModified: dateUtils.localNowDateTime(),
utcDateModified: dateUtils.utcNowDateTime()
};
sql.upsert("blobs", "blobId", pojo);
const hash = utils.hash(`${this.blobId}|${pojo.content.toString()}`);
entityChangesService.addEntityChange({
entityName: 'blobs',
entityId: this.blobId,
hash: hash,
isErased: false,
utcDateChanged: pojo.utcDateModified,
isSynced: true
});
eventService.emit(eventService.ENTITY_CHANGED, {
entityName: 'blobs',
entity: this
});
}
if (newBlobId !== this.blobId) {
this.blobId = newBlobId;
this.save();
}
}
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';

View file

@ -75,76 +75,12 @@ class BNoteRevision extends AbstractBeccaEntity {
*/
/** @returns {*} */
getContent(silentNotFoundError = false) {
const res = sql.getRow(`SELECT content FROM blobs WHERE blobId = ?`, [this.blobId]);
if (!res) {
if (silentNotFoundError) {
return undefined;
}
else {
throw new Error(`Cannot find note revision content for noteRevisionId '${this.noteRevisionId}', blobId '${this.blobId}'`);
}
}
let content = res.content;
if (this.isProtected) {
if (protectedSessionService.isProtectedSessionAvailable()) {
content = protectedSessionService.decrypt(content);
}
else {
content = "";
}
}
if (this.isStringNote()) {
return content === null
? ""
: content.toString("UTF-8");
}
else {
return content;
}
getContent() {
return this._getContent();
}
setContent(content) {
if (this.isProtected) {
if (protectedSessionService.isProtectedSessionAvailable()) {
content = protectedSessionService.encrypt(content);
}
else {
throw new Error(`Cannot update content of noteRevisionId '${this.noteRevisionId}' since we're out of protected session.`);
}
}
this.blobId = utils.hashedBlobId(content);
const blobAlreadyExists = !!sql.getValue('SELECT 1 FROM blobs WHERE blobId = ?', [this.blobId]);
if (!blobAlreadyExists) {
const pojo = {
blobId: this.blobId,
content: content,
dateModified: dateUtils.localNowDate(),
utcDateModified: dateUtils.utcNowDateTime()
};
sql.insert("blobs", pojo);
const hash = utils.hash(`${this.noteRevisionId}|${pojo.content.toString()}`);
entityChangesService.addEntityChange({
entityName: 'blobs',
entityId: this.blobId,
hash: hash,
isErased: false,
utcDateChanged: this.getUtcDateChanged(),
isSynced: true
});
}
this.save(); // saving this.blobId
this._setContent(content);
}
beforeSaving() {

View file

@ -8,7 +8,7 @@ function protectAttachments(note) {
for (const attachment of note.getAttachments()) {
if (note.isProtected !== attachment.isProtected) {
if (!protectedSession.isProtectedSessionAvailable()) {
log.error("Protected session is not available to fix note attachments.");
log.error("Protected session is not available to fix attachments.");
return;
}
@ -24,7 +24,7 @@ function protectAttachments(note) {
attachment.save();
}
catch (e) {
log.error(`Could not un/protect note attachment ID = ${attachment.attachmentId}`);
log.error(`Could not un/protect attachment ID = ${attachment.attachmentId}`);
throw e;
}

View file

@ -231,9 +231,9 @@ class ConsistencyChecks {
this.reloadNeeded = false;
logFix(`Note attachment '${attachmentId}' has been deleted since it references missing note/revision '${parentId}'`);
logFix(`Attachment '${attachmentId}' has been deleted since it references missing note/revision '${parentId}'`);
} else {
logError(`Note attachment '${attachmentId}' references missing note/revision '${parentId}'`);
logError(`Attachment '${attachmentId}' references missing note/revision '${parentId}'`);
}
});
}
@ -358,9 +358,9 @@ class ConsistencyChecks {
this.reloadNeeded = false;
logFix(`Note attachment '${attachmentId}' has been deleted since associated note '${noteId}' is deleted.`);
logFix(`Attachment '${attachmentId}' has been deleted since associated note '${noteId}' is deleted.`);
} else {
logError(`Note attachment '${attachmentId}' is not deleted even though associated note '${noteId}' is deleted.`)
logError(`Attachment '${attachmentId}' is not deleted even though associated note '${noteId}' is deleted.`)
}
});
}