getting rid of note complement WIP

This commit is contained in:
zadam 2023-05-05 16:37:39 +02:00
parent 9cdcbb3125
commit 5e1f81e53e
23 changed files with 163 additions and 90 deletions

View File

@ -137,6 +137,15 @@ class Becca {
.map(row => new BAttachment(row));
}
/** @returns {BBlob|null} */
getBlob(blobId) {
const row = sql.getRow("SELECT *, LENGTH(content) AS contentLength " +
"FROM blob WHERE blobId = ?", [blobId]);
const BBlob = require("./entities/bblob"); // avoiding circular dependency problems
return row ? new BBlob(row) : null;
}
/** @returns {BOption|null} */
getOption(name) {
return this.options[name];
@ -161,6 +170,8 @@ class Becca {
return this.getNoteRevision(entityId);
} else if (entityName === 'attachments') {
return this.getAttachment(entityId);
} else if (entityName === 'blobs') {
return this.getBlob(entityId);
}
const camelCaseEntityName = entityName.toLowerCase().replace(/(_[a-z])/g,

View File

@ -142,7 +142,7 @@ class AbstractBeccaEntity {
throw new Error(`Cannot set null content to ${this.constructor.primaryKeyName} '${this[this.constructor.primaryKeyName]}'`);
}
if (this.isStringNote()) {
if (this.hasStringContent()) {
content = content.toString();
}
else {
@ -246,7 +246,7 @@ class AbstractBeccaEntity {
}
}
if (this.isStringNote()) {
if (this.hasStringContent()) {
return content === null
? ""
: content.toString("UTF-8");

View File

@ -76,7 +76,7 @@ class BAttachment extends AbstractBeccaEntity {
}
/** @returns {boolean} true if the note has string content (not binary) */
isStringNote() {
hasStringContent() {
return utils.isStringNote(this.type, this.mime);
}

View File

@ -0,0 +1,26 @@
class BBlob {
constructor(row) {
/** @type {string} */
this.blobId = row.blobId;
/** @type {string|Buffer} */
this.content = row.content;
/** @type {number} */
this.contentLength = row.contentLength;
/** @type {string} */
this.dateModified = row.dateModified;
/** @type {string} */
this.utcDateModified = row.utcDateModified;
}
getPojo() {
return {
blobId: this.blobId,
content: this.content,
contentLength: this.contentLength,
dateModified: this.dateModified,
utcDateModified: this.utcDateModified
};
}
}
module.exports = BBlob;

View File

@ -103,6 +103,7 @@ class BBranch extends AbstractBeccaEntity {
return this.becca.notes[this.noteId];
}
/** @returns {BNote} */
getNote() {
return this.childNote;
}

View File

@ -301,8 +301,13 @@ class BNote extends AbstractBeccaEntity {
|| (this.type === 'file' && this.mime?.startsWith('image/'));
}
/** @returns {boolean} true if the note has string content (not binary) */
/** @deprecated use hasStringContent() instead */
isStringNote() {
return this.hasStringContent();
}
/** @returns {boolean} true if the note has string content (not binary) */
hasStringContent() {
return utils.isStringNote(this.type, this.mime);
}

View File

@ -61,7 +61,7 @@ class BNoteRevision extends AbstractBeccaEntity {
}
/** @returns {boolean} true if the note has string content (not binary) */
isStringNote() {
hasStringContent() {
return utils.isStringNote(this.type, this.mime);
}

View File

@ -166,15 +166,6 @@ class NoteContext extends Component {
return this.notePath ? this.notePath.split('/') : [];
}
/** @returns {FNoteComplement} */
async getNoteComplement() {
if (!this.noteId) {
return null;
}
return await froca.getNoteComplement(this.noteId);
}
isActive() {
return appContext.tabManager.activeNtxId === this.ntxId;
}

View File

@ -30,6 +30,14 @@ class FAttachment {
getNote() {
return this.froca.notes[this.parentId];
}
/**
* @param [opts.full=false] - force retrieval of the full note
* @return {FBlob}
*/
async getBlob(opts = {}) {
return await this.froca.getBlob('attachments', this.attachmentId, opts);
}
}
export default FAttachment;

View File

@ -0,0 +1,17 @@
class FBlob {
constructor(row) {
/** @type {string} */
this.blobId = row.blobId;
/**
* can either contain the whole content (in e.g. string notes), only part (large text notes) or nothing at all (binary notes, images)
* @type {string}
*/
this.content = row.content;
/** @type {string} */
this.dateModified = row.dateModified;
/** @type {string} */
this.utcDateModified = row.utcDateModified;
}
}

View File

@ -851,13 +851,17 @@ class FNote {
return await this.froca.getNotes(targetRelations.map(tr => tr.noteId));
}
/**
* Return note complement which is most importantly note's content
*
* @returns {Promise<FNoteComplement>}
*/
/** @deprecated use getBlob() instead */
async getNoteComplement() {
return await this.froca.getNoteComplement(this.noteId);
return this.getBlob({ full: true });
}
/**
* @param [opts.full=false] - force retrieval of the full note
* @return {FBlob}
*/
async getBlob(opts = {}) {
return await this.froca.getBlob('notes', this.noteId, opts);
}
toString() {

View File

@ -1,41 +0,0 @@
/**
* FIXME: probably make it a FBlob
* Complements the FNote with the main note content and other extra attributes
*/
class FNoteComplement {
constructor(row) {
/** @type {string} */
this.noteId = row.noteId;
/**
* can either contain the whole content (in e.g. string notes), only part (large text notes) or nothing at all (binary notes, images)
* @type {string}
*/
this.content = row.content;
/** @type {int} */
this.contentLength = row.contentLength;
/** @type {string} */
this.dateCreated = row.dateCreated;
/** @type {string} */
this.dateModified = row.dateModified;
/** @type {string} */
this.utcDateCreated = row.utcDateCreated;
/** @type {string} */
this.utcDateModified = row.utcDateModified;
// "combined" date modified give larger out of note's and blob's dateModified
/** @type {string} */
this.combinedDateModified = row.combinedDateModified;
/** @type {string} */
this.combinedUtcDateModified = row.combinedUtcDateModified;
}
}
export default FNoteComplement;

View File

@ -3,7 +3,7 @@ import FNote from "../entities/fnote.js";
import FAttribute from "../entities/fattribute.js";
import server from "./server.js";
import appContext from "../components/app_context.js";
import FNoteComplement from "../entities/fnote_complement.js";
import FBlob from "../entities/fblob.js";
import FAttachment from "../entities/fattachment.js";
/**
@ -38,8 +38,7 @@ class Froca {
/** @type {Object.<string, FAttachment>} */
this.attachments = {};
// FIXME
/** @type {Object.<string, Promise<FNoteComplement>>} */
/** @type {Object.<string, Promise<FBlob>>} */
this.blobPromises = {};
this.addResp(resp);
@ -321,25 +320,24 @@ class Froca {
return attachmentRow ? new FAttachment(this, attachmentRow) : null;
}
/**
* // FIXME
* @returns {Promise<FNoteComplement>}
*/
async getNoteComplement(noteId) {
if (!this.blobPromises[noteId]) {
this.blobPromises[noteId] = server.get(`notes/${noteId}`)
.then(row => new FNoteComplement(row))
.catch(e => console.error(`Cannot get note complement for note '${noteId}'`));
async getBlob(entityType, entityId, opts = {}) {
opts.full = !!opts.full;
const key = `${entityType}-${entityId}`;
if (!this.blobPromises[key]) {
this.blobPromises[key] = server.get(`${entityType}/${entityId}/blob?full=${opts.full}`)
.then(row => new FBlob(row))
.catch(e => console.error(`Cannot get blob for ${entityType} '${entityId}'`));
// we don't want to keep large payloads forever in memory, so we clean that up quite quickly
// this cache is more meant to share the data between different components within one business transaction (e.g. loading of the note into the tab context and all the components)
// this is also a workaround for missing invalidation after change
this.blobPromises[noteId].then(
() => setTimeout(() => this.blobPromises[noteId] = null, 1000)
this.blobPromises[key].then(
() => setTimeout(() => this.blobPromises[key] = null, 1000)
);
}
return await this.blobPromises[noteId];
return await this.blobPromises[key];
}
}

View File

@ -90,7 +90,7 @@ export default class TocWidget extends RightPanelWidget {
let $toc = "", headingCount = 0;
// Check for type text unconditionally in case alwaysShowWidget is set
if (this.note.type === 'text') {
const { content } = await note.getNoteComplement();
const { content } = await note.getBlob();
({$toc, headingCount} = await this.getToc(content));
}

View File

@ -1,6 +1,13 @@
const becca = require("../../becca/becca");
const NotFoundError = require("../../errors/not_found_error");
const utils = require("../../services/utils");
const blobService = require("../../services/blob.js");
function getAttachmentBlob(req) {
const full = req.query.full === 'true';
return blobService.getBlobPojo('attachments', req.params.attachmentId, { full });
}
function getAttachments(req) {
const includeContent = req.query.includeContent === 'true';
@ -87,6 +94,7 @@ function convertAttachmentToNote(req) {
}
module.exports = {
getAttachmentBlob,
getAttachments,
getAttachment,
saveAttachment,

View File

@ -1,13 +1,19 @@
"use strict";
const beccaService = require('../../becca/becca_service');
const protectedSessionService = require('../../services/protected_session');
const noteRevisionService = require('../../services/note_revisions');
const utils = require('../../services/utils');
const sql = require('../../services/sql');
const cls = require('../../services/cls');
const path = require('path');
const becca = require("../../becca/becca");
const blobService = require("../../services/blob.js");
function getNoteRevisionBlob(req) {
const full = req.query.full === 'true';
return blobService.getBlobPojo('note_revisions', req.params.noteRevisionId, { full });
}
function getNoteRevisions(req) {
return becca.getNoteRevisionsFromQuery(`
@ -20,10 +26,11 @@ function getNoteRevisions(req) {
}
function getNoteRevision(req) {
// FIXME
const noteRevision = becca.getNoteRevision(req.params.noteRevisionId);
if (noteRevision.type === 'file') {
if (noteRevision.isStringNote()) {
if (noteRevision.hasStringContent()) {
noteRevision.content = noteRevision.getContent().substr(0, 10000);
}
}
@ -180,6 +187,7 @@ function getNotePathData(note) {
}
module.exports = {
getNoteRevisionBlob,
getNoteRevisions,
getNoteRevision,
downloadNoteRevision,

View File

@ -10,20 +10,20 @@ const fs = require('fs');
const becca = require("../../becca/becca");
const ValidationError = require("../../errors/validation_error");
const NotFoundError = require("../../errors/not_found_error");
const blobService = require("../../services/blob");
function getNote(req) {
const noteId = req.params.noteId;
const note = becca.getNote(noteId);
const note = becca.getNote(req.params.noteId);
if (!note) {
throw new NotFoundError(`Note '${noteId}' has not been found.`);
throw new NotFoundError(`Note '${req.params.noteId}' has not been found.`);
}
const pojo = note.getPojo();
if (note.isStringNote()) {
if (note.hasStringContent()) {
pojo.content = note.getContent();
// FIXME: use blobs instead
if (note.type === 'file' && pojo.content.length > 10000) {
pojo.content = `${pojo.content.substr(0, 10000)}\r\n\r\n... and ${pojo.content.length - 10000} more characters.`;
}
@ -39,6 +39,12 @@ function getNote(req) {
return pojo;
}
function getNoteBlob(req) {
const full = req.query.full === 'true';
return blobService.getBlobPojo('notes', req.params.noteId, { full });
}
function createNote(req) {
const params = Object.assign({}, req.body); // clone
params.parentNoteId = req.params.parentNoteId;
@ -259,6 +265,7 @@ function convertNoteToAttachment(req) {
module.exports = {
getNote,
getNoteBlob,
updateNoteData,
deleteNote,
undeleteNote,

View File

@ -112,6 +112,7 @@ function register(app) {
apiRoute(PST, '/api/tree/load', treeApiRoute.load);
apiRoute(GET, '/api/notes/:noteId', notesApiRoute.getNote);
apiRoute(GET, '/api/notes/:noteId/blob', notesApiRoute.getNoteBlob);
apiRoute(PUT, '/api/notes/:noteId/data', notesApiRoute.updateNoteData);
apiRoute(DEL, '/api/notes/:noteId', notesApiRoute.deleteNote);
apiRoute(PUT, '/api/notes/:noteId/undelete', notesApiRoute.undeleteNote);
@ -153,6 +154,7 @@ function register(app) {
apiRoute(GET, '/api/attachments/:attachmentId', attachmentsApiRoute.getAttachment);
apiRoute(PST, '/api/attachments/:attachmentId/convert-to-note', attachmentsApiRoute.convertAttachmentToNote);
apiRoute(DEL, '/api/attachments/:attachmentId', attachmentsApiRoute.deleteAttachment);
apiRoute(GET, '/api/attachments/:attachmentId/blob', attachmentsApiRoute.getAttachmentBlob);
route(GET, '/api/attachments/:attachmentId/image/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnAttachedImage);
route(GET, '/api/attachments/:attachmentId/open', [auth.checkApiAuthOrElectron], filesRoute.openAttachment);
route(GET, '/api/attachments/:attachmentId/open-partial', [auth.checkApiAuthOrElectron],
@ -170,6 +172,7 @@ function register(app) {
apiRoute(GET, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.getNoteRevisions);
apiRoute(DEL, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.eraseAllNoteRevisions);
apiRoute(GET, '/api/revisions/:noteRevisionId', noteRevisionsApiRoute.getNoteRevision);
apiRoute(GET, '/api/revisions/:noteRevisionId/blob', noteRevisionsApiRoute.getNoteRevisionBlob);
apiRoute(DEL, '/api/revisions/:noteRevisionId', noteRevisionsApiRoute.eraseNoteRevision);
apiRoute(PST, '/api/revisions/:noteRevisionId/restore', noteRevisionsApiRoute.restoreNoteRevision);
route(GET, '/api/revisions/:noteRevisionId/download', [auth.checkApiAuthOrElectron], noteRevisionsApiRoute.downloadNoteRevision);

28
src/services/blob.js Normal file
View File

@ -0,0 +1,28 @@
const becca = require('../becca/becca');
const NotFoundError = require("../errors/not_found_error");
function getBlobPojo(entityName, entityId, opts = {}) {
opts.full = !!opts.full;
const entity = becca.getEntity(entityName, entityId);
if (!entity) {
throw new NotFoundError(`Entity ${entityName} '${entityId}' was not found.`);
}
const blob = becca.getBlob(entity.blobId);
const pojo = blob.getPojo();
if (!entity.hasStringContent()) {
pojo.content = null;
} else if (!opts.full && pojo.content.length > 10000) {
pojo.content = `${pojo.content.substr(0, 10000)}\r\n\r\n... and ${pojo.content.length - 10000} more characters.`;
}
return pojo;
}
module.exports = {
getBlobPojo
};

View File

@ -24,13 +24,13 @@ function exportToOpml(taskContext, branch, version, res) {
if (opmlVersion === 1) {
const preparedTitle = escapeXmlAttribute(title);
const preparedContent = note.isStringNote() ? prepareText(note.getContent()) : '';
const preparedContent = note.hasStringContent() ? prepareText(note.getContent()) : '';
res.write(`<outline title="${preparedTitle}" text="${preparedContent}">\n`);
}
else if (opmlVersion === 2) {
const preparedTitle = escapeXmlAttribute(title);
const preparedContent = note.isStringNote() ? escapeXmlAttribute(note.getContent()) : '';
const preparedContent = note.hasStringContent() ? escapeXmlAttribute(note.getContent()) : '';
res.write(`<outline text="${preparedTitle}" _note="${preparedContent}">\n`);
}

View File

@ -90,7 +90,7 @@ eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) =>
if (["text", "code"].includes(note.type)
// if the note has already content we're not going to overwrite it with template's one
&& (!content || content.trim().length === 0)
&& templateNote.isStringNote()) {
&& templateNote.hasStringContent()) {
const templateNoteContent = templateNote.getContent();

View File

@ -21,7 +21,6 @@ const htmlSanitizer = require("./html_sanitizer");
const ValidationError = require("../errors/validation_error");
const noteTypesService = require("./note_types");
const fs = require("fs");
const BAttachment = require("../becca/entities/battachment");
/** @param {BNote} parentNote */
function getNewNotePosition(parentNote) {

View File

@ -107,7 +107,7 @@ class SNote extends AbstractShacaEntity {
let content = row.content;
if (this.isStringNote()) {
if (this.hasStringContent()) {
return content === null
? ""
: content.toString("UTF-8");
@ -118,7 +118,7 @@ class SNote extends AbstractShacaEntity {
}
/** @returns {boolean} true if the note has string content (not binary) */
isStringNote() {
hasStringContent() {
return utils.isStringNote(this.type, this.mime);
}