Source: becca/entities/note_revision.js

"use strict";

const protectedSessionService = require('../../services/protected_session');
const utils = require('../../services/utils');
const sql = require('../../services/sql');
const dateUtils = require('../../services/date_utils');
const becca = require('../becca');
const entityChangesService = require('../../services/entity_changes');
const AbstractEntity = require("./abstract_entity");

/**
 * NoteRevision represents snapshot of note's title and content at some point in the past.
 * It's used for seamless note versioning.
 *
 * @extends AbstractEntity
 */
class NoteRevision extends AbstractEntity {
    static get entityName() { return "note_revisions"; }
    static get primaryKeyName() { return "noteRevisionId"; }
    static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "isProtected", "dateLastEdited", "dateCreated", "utcDateLastEdited", "utcDateCreated", "utcDateModified"]; }

    constructor(row, titleDecrypted = false) {
        super();

        /** @type {string} */
        this.noteRevisionId = row.noteRevisionId;
        /** @type {string} */
        this.noteId = row.noteId;
        /** @type {string} */
        this.type = row.type;
        /** @type {string} */
        this.mime = row.mime;
        /** @type {boolean} */
        this.isProtected = !!row.isProtected;
        /** @type {string} */
        this.title = row.title;
        /** @type {string} */
        this.dateLastEdited = row.dateLastEdited;
        /** @type {string} */
        this.dateCreated = row.dateCreated;
        /** @type {string} */
        this.utcDateLastEdited = row.utcDateLastEdited;
        /** @type {string} */
        this.utcDateCreated = row.utcDateCreated;
        /** @type {string} */
        this.utcDateModified = row.utcDateModified;
        /** @type {number} */
        this.contentLength = row.contentLength;

        if (this.isProtected && !titleDecrypted) {
            this.title = protectedSessionService.isProtectedSessionAvailable()
                ? protectedSessionService.decryptString(this.title)
                : "[protected]";
        }
    }

    getNote() {
        return becca.notes[this.noteId];
    }

    /** @returns {boolean} true if the note has string content (not binary) */
    isStringNote() {
        return utils.isStringNote(this.type, this.mime);
    }

    /*
     * Note revision content has quite special handling - it's not a separate entity, but a lazily loaded
     * part of NoteRevision entity with its own sync. Reason behind this hybrid design is that
     * 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.
     *
     * This is the same approach as is used for Note's content.
     */

    /** @returns {*} */
    getContent(silentNotFoundError = false) {
        const res = sql.getRow(`SELECT content FROM note_revision_contents WHERE noteRevisionId = ?`, [this.noteRevisionId]);

        if (!res) {
            if (silentNotFoundError) {
                return undefined;
            }
            else {
                throw new Error("Cannot find note revision content for noteRevisionId=" + this.noteRevisionId);
            }
        }

        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;
        }
    }

    setContent(content, ignoreMissingProtectedSession = false) {
        const pojo = {
            noteRevisionId: this.noteRevisionId,
            content: content,
            utcDateModified: dateUtils.utcNowDateTime()
        };

        if (this.isProtected) {
            if (protectedSessionService.isProtectedSessionAvailable()) {
                pojo.content = protectedSessionService.encrypt(pojo.content);
            }
            else if (!ignoreMissingProtectedSession) {
                throw new Error(`Cannot update content of noteRevisionId=${this.noteRevisionId} since we're out of protected session.`);
            }
        }

        sql.upsert("note_revision_contents", "noteRevisionId", pojo);

        const hash = utils.hash(this.noteRevisionId + "|" + pojo.content.toString());

        entityChangesService.addEntityChange({
            entityName: 'note_revision_contents',
            entityId: this.noteRevisionId,
            hash: hash,
            isErased: false,
            utcDateChanged: this.getUtcDateChanged(),
            isSynced: true
        });
    }

    /** @returns {{contentLength, dateModified, utcDateModified}} */
    getContentMetadata() {
        return sql.getRow(`
            SELECT 
                LENGTH(content) AS contentLength, 
                dateModified,
                utcDateModified 
            FROM note_revision_contents 
            WHERE noteRevisionId = ?`, [this.noteRevisionId]);
    }

    beforeSaving() {
        super.beforeSaving();

        this.utcDateModified = dateUtils.utcNowDateTime();
    }

    getPojo() {
        return {
            noteRevisionId: this.noteRevisionId,
            noteId: this.noteId,
            type: this.type,
            mime: this.mime,
            isProtected: this.isProtected,
            title: this.title,
            dateLastEdited: this.dateLastEdited,
            dateCreated: this.dateCreated,
            utcDateLastEdited: this.utcDateLastEdited,
            utcDateCreated: this.utcDateCreated,
            utcDateModified: this.utcDateModified,
            content: this.content,
            contentLength: this.contentLength
        };
    }

    getPojoToSave() {
        const pojo = this.getPojo();
        delete pojo.content; // not getting persisted
        delete pojo.contentLength; // not getting persisted

        if (pojo.isProtected) {
            if (protectedSessionService.isProtectedSessionAvailable()) {
                pojo.title = protectedSessionService.encrypt(this.title);
            }
            else {
                // updating protected note outside of protected session means we will keep original ciphertexts
                delete pojo.title;
            }
        }

        return pojo;
    }
}

module.exports = NoteRevision;