attachment ETAPI support WIP

This commit is contained in:
zadam 2023-06-05 09:23:42 +02:00
parent 49241ab318
commit 3b3f6082a7
19 changed files with 229 additions and 83 deletions

View file

@ -29,12 +29,12 @@ function dumpDocument(documentPath, targetPath, options) {
function dumpNote(targetPath, noteId) {
console.log(`Reading note '${noteId}'`);
let childTargetPath, note, fileNameWithPath;
let childTargetPath, noteRow, fileNameWithPath;
try {
note = sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
noteRow = sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
if (note.isDeleted) {
if (noteRow.isDeleted) {
stats.deleted++;
if (!options.includeDeleted) {
@ -44,13 +44,13 @@ function dumpDocument(documentPath, targetPath, options) {
}
}
if (note.isProtected) {
if (noteRow.isProtected) {
stats.protected++;
note.title = decryptService.decryptString(dataKey, note.title);
noteRow.title = decryptService.decryptString(dataKey, noteRow.title);
}
let safeTitle = sanitize(note.title);
let safeTitle = sanitize(noteRow.title);
if (safeTitle.length > 20) {
safeTitle = safeTitle.substring(0, 20);
@ -64,8 +64,8 @@ function dumpDocument(documentPath, targetPath, options) {
existingPaths[childTargetPath] = true;
if (note.noteId in noteIdToPath) {
const message = `Note '${noteId}' has been already dumped to ${noteIdToPath[note.noteId]}`;
if (noteRow.noteId in noteIdToPath) {
const message = `Note '${noteId}' has been already dumped to ${noteIdToPath[noteRow.noteId]}`;
console.log(message);
@ -74,16 +74,16 @@ function dumpDocument(documentPath, targetPath, options) {
return;
}
let {content} = sql.getRow("SELECT content FROM blobs WHERE blobId = ?", [note.blobId]);
let {content} = sql.getRow("SELECT content FROM blobs WHERE blobId = ?", [noteRow.blobId]);
if (content !== null && note.isProtected && dataKey) {
if (content !== null && noteRow.isProtected && dataKey) {
content = decryptService.decrypt(dataKey, content);
}
if (isContentEmpty(content)) {
console.log(`Note '${noteId}' is empty, skipping.`);
} else {
fileNameWithPath = extensionService.getFileName(note, childTargetPath, safeTitle);
fileNameWithPath = extensionService.getFileName(noteRow, childTargetPath, safeTitle);
fs.writeFileSync(fileNameWithPath, content);

View file

@ -187,7 +187,7 @@ class BBranch extends AbstractBeccaEntity {
// first delete children and then parent - this will show up better in recent changes
log.info(`Deleting note ${note.noteId}`);
log.info(`Deleting note '${note.noteId}'`);
this.becca.notes[note.noteId].isBeingDeleted = true;

View file

@ -1549,6 +1549,8 @@ class BNote extends AbstractBeccaEntity {
}
get isDeleted() {
// isBeingDeleted is relevant only in the transition period when the deletion process have begun, but not yet
// finished (note is still in becca)
return !(this.noteId in this.becca.notes) || this.isBeingDeleted;
}
@ -1602,7 +1604,7 @@ class BNote extends AbstractBeccaEntity {
/**
* @returns {BAttachment}
*/
saveAttachment({attachmentId, role, mime, title, content}) {
saveAttachment({attachmentId, role, mime, title, content, position}) {
let attachment;
if (attachmentId) {
@ -1613,15 +1615,13 @@ class BNote extends AbstractBeccaEntity {
title,
role,
mime,
isProtected: this.isProtected
isProtected: this.isProtected,
position
});
}
if (content !== undefined && content !== null) {
attachment.setContent(content, {forceSave: true});
} else {
attachment.save();
}
content = content || "";
attachment.setContent(content, {forceSave: true});
return attachment;
}

104
src/etapi/attachments.js Normal file
View file

@ -0,0 +1,104 @@
const becca = require("../becca/becca");
const eu = require("./etapi_utils");
const mappers = require("./mappers");
const v = require("./validators");
const utils = require("../services/utils.js");
const noteService = require("../services/notes.js");
function register(router) {
const ALLOWED_PROPERTIES_FOR_CREATE_ATTACHMENT = {
'parentId': [v.notNull, v.isNoteId],
'role': [v.notNull, v.isString],
'mime': [v.notNull, v.isString],
'title': [v.notNull, v.isString],
'position': [v.notNull, v.isInteger],
'content': [v.isString],
};
eu.route(router, 'post' ,'/etapi/attachments', (req, res, next) => {
const params = {};
eu.validateAndPatch(params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_ATTACHMENT);
try {
const note = becca.getNoteOrThrow(params.parentId);
const attachment = note.saveAttachment(params);
res.status(201).json(mappers.mapAttachmentToPojo(attachment));
}
catch (e) {
throw new eu.EtapiError(500, eu.GENERIC_CODE, e.message);
}
});
eu.route(router, 'get', '/etapi/attachments/:attachmentId', (req, res, next) => {
const attachment = eu.getAndCheckAttachment(req.params.attachmentId);
res.json(mappers.mapAttachmentToPojo(attachment));
});
const ALLOWED_PROPERTIES_FOR_PATCH = {
'role': [v.notNull, v.isString],
'mime': [v.notNull, v.isString],
'title': [v.notNull, v.isString],
'position': [v.notNull, v.isInteger],
};
eu.route(router, 'patch' ,'/etapi/attachments/:attachmentId', (req, res, next) => {
const attachment = eu.getAndCheckAttachment(req.params.attachmentId);
if (attachment.isProtected) {
throw new eu.EtapiError(400, "ATTACHMENT_IS_PROTECTED", `Attachment '${req.params.attachmentId}' is protected and cannot be modified through ETAPI.`);
}
eu.validateAndPatch(attachment, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
attachment.save();
res.json(mappers.mapAttachmentToPojo(attachment));
});
eu.route(router, 'get', '/etapi/attachments/:attachmentId/content', (req, res, next) => {
const attachment = eu.getAndCheckAttachment(req.params.attachmentId);
if (attachment.isProtected) {
throw new eu.EtapiError(400, "ATTACHMENT_IS_PROTECTED", `Attachment '${req.params.attachmentId}' is protected and content cannot be read through ETAPI.`);
}
const filename = utils.formatDownloadTitle(attachment.title, attachment.type, attachment.mime);
res.setHeader('Content-Disposition', utils.getContentDisposition(filename));
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.setHeader('Content-Type', attachment.mime);
res.send(attachment.getContent());
});
eu.route(router, 'put', '/etapi/attachments/:attachmentId/content', (req, res, next) => {
const attachment = eu.getAndCheckAttachment(req.params.attachmentId);
if (attachment.isProtected) {
throw new eu.EtapiError(400, "ATTACHMENT_IS_PROTECTED", `Attachment '${req.params.attachmentId}' is protected and cannot be modified through ETAPI.`);
}
attachment.setContent(req.body);
return res.sendStatus(204);
});
eu.route(router, 'delete' ,'/etapi/attachments/:attachmentId', (req, res, next) => {
const attachment = becca.getAttachment(req.params.attachmentId);
if (!attachment) {
return res.sendStatus(204);
}
attachment.markAsDeleted();
res.sendStatus(204);
});
}
module.exports = {
register
};

View file

@ -68,7 +68,7 @@ function register(router) {
eu.route(router, 'delete' ,'/etapi/attributes/:attributeId', (req, res, next) => {
const attribute = becca.getAttribute(req.params.attributeId);
if (!attribute || attribute.isDeleted) {
if (!attribute) {
return res.sendStatus(204);
}

View file

@ -64,7 +64,7 @@ function register(router) {
eu.route(router, 'delete' ,'/etapi/branches/:branchId', (req, res, next) => {
const branch = becca.getBranch(req.params.branchId);
if (!branch || branch.isDeleted) {
if (!branch) {
return res.sendStatus(204);
}

View file

@ -77,7 +77,18 @@ function getAndCheckNote(noteId) {
return note;
}
else {
throw new EtapiError(404, "NOTE_NOT_FOUND", `Note '${noteId}' not found`);
throw new EtapiError(404, "NOTE_NOT_FOUND", `Note '${noteId}' not found.`);
}
}
function getAndCheckAttachment(attachmentId) {
const attachment = becca.getAttachment(attachmentId, {includeContentLength: true});
if (attachment) {
return attachment;
}
else {
throw new EtapiError(404, "ATTACHMENT_NOT_FOUND", `Attachment '${attachmentId}' not found.`);
}
}
@ -88,7 +99,7 @@ function getAndCheckBranch(branchId) {
return branch;
}
else {
throw new EtapiError(404, "BRANCH_NOT_FOUND", `Branch '${branchId}' not found`);
throw new EtapiError(404, "BRANCH_NOT_FOUND", `Branch '${branchId}' not found.`);
}
}
@ -99,7 +110,7 @@ function getAndCheckAttribute(attributeId) {
return attribute;
}
else {
throw new EtapiError(404, "ATTRIBUTE_NOT_FOUND", `Attribute '${attributeId}' not found`);
throw new EtapiError(404, "ATTRIBUTE_NOT_FOUND", `Attribute '${attributeId}' not found.`);
}
}
@ -113,7 +124,7 @@ function validateAndPatch(target, source, allowedProperties) {
const validationResult = validator(source[key]);
if (validationResult) {
throw new EtapiError(400, "PROPERTY_VALIDATION_ERROR", `Validation failed on property '${key}': ${validationResult}`);
throw new EtapiError(400, "PROPERTY_VALIDATION_ERROR", `Validation failed on property '${key}': ${validationResult}.`);
}
}
}
@ -134,5 +145,6 @@ module.exports = {
validateAndPatch,
getAndCheckNote,
getAndCheckBranch,
getAndCheckAttribute
getAndCheckAttribute,
getAndCheckAttachment
}

View file

@ -46,8 +46,26 @@ function mapAttributeToPojo(attr) {
};
}
/** @param {BAttachment} attachment */
function mapAttachmentToPojo(attachment) {
return {
attachmentId: attachment.attachmentId,
parentId: attachment.parentId,
role: attachment.role,
mime: attachment.mime,
title: attachment.title,
position: attachment.position,
blobId: attachment.blobId,
dateModified: attachment.dateModified,
utcDateModified: attachment.utcDateModified,
utcDateScheduledForErasureSince: attachment.utcDateScheduledForErasureSince,
contentLength: attachment.contentLength
};
}
module.exports = {
mapNoteToPojo,
mapBranchToPojo,
mapAttributeToPojo
mapAttributeToPojo,
mapAttachmentToPojo
};

View file

@ -14,7 +14,7 @@ function register(router) {
const {search} = req.query;
if (!search?.trim()) {
throw new eu.EtapiError(400, 'SEARCH_QUERY_PARAM_MANDATORY', "'search' query parameter is mandatory");
throw new eu.EtapiError(400, 'SEARCH_QUERY_PARAM_MANDATORY', "'search' query parameter is mandatory.");
}
const searchParams = parseSearchParams(req);
@ -78,10 +78,10 @@ function register(router) {
};
eu.route(router, 'patch' ,'/etapi/notes/:noteId', (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId)
const note = eu.getAndCheckNote(req.params.noteId);
if (note.isProtected) {
throw new eu.EtapiError(400, "NOTE_IS_PROTECTED", `Note '${req.params.noteId}' is protected and cannot be modified through ETAPI`);
throw new eu.EtapiError(400, "NOTE_IS_PROTECTED", `Note '${req.params.noteId}' is protected and cannot be modified through ETAPI.`);
}
eu.validateAndPatch(note, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
@ -95,7 +95,7 @@ function register(router) {
const note = becca.getNote(noteId);
if (!note || note.isDeleted) {
if (!note) {
return res.sendStatus(204);
}
@ -107,6 +107,10 @@ function register(router) {
eu.route(router, 'get', '/etapi/notes/:noteId/content', (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
if (note.isProtected) {
throw new eu.EtapiError(400, "NOTE_IS_PROTECTED", `Note '${req.params.noteId}' is protected and content cannot be read through ETAPI.`);
}
const filename = utils.formatDownloadTitle(note.title, note.type, note.mime);
res.setHeader('Content-Disposition', utils.getContentDisposition(filename));
@ -120,6 +124,10 @@ function register(router) {
eu.route(router, 'put', '/etapi/notes/:noteId/content', (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
if (note.isProtected) {
throw new eu.EtapiError(400, "NOTE_IS_PROTECTED", `Note '${req.params.noteId}' is protected and cannot be modified through ETAPI.`);
}
note.setContent(req.body);
noteService.asyncPostProcessContent(note, req.body);
@ -132,7 +140,7 @@ function register(router) {
const format = req.query.format || "html";
if (!["html", "markdown"].includes(format)) {
throw new eu.EtapiError(400, "UNRECOGNIZED_EXPORT_FORMAT", `Unrecognized export format '${format}', supported values are 'html' (default) or 'markdown'`);
throw new eu.EtapiError(400, "UNRECOGNIZED_EXPORT_FORMAT", `Unrecognized export format '${format}', supported values are 'html' (default) or 'markdown'.`);
}
const taskContext = new TaskContext('no-progress-reporting');
@ -153,6 +161,15 @@ function register(router) {
return res.sendStatus(204);
});
eu.route(router, 'get', '/etapi/notes/:noteId/attachments', (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
const attachments = note.getAttachments({includeContentLength: true})
res.json(
attachments.map(attachment => mappers.mapAttachmentToPojo(attachment))
);
});
}
function parseSearchParams(req) {
@ -186,7 +203,7 @@ function parseBoolean(obj, name) {
}
if (!['true', 'false'].includes(obj[name])) {
throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse boolean '${name}' value '${obj[name]}, allowed values are 'true' and 'false'`);
throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse boolean '${name}' value '${obj[name]}, allowed values are 'true' and 'false'.`);
}
return obj[name] === 'true';
@ -200,7 +217,7 @@ function parseOrderDirection(obj, name) {
const integer = parseInt(obj[name]);
if (!['asc', 'desc'].includes(obj[name])) {
throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse order direction value '${obj[name]}, allowed values are 'asc' and 'desc'`);
throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse order direction value '${obj[name]}, allowed values are 'asc' and 'desc'.`);
}
return integer;
@ -214,7 +231,7 @@ function parseInteger(obj, name) {
const integer = parseInt(obj[name]);
if (Number.isNaN(integer)) {
throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse integer '${name}' value '${obj[name]}`);
throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse integer '${name}' value '${obj[name]}'.`);
}
return integer;

View file

@ -149,7 +149,7 @@ function getEditedNotesOnDate(req) {
}
return notes.map(note => {
const notePath = note.isDeleted ? null : getNotePathData(note);
const notePath = getNotePathData(note);
const notePojo = note.getPojo();
notePojo.notePath = notePath ? notePath.notePath : null;

View file

@ -11,8 +11,8 @@ const ValidationError = require("../../errors/validation_error");
function searchFromNote(req) {
const note = becca.getNoteOrThrow(req.params.noteId);
if (note.isDeleted) {
// this can be triggered from recent changes, and it's harmless to return empty list rather than fail
if (!note) {
// this can be triggered from recent changes, and it's harmless to return an empty list rather than fail
return [];
}
@ -26,8 +26,8 @@ function searchFromNote(req) {
function searchAndExecute(req) {
const note = becca.getNoteOrThrow(req.params.noteId);
if (note.isDeleted) {
// this can be triggered from recent changes, and it's harmless to return empty list rather than fail
if (!note) {
// this can be triggered from recent changes, and it's harmless to return an empty list rather than fail
return [];
}

View file

@ -63,6 +63,7 @@ const shareRoutes = require('../share/routes');
const etapiAuthRoutes = require('../etapi/auth');
const etapiAppInfoRoutes = require('../etapi/app_info');
const etapiAttachmentRoutes = require('../etapi/attachments');
const etapiAttributeRoutes = require('../etapi/attributes');
const etapiBranchRoutes = require('../etapi/branches');
const etapiNoteRoutes = require('../etapi/notes');
@ -332,6 +333,7 @@ function register(app) {
etapiAuthRoutes.register(router, [loginRateLimiter]);
etapiAppInfoRoutes.register(router);
etapiAttachmentRoutes.register(router);
etapiAttributeRoutes.register(router);
etapiBranchRoutes.register(router);
etapiNoteRoutes.register(router);

View file

@ -134,7 +134,7 @@ function executeActions(note, searchResultNoteIds) {
for (const resultNoteId of searchResultNoteIds) {
const resultNote = becca.getNote(resultNoteId);
if (!resultNote || resultNote.isDeleted) {
if (!resultNote) {
continue;
}

View file

@ -9,6 +9,10 @@ const beccaService = require("../becca/becca_service");
const log = require("./log");
function cloneNoteToParentNote(noteId, parentNoteId, prefix) {
if (!(noteId in becca.notes) || !(parentNoteId in becca.notes)) {
return { success: false, message: 'Note cannot be cloned because either the cloned note or the intended parent is deleted.' };
}
const parentNote = becca.getNote(parentNoteId);
if (parentNote.type === 'search') {
@ -18,10 +22,6 @@ function cloneNoteToParentNote(noteId, parentNoteId, prefix) {
};
}
if (isNoteDeleted(noteId) || isNoteDeleted(parentNoteId)) {
return { success: false, message: 'Note cannot be cloned because either the cloned note or the intended parent is deleted.' };
}
const validationResult = treeService.validateParentChild(parentNoteId, noteId);
if (!validationResult.success) {
@ -174,12 +174,6 @@ function cloneNoteAfter(noteId, afterBranchId) {
return { success: true, branchId: branch.branchId };
}
function isNoteDeleted(noteId) {
const note = becca.getNote(noteId);
return !note || note.isDeleted;
}
module.exports = {
cloneNoteToBranch,
cloneNoteToParentNote,

View file

@ -607,16 +607,16 @@ class ConsistencyChecks {
WHERE
entity_changes.id IS NULL`,
({entityId}) => {
const entity = sql.getRow(`SELECT * FROM ${entityName} WHERE ${key} = ?`, [entityId]);
const entityRow = sql.getRow(`SELECT * FROM ${entityName} WHERE ${key} = ?`, [entityId]);
if (this.autoFix) {
entityChangesService.addEntityChange({
entityName,
entityId,
hash: utils.randomString(10), // doesn't matter, will force sync, but that's OK
isErased: !!entity.isErased,
utcDateChanged: entity.utcDateModified || entity.utcDateCreated,
isSynced: entityName !== 'options' || entity.isSynced
isErased: !!entityRow.isErased,
utcDateChanged: entityRow.utcDateModified || entityRow.utcDateCreated,
isSynced: entityName !== 'options' || entityRow.isSynced
});
logFix(`Created missing entity change for entityName '${entityName}', entityId '${entityId}'`);

View file

@ -570,7 +570,7 @@ function downloadImages(noteId, content) {
for (const url in imageUrlToAttachmentIdMapping) {
const imageNote = imageNotes.find(note => note.noteId === imageUrlToAttachmentIdMapping[url]);
if (imageNote && !imageNote.isDeleted) {
if (imageNote) {
updatedContent = replaceUrl(updatedContent, url, imageNote);
}
}
@ -697,14 +697,14 @@ function updateNoteData(noteId, content) {
* @param {TaskContext} taskContext
*/
function undeleteNote(noteId, taskContext) {
const note = sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
const noteRow = sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
if (!note.isDeleted) {
if (!noteRow.isDeleted) {
log.error(`Note '${noteId}' is not deleted and thus cannot be undeleted.`);
return;
}
const undeletedParentBranchIds = getUndeletedParentBranchIds(noteId, note.deleteId);
const undeletedParentBranchIds = getUndeletedParentBranchIds(noteId, noteRow.deleteId);
if (undeletedParentBranchIds.length === 0) {
// cannot undelete if there's no undeleted parent
@ -712,7 +712,7 @@ function undeleteNote(noteId, taskContext) {
}
for (const parentBranchId of undeletedParentBranchIds) {
undeleteBranch(parentBranchId, note.deleteId, taskContext);
undeleteBranch(parentBranchId, noteRow.deleteId, taskContext);
}
}
@ -722,38 +722,38 @@ function undeleteNote(noteId, taskContext) {
* @param {TaskContext} taskContext
*/
function undeleteBranch(branchId, deleteId, taskContext) {
const branch = sql.getRow("SELECT * FROM branches WHERE branchId = ?", [branchId])
const branchRow = sql.getRow("SELECT * FROM branches WHERE branchId = ?", [branchId])
if (!branch.isDeleted) {
if (!branchRow.isDeleted) {
return;
}
const note = sql.getRow("SELECT * FROM notes WHERE noteId = ?", [branch.noteId]);
const noteRow = sql.getRow("SELECT * FROM notes WHERE noteId = ?", [branchRow.noteId]);
if (note.isDeleted && note.deleteId !== deleteId) {
if (noteRow.isDeleted && noteRow.deleteId !== deleteId) {
return;
}
new BBranch(branch).save();
new BBranch(branchRow).save();
taskContext.increaseProgressCount();
if (note.isDeleted && note.deleteId === deleteId) {
if (noteRow.isDeleted && noteRow.deleteId === deleteId) {
// becca entity was already created as skeleton in "new Branch()" above
const noteEntity = becca.getNote(note.noteId);
noteEntity.updateFromRow(note);
const noteEntity = becca.getNote(noteRow.noteId);
noteEntity.updateFromRow(noteRow);
noteEntity.save();
const attributes = sql.getRows(`
const attributeRows = sql.getRows(`
SELECT * FROM attributes
WHERE isDeleted = 1
AND deleteId = ?
AND (noteId = ?
OR (type = 'relation' AND value = ?))`, [deleteId, note.noteId, note.noteId]);
OR (type = 'relation' AND value = ?))`, [deleteId, noteRow.noteId, noteRow.noteId]);
for (const attribute of attributes) {
for (const attributeRow of attributeRows) {
// relation might point to a note which hasn't been undeleted yet and would thus throw up
new BAttribute(attribute).save({skipValidation: true});
new BAttribute(attributeRow).save({skipValidation: true});
}
const childBranchIds = sql.getColumn(`
@ -761,7 +761,7 @@ function undeleteBranch(branchId, deleteId, taskContext) {
FROM branches
WHERE branches.isDeleted = 1
AND branches.deleteId = ?
AND branches.parentNoteId = ?`, [deleteId, note.noteId]);
AND branches.parentNoteId = ?`, [deleteId, noteRow.noteId]);
for (const childBranchId of childBranchIds) {
undeleteBranch(childBranchId, deleteId, taskContext);

View file

@ -155,7 +155,6 @@ function findResultsWithExpression(expression, searchContext) {
const noteSet = expression.execute(allNoteSet, executionContext, searchContext);
const searchResults = noteSet.notes
.filter(note => !note.isDeleted)
.map(note => {
const notePathArray = executionContext.noteIdToNotePath[note.noteId] || note.getBestNotePath();

View file

@ -315,21 +315,21 @@ function getEntityChangeRow(entityName, entityId) {
throw new Error(`Unknown entity '${entityName}'`);
}
const entity = sql.getRow(`SELECT * FROM ${entityName} WHERE ${primaryKey} = ?`, [entityId]);
const entityRow = sql.getRow(`SELECT * FROM ${entityName} WHERE ${primaryKey} = ?`, [entityId]);
if (!entity) {
if (!entityRow) {
throw new Error(`Entity ${entityName} '${entityId}' not found.`);
}
if (entityName === 'blobs' && entity.content !== null) {
if (typeof entity.content === 'string') {
entity.content = Buffer.from(entity.content, 'utf-8');
if (entityName === 'blobs' && entityRow.content !== null) {
if (typeof entityRow.content === 'string') {
entityRow.content = Buffer.from(entityRow.content, 'utf-8');
}
entity.content = entity.content.toString("base64");
entityRow.content = entityRow.content.toString("base64");
}
return entity;
return entityRow;
}
}

View file

@ -199,7 +199,7 @@ function sortNotesIfNeeded(parentNoteId) {
function setNoteToParent(noteId, prefix, parentNoteId) {
const parentNote = becca.getNote(parentNoteId);
if (parentNote && parentNote.isDeleted) {
if (!parentNote) {
throw new Error(`Cannot move note to deleted parent note '${parentNoteId}'`);
}
@ -209,7 +209,7 @@ function setNoteToParent(noteId, prefix, parentNoteId) {
if (branch) {
if (!parentNoteId) {
log.info(`Removing note ${noteId} from parent ${parentNoteId}`);
log.info(`Removing note '${noteId}' from parent '${parentNoteId}'`);
branch.markAsDeleted();
}