mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
attachment ETAPI support WIP
This commit is contained in:
parent
49241ab318
commit
3b3f6082a7
@ -29,12 +29,12 @@ function dumpDocument(documentPath, targetPath, options) {
|
|||||||
function dumpNote(targetPath, noteId) {
|
function dumpNote(targetPath, noteId) {
|
||||||
console.log(`Reading note '${noteId}'`);
|
console.log(`Reading note '${noteId}'`);
|
||||||
|
|
||||||
let childTargetPath, note, fileNameWithPath;
|
let childTargetPath, noteRow, fileNameWithPath;
|
||||||
|
|
||||||
try {
|
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++;
|
stats.deleted++;
|
||||||
|
|
||||||
if (!options.includeDeleted) {
|
if (!options.includeDeleted) {
|
||||||
@ -44,13 +44,13 @@ function dumpDocument(documentPath, targetPath, options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note.isProtected) {
|
if (noteRow.isProtected) {
|
||||||
stats.protected++;
|
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) {
|
if (safeTitle.length > 20) {
|
||||||
safeTitle = safeTitle.substring(0, 20);
|
safeTitle = safeTitle.substring(0, 20);
|
||||||
@ -64,8 +64,8 @@ function dumpDocument(documentPath, targetPath, options) {
|
|||||||
|
|
||||||
existingPaths[childTargetPath] = true;
|
existingPaths[childTargetPath] = true;
|
||||||
|
|
||||||
if (note.noteId in noteIdToPath) {
|
if (noteRow.noteId in noteIdToPath) {
|
||||||
const message = `Note '${noteId}' has been already dumped to ${noteIdToPath[note.noteId]}`;
|
const message = `Note '${noteId}' has been already dumped to ${noteIdToPath[noteRow.noteId]}`;
|
||||||
|
|
||||||
console.log(message);
|
console.log(message);
|
||||||
|
|
||||||
@ -74,16 +74,16 @@ function dumpDocument(documentPath, targetPath, options) {
|
|||||||
return;
|
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);
|
content = decryptService.decrypt(dataKey, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isContentEmpty(content)) {
|
if (isContentEmpty(content)) {
|
||||||
console.log(`Note '${noteId}' is empty, skipping.`);
|
console.log(`Note '${noteId}' is empty, skipping.`);
|
||||||
} else {
|
} else {
|
||||||
fileNameWithPath = extensionService.getFileName(note, childTargetPath, safeTitle);
|
fileNameWithPath = extensionService.getFileName(noteRow, childTargetPath, safeTitle);
|
||||||
|
|
||||||
fs.writeFileSync(fileNameWithPath, content);
|
fs.writeFileSync(fileNameWithPath, content);
|
||||||
|
|
||||||
|
@ -187,7 +187,7 @@ class BBranch extends AbstractBeccaEntity {
|
|||||||
|
|
||||||
// first delete children and then parent - this will show up better in recent changes
|
// 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;
|
this.becca.notes[note.noteId].isBeingDeleted = true;
|
||||||
|
|
||||||
|
@ -1549,6 +1549,8 @@ class BNote extends AbstractBeccaEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get isDeleted() {
|
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;
|
return !(this.noteId in this.becca.notes) || this.isBeingDeleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1602,7 +1604,7 @@ class BNote extends AbstractBeccaEntity {
|
|||||||
/**
|
/**
|
||||||
* @returns {BAttachment}
|
* @returns {BAttachment}
|
||||||
*/
|
*/
|
||||||
saveAttachment({attachmentId, role, mime, title, content}) {
|
saveAttachment({attachmentId, role, mime, title, content, position}) {
|
||||||
let attachment;
|
let attachment;
|
||||||
|
|
||||||
if (attachmentId) {
|
if (attachmentId) {
|
||||||
@ -1613,15 +1615,13 @@ class BNote extends AbstractBeccaEntity {
|
|||||||
title,
|
title,
|
||||||
role,
|
role,
|
||||||
mime,
|
mime,
|
||||||
isProtected: this.isProtected
|
isProtected: this.isProtected,
|
||||||
|
position
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (content !== undefined && content !== null) {
|
content = content || "";
|
||||||
attachment.setContent(content, {forceSave: true});
|
attachment.setContent(content, {forceSave: true});
|
||||||
} else {
|
|
||||||
attachment.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
return attachment;
|
return attachment;
|
||||||
}
|
}
|
||||||
|
104
src/etapi/attachments.js
Normal file
104
src/etapi/attachments.js
Normal 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
|
||||||
|
};
|
@ -68,7 +68,7 @@ function register(router) {
|
|||||||
eu.route(router, 'delete' ,'/etapi/attributes/:attributeId', (req, res, next) => {
|
eu.route(router, 'delete' ,'/etapi/attributes/:attributeId', (req, res, next) => {
|
||||||
const attribute = becca.getAttribute(req.params.attributeId);
|
const attribute = becca.getAttribute(req.params.attributeId);
|
||||||
|
|
||||||
if (!attribute || attribute.isDeleted) {
|
if (!attribute) {
|
||||||
return res.sendStatus(204);
|
return res.sendStatus(204);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ function register(router) {
|
|||||||
eu.route(router, 'delete' ,'/etapi/branches/:branchId', (req, res, next) => {
|
eu.route(router, 'delete' ,'/etapi/branches/:branchId', (req, res, next) => {
|
||||||
const branch = becca.getBranch(req.params.branchId);
|
const branch = becca.getBranch(req.params.branchId);
|
||||||
|
|
||||||
if (!branch || branch.isDeleted) {
|
if (!branch) {
|
||||||
return res.sendStatus(204);
|
return res.sendStatus(204);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,7 +77,18 @@ function getAndCheckNote(noteId) {
|
|||||||
return note;
|
return note;
|
||||||
}
|
}
|
||||||
else {
|
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;
|
return branch;
|
||||||
}
|
}
|
||||||
else {
|
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;
|
return attribute;
|
||||||
}
|
}
|
||||||
else {
|
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]);
|
const validationResult = validator(source[key]);
|
||||||
|
|
||||||
if (validationResult) {
|
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,
|
validateAndPatch,
|
||||||
getAndCheckNote,
|
getAndCheckNote,
|
||||||
getAndCheckBranch,
|
getAndCheckBranch,
|
||||||
getAndCheckAttribute
|
getAndCheckAttribute,
|
||||||
|
getAndCheckAttachment
|
||||||
}
|
}
|
||||||
|
@ -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 = {
|
module.exports = {
|
||||||
mapNoteToPojo,
|
mapNoteToPojo,
|
||||||
mapBranchToPojo,
|
mapBranchToPojo,
|
||||||
mapAttributeToPojo
|
mapAttributeToPojo,
|
||||||
|
mapAttachmentToPojo
|
||||||
};
|
};
|
||||||
|
@ -14,7 +14,7 @@ function register(router) {
|
|||||||
const {search} = req.query;
|
const {search} = req.query;
|
||||||
|
|
||||||
if (!search?.trim()) {
|
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);
|
const searchParams = parseSearchParams(req);
|
||||||
@ -78,10 +78,10 @@ function register(router) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
eu.route(router, 'patch' ,'/etapi/notes/:noteId', (req, res, next) => {
|
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) {
|
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);
|
eu.validateAndPatch(note, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
|
||||||
@ -95,7 +95,7 @@ function register(router) {
|
|||||||
|
|
||||||
const note = becca.getNote(noteId);
|
const note = becca.getNote(noteId);
|
||||||
|
|
||||||
if (!note || note.isDeleted) {
|
if (!note) {
|
||||||
return res.sendStatus(204);
|
return res.sendStatus(204);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,6 +107,10 @@ function register(router) {
|
|||||||
eu.route(router, 'get', '/etapi/notes/:noteId/content', (req, res, next) => {
|
eu.route(router, 'get', '/etapi/notes/:noteId/content', (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 content cannot be read through ETAPI.`);
|
||||||
|
}
|
||||||
|
|
||||||
const filename = utils.formatDownloadTitle(note.title, note.type, note.mime);
|
const filename = utils.formatDownloadTitle(note.title, note.type, note.mime);
|
||||||
|
|
||||||
res.setHeader('Content-Disposition', utils.getContentDisposition(filename));
|
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) => {
|
eu.route(router, 'put', '/etapi/notes/:noteId/content', (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.`);
|
||||||
|
}
|
||||||
|
|
||||||
note.setContent(req.body);
|
note.setContent(req.body);
|
||||||
|
|
||||||
noteService.asyncPostProcessContent(note, req.body);
|
noteService.asyncPostProcessContent(note, req.body);
|
||||||
@ -132,7 +140,7 @@ function register(router) {
|
|||||||
const format = req.query.format || "html";
|
const format = req.query.format || "html";
|
||||||
|
|
||||||
if (!["html", "markdown"].includes(format)) {
|
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');
|
const taskContext = new TaskContext('no-progress-reporting');
|
||||||
@ -153,6 +161,15 @@ function register(router) {
|
|||||||
|
|
||||||
return res.sendStatus(204);
|
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) {
|
function parseSearchParams(req) {
|
||||||
@ -186,7 +203,7 @@ function parseBoolean(obj, name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!['true', 'false'].includes(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';
|
return obj[name] === 'true';
|
||||||
@ -200,7 +217,7 @@ function parseOrderDirection(obj, name) {
|
|||||||
const integer = parseInt(obj[name]);
|
const integer = parseInt(obj[name]);
|
||||||
|
|
||||||
if (!['asc', 'desc'].includes(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;
|
return integer;
|
||||||
@ -214,7 +231,7 @@ function parseInteger(obj, name) {
|
|||||||
const integer = parseInt(obj[name]);
|
const integer = parseInt(obj[name]);
|
||||||
|
|
||||||
if (Number.isNaN(integer)) {
|
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;
|
return integer;
|
||||||
|
@ -149,7 +149,7 @@ function getEditedNotesOnDate(req) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return notes.map(note => {
|
return notes.map(note => {
|
||||||
const notePath = note.isDeleted ? null : getNotePathData(note);
|
const notePath = getNotePathData(note);
|
||||||
|
|
||||||
const notePojo = note.getPojo();
|
const notePojo = note.getPojo();
|
||||||
notePojo.notePath = notePath ? notePath.notePath : null;
|
notePojo.notePath = notePath ? notePath.notePath : null;
|
||||||
|
@ -11,8 +11,8 @@ const ValidationError = require("../../errors/validation_error");
|
|||||||
function searchFromNote(req) {
|
function searchFromNote(req) {
|
||||||
const note = becca.getNoteOrThrow(req.params.noteId);
|
const note = becca.getNoteOrThrow(req.params.noteId);
|
||||||
|
|
||||||
if (note.isDeleted) {
|
if (!note) {
|
||||||
// this can be triggered from recent changes, and it's harmless to return empty list rather than fail
|
// this can be triggered from recent changes, and it's harmless to return an empty list rather than fail
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,8 +26,8 @@ function searchFromNote(req) {
|
|||||||
function searchAndExecute(req) {
|
function searchAndExecute(req) {
|
||||||
const note = becca.getNoteOrThrow(req.params.noteId);
|
const note = becca.getNoteOrThrow(req.params.noteId);
|
||||||
|
|
||||||
if (note.isDeleted) {
|
if (!note) {
|
||||||
// this can be triggered from recent changes, and it's harmless to return empty list rather than fail
|
// this can be triggered from recent changes, and it's harmless to return an empty list rather than fail
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,6 +63,7 @@ const shareRoutes = require('../share/routes');
|
|||||||
|
|
||||||
const etapiAuthRoutes = require('../etapi/auth');
|
const etapiAuthRoutes = require('../etapi/auth');
|
||||||
const etapiAppInfoRoutes = require('../etapi/app_info');
|
const etapiAppInfoRoutes = require('../etapi/app_info');
|
||||||
|
const etapiAttachmentRoutes = require('../etapi/attachments');
|
||||||
const etapiAttributeRoutes = require('../etapi/attributes');
|
const etapiAttributeRoutes = require('../etapi/attributes');
|
||||||
const etapiBranchRoutes = require('../etapi/branches');
|
const etapiBranchRoutes = require('../etapi/branches');
|
||||||
const etapiNoteRoutes = require('../etapi/notes');
|
const etapiNoteRoutes = require('../etapi/notes');
|
||||||
@ -332,6 +333,7 @@ function register(app) {
|
|||||||
|
|
||||||
etapiAuthRoutes.register(router, [loginRateLimiter]);
|
etapiAuthRoutes.register(router, [loginRateLimiter]);
|
||||||
etapiAppInfoRoutes.register(router);
|
etapiAppInfoRoutes.register(router);
|
||||||
|
etapiAttachmentRoutes.register(router);
|
||||||
etapiAttributeRoutes.register(router);
|
etapiAttributeRoutes.register(router);
|
||||||
etapiBranchRoutes.register(router);
|
etapiBranchRoutes.register(router);
|
||||||
etapiNoteRoutes.register(router);
|
etapiNoteRoutes.register(router);
|
||||||
|
@ -134,7 +134,7 @@ function executeActions(note, searchResultNoteIds) {
|
|||||||
for (const resultNoteId of searchResultNoteIds) {
|
for (const resultNoteId of searchResultNoteIds) {
|
||||||
const resultNote = becca.getNote(resultNoteId);
|
const resultNote = becca.getNote(resultNoteId);
|
||||||
|
|
||||||
if (!resultNote || resultNote.isDeleted) {
|
if (!resultNote) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,10 @@ const beccaService = require("../becca/becca_service");
|
|||||||
const log = require("./log");
|
const log = require("./log");
|
||||||
|
|
||||||
function cloneNoteToParentNote(noteId, parentNoteId, prefix) {
|
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);
|
const parentNote = becca.getNote(parentNoteId);
|
||||||
|
|
||||||
if (parentNote.type === 'search') {
|
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);
|
const validationResult = treeService.validateParentChild(parentNoteId, noteId);
|
||||||
|
|
||||||
if (!validationResult.success) {
|
if (!validationResult.success) {
|
||||||
@ -174,12 +174,6 @@ function cloneNoteAfter(noteId, afterBranchId) {
|
|||||||
return { success: true, branchId: branch.branchId };
|
return { success: true, branchId: branch.branchId };
|
||||||
}
|
}
|
||||||
|
|
||||||
function isNoteDeleted(noteId) {
|
|
||||||
const note = becca.getNote(noteId);
|
|
||||||
|
|
||||||
return !note || note.isDeleted;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
cloneNoteToBranch,
|
cloneNoteToBranch,
|
||||||
cloneNoteToParentNote,
|
cloneNoteToParentNote,
|
||||||
|
@ -607,16 +607,16 @@ class ConsistencyChecks {
|
|||||||
WHERE
|
WHERE
|
||||||
entity_changes.id IS NULL`,
|
entity_changes.id IS NULL`,
|
||||||
({entityId}) => {
|
({entityId}) => {
|
||||||
const entity = sql.getRow(`SELECT * FROM ${entityName} WHERE ${key} = ?`, [entityId]);
|
const entityRow = sql.getRow(`SELECT * FROM ${entityName} WHERE ${key} = ?`, [entityId]);
|
||||||
|
|
||||||
if (this.autoFix) {
|
if (this.autoFix) {
|
||||||
entityChangesService.addEntityChange({
|
entityChangesService.addEntityChange({
|
||||||
entityName,
|
entityName,
|
||||||
entityId,
|
entityId,
|
||||||
hash: utils.randomString(10), // doesn't matter, will force sync, but that's OK
|
hash: utils.randomString(10), // doesn't matter, will force sync, but that's OK
|
||||||
isErased: !!entity.isErased,
|
isErased: !!entityRow.isErased,
|
||||||
utcDateChanged: entity.utcDateModified || entity.utcDateCreated,
|
utcDateChanged: entityRow.utcDateModified || entityRow.utcDateCreated,
|
||||||
isSynced: entityName !== 'options' || entity.isSynced
|
isSynced: entityName !== 'options' || entityRow.isSynced
|
||||||
});
|
});
|
||||||
|
|
||||||
logFix(`Created missing entity change for entityName '${entityName}', entityId '${entityId}'`);
|
logFix(`Created missing entity change for entityName '${entityName}', entityId '${entityId}'`);
|
||||||
|
@ -570,7 +570,7 @@ function downloadImages(noteId, content) {
|
|||||||
for (const url in imageUrlToAttachmentIdMapping) {
|
for (const url in imageUrlToAttachmentIdMapping) {
|
||||||
const imageNote = imageNotes.find(note => note.noteId === imageUrlToAttachmentIdMapping[url]);
|
const imageNote = imageNotes.find(note => note.noteId === imageUrlToAttachmentIdMapping[url]);
|
||||||
|
|
||||||
if (imageNote && !imageNote.isDeleted) {
|
if (imageNote) {
|
||||||
updatedContent = replaceUrl(updatedContent, url, imageNote);
|
updatedContent = replaceUrl(updatedContent, url, imageNote);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -697,14 +697,14 @@ function updateNoteData(noteId, content) {
|
|||||||
* @param {TaskContext} taskContext
|
* @param {TaskContext} taskContext
|
||||||
*/
|
*/
|
||||||
function undeleteNote(noteId, 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.`);
|
log.error(`Note '${noteId}' is not deleted and thus cannot be undeleted.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const undeletedParentBranchIds = getUndeletedParentBranchIds(noteId, note.deleteId);
|
const undeletedParentBranchIds = getUndeletedParentBranchIds(noteId, noteRow.deleteId);
|
||||||
|
|
||||||
if (undeletedParentBranchIds.length === 0) {
|
if (undeletedParentBranchIds.length === 0) {
|
||||||
// cannot undelete if there's no undeleted parent
|
// cannot undelete if there's no undeleted parent
|
||||||
@ -712,7 +712,7 @@ function undeleteNote(noteId, taskContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const parentBranchId of undeletedParentBranchIds) {
|
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
|
* @param {TaskContext} taskContext
|
||||||
*/
|
*/
|
||||||
function undeleteBranch(branchId, deleteId, 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;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
new BBranch(branch).save();
|
new BBranch(branchRow).save();
|
||||||
|
|
||||||
taskContext.increaseProgressCount();
|
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
|
// becca entity was already created as skeleton in "new Branch()" above
|
||||||
const noteEntity = becca.getNote(note.noteId);
|
const noteEntity = becca.getNote(noteRow.noteId);
|
||||||
noteEntity.updateFromRow(note);
|
noteEntity.updateFromRow(noteRow);
|
||||||
noteEntity.save();
|
noteEntity.save();
|
||||||
|
|
||||||
const attributes = sql.getRows(`
|
const attributeRows = sql.getRows(`
|
||||||
SELECT * FROM attributes
|
SELECT * FROM attributes
|
||||||
WHERE isDeleted = 1
|
WHERE isDeleted = 1
|
||||||
AND deleteId = ?
|
AND deleteId = ?
|
||||||
AND (noteId = ?
|
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
|
// 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(`
|
const childBranchIds = sql.getColumn(`
|
||||||
@ -761,7 +761,7 @@ function undeleteBranch(branchId, deleteId, taskContext) {
|
|||||||
FROM branches
|
FROM branches
|
||||||
WHERE branches.isDeleted = 1
|
WHERE branches.isDeleted = 1
|
||||||
AND branches.deleteId = ?
|
AND branches.deleteId = ?
|
||||||
AND branches.parentNoteId = ?`, [deleteId, note.noteId]);
|
AND branches.parentNoteId = ?`, [deleteId, noteRow.noteId]);
|
||||||
|
|
||||||
for (const childBranchId of childBranchIds) {
|
for (const childBranchId of childBranchIds) {
|
||||||
undeleteBranch(childBranchId, deleteId, taskContext);
|
undeleteBranch(childBranchId, deleteId, taskContext);
|
||||||
|
@ -155,7 +155,6 @@ function findResultsWithExpression(expression, searchContext) {
|
|||||||
const noteSet = expression.execute(allNoteSet, executionContext, searchContext);
|
const noteSet = expression.execute(allNoteSet, executionContext, searchContext);
|
||||||
|
|
||||||
const searchResults = noteSet.notes
|
const searchResults = noteSet.notes
|
||||||
.filter(note => !note.isDeleted)
|
|
||||||
.map(note => {
|
.map(note => {
|
||||||
const notePathArray = executionContext.noteIdToNotePath[note.noteId] || note.getBestNotePath();
|
const notePathArray = executionContext.noteIdToNotePath[note.noteId] || note.getBestNotePath();
|
||||||
|
|
||||||
|
@ -315,21 +315,21 @@ function getEntityChangeRow(entityName, entityId) {
|
|||||||
throw new Error(`Unknown entity '${entityName}'`);
|
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.`);
|
throw new Error(`Entity ${entityName} '${entityId}' not found.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entityName === 'blobs' && entity.content !== null) {
|
if (entityName === 'blobs' && entityRow.content !== null) {
|
||||||
if (typeof entity.content === 'string') {
|
if (typeof entityRow.content === 'string') {
|
||||||
entity.content = Buffer.from(entity.content, 'utf-8');
|
entityRow.content = Buffer.from(entityRow.content, 'utf-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
entity.content = entity.content.toString("base64");
|
entityRow.content = entityRow.content.toString("base64");
|
||||||
}
|
}
|
||||||
|
|
||||||
return entity;
|
return entityRow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,7 +199,7 @@ function sortNotesIfNeeded(parentNoteId) {
|
|||||||
function setNoteToParent(noteId, prefix, parentNoteId) {
|
function setNoteToParent(noteId, prefix, parentNoteId) {
|
||||||
const parentNote = becca.getNote(parentNoteId);
|
const parentNote = becca.getNote(parentNoteId);
|
||||||
|
|
||||||
if (parentNote && parentNote.isDeleted) {
|
if (!parentNote) {
|
||||||
throw new Error(`Cannot move note to deleted parent note '${parentNoteId}'`);
|
throw new Error(`Cannot move note to deleted parent note '${parentNoteId}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,7 +209,7 @@ function setNoteToParent(noteId, prefix, parentNoteId) {
|
|||||||
|
|
||||||
if (branch) {
|
if (branch) {
|
||||||
if (!parentNoteId) {
|
if (!parentNoteId) {
|
||||||
log.info(`Removing note ${noteId} from parent ${parentNoteId}`);
|
log.info(`Removing note '${noteId}' from parent '${parentNoteId}'`);
|
||||||
|
|
||||||
branch.markAsDeleted();
|
branch.markAsDeleted();
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user