Notes/src/etapi/notes.js

257 lines
8.8 KiB
JavaScript
Raw Normal View History

const becca = require('../becca/becca.js');
const utils = require('../services/utils.js');
const eu = require('./etapi_utils.js');
const mappers = require('./mappers.js');
const noteService = require('../services/notes.js');
const TaskContext = require('../services/task_context.js');
const v = require('./validators.js');
const searchService = require('../services/search/services/search.js');
const SearchContext = require('../services/search/search_context.js');
const zipExportService = require('../services/export/zip.js');
const zipImportService = require('../services/import/zip.js');
2022-01-07 19:33:59 +01:00
function register(router) {
2022-01-10 17:09:20 +01:00
eu.route(router, 'get', '/etapi/notes', (req, res, next) => {
2022-01-08 13:18:12 +01:00
const {search} = req.query;
if (!search?.trim()) {
2023-06-05 09:23:42 +02:00
throw new eu.EtapiError(400, 'SEARCH_QUERY_PARAM_MANDATORY', "'search' query parameter is mandatory.");
2022-01-08 13:18:12 +01:00
}
2022-01-08 13:18:12 +01:00
const searchParams = parseSearchParams(req);
2022-01-10 17:09:20 +01:00
const searchContext = new SearchContext(searchParams);
2022-01-10 17:09:20 +01:00
const searchResults = searchService.findResultsWithQuery(search, searchContext);
const foundNotes = searchResults.map(sr => becca.notes[sr.noteId]);
2022-01-10 17:09:20 +01:00
const resp = {
results: foundNotes.map(note => mappers.mapNoteToPojo(note))
};
2022-01-10 17:09:20 +01:00
if (searchContext.debugInfo) {
resp.debugInfo = searchContext.debugInfo;
}
2022-01-10 17:09:20 +01:00
res.json(resp);
2022-01-08 13:18:12 +01:00
});
2022-01-10 17:09:20 +01:00
eu.route(router, 'get', '/etapi/notes/:noteId', (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
2022-01-07 19:33:59 +01:00
res.json(mappers.mapNoteToPojo(note));
});
2022-01-12 19:32:23 +01:00
const ALLOWED_PROPERTIES_FOR_CREATE_NOTE = {
'parentNoteId': [v.mandatory, v.notNull, v.isNoteId],
'title': [v.mandatory, v.notNull, v.isString],
'type': [v.mandatory, v.notNull, v.isNoteType],
'mime': [v.notNull, v.isString],
'content': [v.notNull, v.isString],
'notePosition': [v.notNull, v.isInteger],
'prefix': [v.notNull, v.isString],
2022-01-12 19:32:23 +01:00
'isExpanded': [v.notNull, v.isBoolean],
'noteId': [v.notNull, v.isValidEntityId],
'dateCreated': [v.notNull, v.isString, v.isLocalDateTime],
'utcDateCreated': [v.notNull, v.isString, v.isUtcDateTime]
2022-01-12 19:32:23 +01:00
};
2022-01-10 17:09:20 +01:00
eu.route(router, 'post' ,'/etapi/create-note', (req, res, next) => {
2022-01-12 19:32:23 +01:00
const params = {};
2022-01-12 19:32:23 +01:00
eu.validateAndPatch(params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_NOTE);
2022-01-07 19:33:59 +01:00
try {
const resp = noteService.createNewNote(params);
res.status(201).json({
2022-01-07 19:33:59 +01:00
note: mappers.mapNoteToPojo(resp.note),
branch: mappers.mapBranchToPojo(resp.branch)
});
}
catch (e) {
2022-01-12 19:32:23 +01:00
return eu.sendError(res, 500, eu.GENERIC_CODE, e.message);
2022-01-07 19:33:59 +01:00
}
});
const ALLOWED_PROPERTIES_FOR_PATCH = {
2022-01-12 19:32:23 +01:00
'title': [v.notNull, v.isString],
'type': [v.notNull, v.isString],
'mime': [v.notNull, v.isString],
'dateCreated': [v.notNull, v.isString, v.isLocalDateTime],
'utcDateCreated': [v.notNull, v.isString, v.isUtcDateTime]
2022-01-07 19:33:59 +01:00
};
2022-01-10 17:09:20 +01:00
eu.route(router, 'patch' ,'/etapi/notes/:noteId', (req, res, next) => {
2023-06-05 09:23:42 +02:00
const note = eu.getAndCheckNote(req.params.noteId);
2022-01-07 23:06:04 +01:00
2022-01-07 19:33:59 +01:00
if (note.isProtected) {
2023-06-05 09:23:42 +02:00
throw new eu.EtapiError(400, "NOTE_IS_PROTECTED", `Note '${req.params.noteId}' is protected and cannot be modified through ETAPI.`);
2022-01-07 19:33:59 +01:00
}
2022-01-07 23:06:04 +01:00
2022-01-10 17:09:20 +01:00
eu.validateAndPatch(note, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
2022-01-12 19:32:23 +01:00
note.save();
2022-01-07 23:06:04 +01:00
2022-01-07 19:33:59 +01:00
res.json(mappers.mapNoteToPojo(note));
});
2022-01-10 17:09:20 +01:00
eu.route(router, 'delete' ,'/etapi/notes/:noteId', (req, res, next) => {
2022-01-07 19:33:59 +01:00
const {noteId} = req.params;
const note = becca.getNote(noteId);
2023-06-05 09:23:42 +02:00
if (!note) {
2022-01-07 19:33:59 +01:00
return res.sendStatus(204);
}
note.deleteNote(null, new TaskContext('no-progress-reporting'));
2022-01-07 19:33:59 +01:00
res.sendStatus(204);
});
2022-01-08 12:01:54 +01:00
2022-01-10 17:09:20 +01:00
eu.route(router, 'get', '/etapi/notes/:noteId/content', (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
2022-01-08 12:01:54 +01:00
2023-06-05 09:23:42 +02:00
if (note.isProtected) {
throw new eu.EtapiError(400, "NOTE_IS_PROTECTED", `Note '${req.params.noteId}' is protected and content cannot be read through ETAPI.`);
}
2022-01-08 12:01:54 +01:00
const filename = utils.formatDownloadTitle(note.title, note.type, note.mime);
res.setHeader('Content-Disposition', utils.getContentDisposition(filename));
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.setHeader('Content-Type', note.mime);
res.send(note.getContent());
});
2022-01-10 17:09:20 +01:00
eu.route(router, 'put', '/etapi/notes/:noteId/content', (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
2022-01-08 12:01:54 +01:00
2023-06-05 09:23:42 +02:00
if (note.isProtected) {
throw new eu.EtapiError(400, "NOTE_IS_PROTECTED", `Note '${req.params.noteId}' is protected and cannot be modified through ETAPI.`);
}
2022-01-08 12:01:54 +01:00
note.setContent(req.body);
noteService.asyncPostProcessContent(note, req.body);
2022-01-08 12:01:54 +01:00
return res.sendStatus(204);
});
2022-07-24 21:30:29 +02:00
eu.route(router, 'get' ,'/etapi/notes/:noteId/export', (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
const format = req.query.format || "html";
if (!["html", "markdown"].includes(format)) {
2023-06-05 09:23:42 +02:00
throw new eu.EtapiError(400, "UNRECOGNIZED_EXPORT_FORMAT", `Unrecognized export format '${format}', supported values are 'html' (default) or 'markdown'.`);
2022-07-24 21:30:29 +02:00
}
const taskContext = new TaskContext('no-progress-reporting');
// technically a branch is being exported (includes prefix), but it's such a minor difference yet usability pain
// (e.g. branchIds are not seen in UI), that we export "note export" instead.
const branch = note.getParentBranches()[0];
zipExportService.exportToZip(taskContext, branch, format, res);
});
2023-06-15 23:21:40 +02:00
eu.route(router, 'post' ,'/etapi/notes/:noteId/import', (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
const taskContext = new TaskContext('no-progress-reporting');
zipImportService.importZip(taskContext, req.body, note).then(importedNote => {
res.status(201).json({
note: mappers.mapNoteToPojo(importedNote),
2023-11-03 01:11:47 +01:00
branch: mappers.mapBranchToPojo(importedNote.getParentBranches()[0]),
2023-06-15 23:21:40 +02:00
});
}); // we need better error handling here, async errors won't be properly processed.
});
eu.route(router, 'post' ,'/etapi/notes/:noteId/revision', (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
note.saveRevision();
return res.sendStatus(204);
});
2023-06-05 09:23:42 +02:00
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))
);
});
2022-01-07 19:33:59 +01:00
}
2022-01-08 13:18:12 +01:00
function parseSearchParams(req) {
const rawSearchParams = {
fastSearch: parseBoolean(req.query, 'fastSearch'),
includeArchivedNotes: parseBoolean(req.query, 'includeArchivedNotes'),
ancestorNoteId: req.query['ancestorNoteId'],
ancestorDepth: req.query['ancestorDepth'], // e.g. "eq5"
orderBy: req.query['orderBy'],
orderDirection: parseOrderDirection(req.query, 'orderDirection'),
limit: parseInteger(req.query, 'limit'),
debug: parseBoolean(req.query, 'debug')
2022-01-08 13:18:12 +01:00
};
const searchParams = {};
for (const paramName of Object.keys(rawSearchParams)) {
if (rawSearchParams[paramName] !== undefined) {
searchParams[paramName] = rawSearchParams[paramName];
}
}
return searchParams;
}
const SEARCH_PARAM_ERROR = "SEARCH_PARAM_VALIDATION_ERROR";
function parseBoolean(obj, name) {
if (!(name in obj)) {
return undefined;
}
if (!['true', 'false'].includes(obj[name])) {
2023-06-05 09:23:42 +02:00
throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse boolean '${name}' value '${obj[name]}, allowed values are 'true' and 'false'.`);
2022-01-08 13:18:12 +01:00
}
return obj[name] === 'true';
}
2022-02-11 02:28:42 +08:00
function parseOrderDirection(obj, name) {
2022-01-08 13:18:12 +01:00
if (!(name in obj)) {
return undefined;
}
const integer = parseInt(obj[name]);
if (!['asc', 'desc'].includes(obj[name])) {
2023-06-05 09:23:42 +02:00
throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse order direction value '${obj[name]}, allowed values are 'asc' and 'desc'.`);
2022-01-08 13:18:12 +01:00
}
return integer;
}
2022-02-11 02:28:42 +08:00
function parseInteger(obj, name) {
2022-01-08 13:18:12 +01:00
if (!(name in obj)) {
return undefined;
}
const integer = parseInt(obj[name]);
if (Number.isNaN(integer)) {
2023-06-05 09:23:42 +02:00
throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse integer '${name}' value '${obj[name]}'.`);
2022-01-08 13:18:12 +01:00
}
return integer;
}
2022-01-07 19:33:59 +01:00
module.exports = {
register
2022-01-07 23:06:04 +01:00
};