Merge pull request #45 from TriliumNext/feature/typescript_backend_9

Convert backend to TypeScript (81% -> 84%)
This commit is contained in:
Elian Doran 2024-04-20 09:35:18 +03:00 committed by GitHub
commit 19cf9df52c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 268 additions and 197 deletions

View File

@ -141,7 +141,7 @@ class BBranch extends AbstractBeccaEntity<BBranch> {
* *
* @returns true if note has been deleted, false otherwise * @returns true if note has been deleted, false otherwise
*/ */
deleteBranch(deleteId: string, taskContext: TaskContext): boolean { deleteBranch(deleteId?: string, taskContext?: TaskContext): boolean {
if (!deleteId) { if (!deleteId) {
deleteId = utils.randomString(10); deleteId = utils.randomString(10);
} }

View File

@ -1,12 +0,0 @@
const appInfo = require('../services/app_info');
const eu = require('./etapi_utils');
function register(router) {
eu.route(router, 'get', '/etapi/app-info', (req, res, next) => {
res.status(200).json(appInfo);
});
}
module.exports = {
register
};

13
src/etapi/app_info.ts Normal file
View File

@ -0,0 +1,13 @@
import { Router } from 'express';
import appInfo = require('../services/app_info');
import eu = require('./etapi_utils');
function register(router: Router) {
eu.route(router, 'get', '/etapi/app-info', (req, res, next) => {
res.status(200).json(appInfo);
});
}
export = {
register
};

View File

@ -1,11 +1,13 @@
const becca = require('../becca/becca'); import becca = require('../becca/becca');
const eu = require('./etapi_utils'); import eu = require('./etapi_utils');
const mappers = require('./mappers.js'); import mappers = require('./mappers');
const v = require('./validators.js'); import v = require('./validators');
const utils = require('../services/utils'); import utils = require('../services/utils');
import { Router } from 'express';
import { AttachmentRow } from '../becca/entities/rows';
function register(router) { function register(router: Router) {
const ALLOWED_PROPERTIES_FOR_CREATE_ATTACHMENT = { const ALLOWED_PROPERTIES_FOR_CREATE_ATTACHMENT: ValidatorMap = {
'ownerId': [v.notNull, v.isNoteId], 'ownerId': [v.notNull, v.isNoteId],
'role': [v.notNull, v.isString], 'role': [v.notNull, v.isString],
'mime': [v.notNull, v.isString], 'mime': [v.notNull, v.isString],
@ -14,18 +16,21 @@ function register(router) {
'content': [v.isString], 'content': [v.isString],
}; };
eu.route(router, 'post' ,'/etapi/attachments', (req, res, next) => { eu.route(router, 'post', '/etapi/attachments', (req, res, next) => {
const params = {}; const _params: Partial<AttachmentRow> = {};
eu.validateAndPatch(_params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_ATTACHMENT);
eu.validateAndPatch(params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_ATTACHMENT); const params = _params as AttachmentRow;
try { try {
if (!params.ownerId) {
throw new Error("Missing owner ID.");
}
const note = becca.getNoteOrThrow(params.ownerId); const note = becca.getNoteOrThrow(params.ownerId);
const attachment = note.saveAttachment(params); const attachment = note.saveAttachment(params);
res.status(201).json(mappers.mapAttachmentToPojo(attachment)); res.status(201).json(mappers.mapAttachmentToPojo(attachment));
} }
catch (e) { catch (e: any) {
throw new eu.EtapiError(500, eu.GENERIC_CODE, e.message); throw new eu.EtapiError(500, eu.GENERIC_CODE, e.message);
} }
}); });
@ -43,7 +48,7 @@ function register(router) {
'position': [v.notNull, v.isInteger], 'position': [v.notNull, v.isInteger],
}; };
eu.route(router, 'patch' ,'/etapi/attachments/:attachmentId', (req, res, next) => { eu.route(router, 'patch', '/etapi/attachments/:attachmentId', (req, res, next) => {
const attachment = eu.getAndCheckAttachment(req.params.attachmentId); const attachment = eu.getAndCheckAttachment(req.params.attachmentId);
if (attachment.isProtected) { if (attachment.isProtected) {
@ -85,7 +90,7 @@ function register(router) {
return res.sendStatus(204); return res.sendStatus(204);
}); });
eu.route(router, 'delete' ,'/etapi/attachments/:attachmentId', (req, res, next) => { eu.route(router, 'delete', '/etapi/attachments/:attachmentId', (req, res, next) => {
const attachment = becca.getAttachment(req.params.attachmentId); const attachment = becca.getAttachment(req.params.attachmentId);
if (!attachment) { if (!attachment) {

View File

@ -1,17 +1,19 @@
const becca = require('../becca/becca'); import becca = require('../becca/becca');
const eu = require('./etapi_utils'); import eu = require('./etapi_utils');
const mappers = require('./mappers.js'); import mappers = require('./mappers');
const attributeService = require('../services/attributes'); import attributeService = require('../services/attributes');
const v = require('./validators.js'); import v = require('./validators');
import { Router } from 'express';
import { AttributeRow } from '../becca/entities/rows';
function register(router) { function register(router: Router) {
eu.route(router, 'get', '/etapi/attributes/:attributeId', (req, res, next) => { eu.route(router, 'get', '/etapi/attributes/:attributeId', (req, res, next) => {
const attribute = eu.getAndCheckAttribute(req.params.attributeId); const attribute = eu.getAndCheckAttribute(req.params.attributeId);
res.json(mappers.mapAttributeToPojo(attribute)); res.json(mappers.mapAttributeToPojo(attribute));
}); });
const ALLOWED_PROPERTIES_FOR_CREATE_ATTRIBUTE = { const ALLOWED_PROPERTIES_FOR_CREATE_ATTRIBUTE: ValidatorMap = {
'attributeId': [v.mandatory, v.notNull, v.isValidEntityId], 'attributeId': [v.mandatory, v.notNull, v.isValidEntityId],
'noteId': [v.mandatory, v.notNull, v.isNoteId], 'noteId': [v.mandatory, v.notNull, v.isNoteId],
'type': [v.mandatory, v.notNull, v.isAttributeType], 'type': [v.mandatory, v.notNull, v.isAttributeType],
@ -21,21 +23,21 @@ function register(router) {
'position': [v.notNull, v.isInteger] 'position': [v.notNull, v.isInteger]
}; };
eu.route(router, 'post' ,'/etapi/attributes', (req, res, next) => { eu.route(router, 'post', '/etapi/attributes', (req, res, next) => {
if (req.body.type === 'relation') { if (req.body.type === 'relation') {
eu.getAndCheckNote(req.body.value); eu.getAndCheckNote(req.body.value);
} }
const params = {}; const _params = {};
eu.validateAndPatch(_params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_ATTRIBUTE);
eu.validateAndPatch(params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_ATTRIBUTE); const params: AttributeRow = _params as AttributeRow;
try { try {
const attr = attributeService.createAttribute(params); const attr = attributeService.createAttribute(params);
res.status(201).json(mappers.mapAttributeToPojo(attr)); res.status(201).json(mappers.mapAttributeToPojo(attr));
} }
catch (e) { catch (e: any) {
throw new eu.EtapiError(500, eu.GENERIC_CODE, e.message); throw new eu.EtapiError(500, eu.GENERIC_CODE, e.message);
} }
}); });
@ -49,7 +51,7 @@ function register(router) {
'position': [v.notNull, v.isInteger] 'position': [v.notNull, v.isInteger]
}; };
eu.route(router, 'patch' ,'/etapi/attributes/:attributeId', (req, res, next) => { eu.route(router, 'patch', '/etapi/attributes/:attributeId', (req, res, next) => {
const attribute = eu.getAndCheckAttribute(req.params.attributeId); const attribute = eu.getAndCheckAttribute(req.params.attributeId);
if (attribute.type === 'label') { if (attribute.type === 'label') {
@ -65,7 +67,7 @@ function register(router) {
res.json(mappers.mapAttributeToPojo(attribute)); res.json(mappers.mapAttributeToPojo(attribute));
}); });
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) { if (!attribute) {
@ -78,6 +80,6 @@ function register(router) {
}); });
} }
module.exports = { export = {
register register
}; };

View File

@ -1,9 +1,10 @@
const becca = require('../becca/becca'); import becca = require('../becca/becca');
const eu = require('./etapi_utils'); import eu = require('./etapi_utils');
const passwordEncryptionService = require('../services/encryption/password_encryption'); import passwordEncryptionService = require('../services/encryption/password_encryption');
const etapiTokenService = require('../services/etapi_tokens'); import etapiTokenService = require('../services/etapi_tokens');
import { RequestHandler, Router } from 'express';
function register(router, loginMiddleware) { function register(router: Router, loginMiddleware: RequestHandler[]) {
eu.NOT_AUTHENTICATED_ROUTE(router, 'post', '/etapi/auth/login', loginMiddleware, (req, res, next) => { eu.NOT_AUTHENTICATED_ROUTE(router, 'post', '/etapi/auth/login', loginMiddleware, (req, res, next) => {
const {password, tokenName} = req.body; const {password, tokenName} = req.body;
@ -38,6 +39,6 @@ function register(router, loginMiddleware) {
}); });
} }
module.exports = { export = {
register register
} }

View File

@ -1,7 +1,9 @@
const eu = require('./etapi_utils'); import { Router } from "express";
const backupService = require('../services/backup');
function register(router) { import eu = require('./etapi_utils');
import backupService = require('../services/backup');
function register(router: Router) {
eu.route(router, 'put', '/etapi/backup/:backupName', async (req, res, next) => { eu.route(router, 'put', '/etapi/backup/:backupName', async (req, res, next) => {
await backupService.backupNow(req.params.backupName); await backupService.backupNow(req.params.backupName);
@ -9,6 +11,6 @@ function register(router) {
}); });
} }
module.exports = { export = {
register register
}; };

View File

@ -1,11 +1,14 @@
const becca = require('../becca/becca'); import { Router } from "express";
const eu = require('./etapi_utils');
const mappers = require('./mappers.js');
const BBranch = require('../becca/entities/bbranch');
const entityChangesService = require('../services/entity_changes');
const v = require('./validators.js');
function register(router) { import becca = require('../becca/becca');
import eu = require('./etapi_utils');
import mappers = require('./mappers');
import BBranch = require('../becca/entities/bbranch');
import entityChangesService = require('../services/entity_changes');
import v = require('./validators');
import { BranchRow } from "../becca/entities/rows";
function register(router: Router) {
eu.route(router, 'get', '/etapi/branches/:branchId', (req, res, next) => { eu.route(router, 'get', '/etapi/branches/:branchId', (req, res, next) => {
const branch = eu.getAndCheckBranch(req.params.branchId); const branch = eu.getAndCheckBranch(req.params.branchId);
@ -20,17 +23,17 @@ function register(router) {
'isExpanded': [v.notNull, v.isBoolean] 'isExpanded': [v.notNull, v.isBoolean]
}; };
eu.route(router, 'post' ,'/etapi/branches', (req, res, next) => { eu.route(router, 'post', '/etapi/branches', (req, res, next) => {
const params = {}; const _params = {};
eu.validateAndPatch(_params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_BRANCH);
eu.validateAndPatch(params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_BRANCH); const params: BranchRow = _params as BranchRow;
const existing = becca.getBranchFromChildAndParent(params.noteId, params.parentNoteId); const existing = becca.getBranchFromChildAndParent(params.noteId, params.parentNoteId);
if (existing) { if (existing) {
existing.notePosition = params.notePosition; existing.notePosition = params.notePosition as number;
existing.prefix = params.prefix; existing.prefix = params.prefix as string;
existing.isExpanded = params.isExpanded; existing.isExpanded = params.isExpanded as boolean;
existing.save(); existing.save();
return res.status(200).json(mappers.mapBranchToPojo(existing)); return res.status(200).json(mappers.mapBranchToPojo(existing));
@ -39,7 +42,7 @@ function register(router) {
const branch = new BBranch(params).save(); const branch = new BBranch(params).save();
res.status(201).json(mappers.mapBranchToPojo(branch)); res.status(201).json(mappers.mapBranchToPojo(branch));
} catch (e) { } catch (e: any) {
throw new eu.EtapiError(400, eu.GENERIC_CODE, e.message); throw new eu.EtapiError(400, eu.GENERIC_CODE, e.message);
} }
} }
@ -51,7 +54,7 @@ function register(router) {
'isExpanded': [v.notNull, v.isBoolean] 'isExpanded': [v.notNull, v.isBoolean]
}; };
eu.route(router, 'patch' ,'/etapi/branches/:branchId', (req, res, next) => { eu.route(router, 'patch', '/etapi/branches/:branchId', (req, res, next) => {
const branch = eu.getAndCheckBranch(req.params.branchId); const branch = eu.getAndCheckBranch(req.params.branchId);
eu.validateAndPatch(branch, req.body, ALLOWED_PROPERTIES_FOR_PATCH); eu.validateAndPatch(branch, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
@ -60,7 +63,7 @@ function register(router) {
res.json(mappers.mapBranchToPojo(branch)); res.json(mappers.mapBranchToPojo(branch));
}); });
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) { if (!branch) {
@ -72,7 +75,7 @@ function register(router) {
res.sendStatus(204); res.sendStatus(204);
}); });
eu.route(router, 'post' ,'/etapi/refresh-note-ordering/:parentNoteId', (req, res, next) => { eu.route(router, 'post', '/etapi/refresh-note-ordering/:parentNoteId', (req, res, next) => {
eu.getAndCheckNote(req.params.parentNoteId); eu.getAndCheckNote(req.params.parentNoteId);
entityChangesService.putNoteReorderingEntityChange(req.params.parentNoteId, "etapi"); entityChangesService.putNoteReorderingEntityChange(req.params.parentNoteId, "etapi");
@ -81,6 +84,6 @@ function register(router) {
}); });
} }
module.exports = { export = {
register register
}; };

View File

@ -0,0 +1,3 @@
type ValidatorFunc = (obj: unknown) => (string | undefined);
type ValidatorMap = Record<string, ValidatorFunc[]>;

View File

@ -1,15 +1,22 @@
const cls = require('../services/cls'); import cls = require('../services/cls');
const sql = require('../services/sql'); import sql = require('../services/sql');
const log = require('../services/log'); import log = require('../services/log');
const becca = require('../becca/becca'); import becca = require('../becca/becca');
const etapiTokenService = require('../services/etapi_tokens'); import etapiTokenService = require('../services/etapi_tokens');
const config = require('../services/config'); import config = require('../services/config');
import { NextFunction, Request, RequestHandler, Response, Router } from 'express';
import { AppRequest, AppRequestHandler } from '../routes/route-interface';
const GENERIC_CODE = "GENERIC"; const GENERIC_CODE = "GENERIC";
type HttpMethod = "all" | "get" | "post" | "put" | "delete" | "patch" | "options" | "head";
const noAuthentication = config.General && config.General.noAuthentication === true; const noAuthentication = config.General && config.General.noAuthentication === true;
class EtapiError extends Error { class EtapiError extends Error {
constructor(statusCode, code, message) { statusCode: number;
code: string;
constructor(statusCode: number, code: string, message: string) {
super(); super();
this.statusCode = statusCode; this.statusCode = statusCode;
@ -18,7 +25,7 @@ class EtapiError extends Error {
} }
} }
function sendError(res, statusCode, code, message) { function sendError(res: Response, statusCode: number, code: string, message: string) {
return res return res
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')
.status(statusCode) .status(statusCode)
@ -29,7 +36,7 @@ function sendError(res, statusCode, code, message) {
})); }));
} }
function checkEtapiAuth(req, res, next) { function checkEtapiAuth(req: Request, res: Response, next: NextFunction) {
if (noAuthentication || etapiTokenService.isValidAuthHeader(req.headers.authorization)) { if (noAuthentication || etapiTokenService.isValidAuthHeader(req.headers.authorization)) {
next(); next();
} }
@ -38,7 +45,7 @@ function checkEtapiAuth(req, res, next) {
} }
} }
function processRequest(req, res, routeHandler, next, method, path) { function processRequest(req: Request, res: Response, routeHandler: AppRequestHandler, next: NextFunction, method: string, path: string) {
try { try {
cls.namespace.bindEmitter(req); cls.namespace.bindEmitter(req);
cls.namespace.bindEmitter(res); cls.namespace.bindEmitter(res);
@ -47,11 +54,11 @@ function processRequest(req, res, routeHandler, next, method, path) {
cls.set('componentId', "etapi"); cls.set('componentId', "etapi");
cls.set('localNowDateTime', req.headers['trilium-local-now-datetime']); cls.set('localNowDateTime', req.headers['trilium-local-now-datetime']);
const cb = () => routeHandler(req, res, next); const cb = () => routeHandler(req as AppRequest, res, next);
return sql.transactional(cb); return sql.transactional(cb);
}); });
} catch (e) { } catch (e: any) {
log.error(`${method} ${path} threw exception ${e.message} with stacktrace: ${e.stack}`); log.error(`${method} ${path} threw exception ${e.message} with stacktrace: ${e.stack}`);
if (e instanceof EtapiError) { if (e instanceof EtapiError) {
@ -62,15 +69,15 @@ function processRequest(req, res, routeHandler, next, method, path) {
} }
} }
function route(router, method, path, routeHandler) { function route(router: Router, method: HttpMethod, path: string, routeHandler: AppRequestHandler) {
router[method](path, checkEtapiAuth, (req, res, next) => processRequest(req, res, routeHandler, next, method, path)); router[method](path, checkEtapiAuth, (req: Request, res: Response, next: NextFunction) => processRequest(req, res, routeHandler, next, method, path));
} }
function NOT_AUTHENTICATED_ROUTE(router, method, path, middleware, routeHandler) { function NOT_AUTHENTICATED_ROUTE(router: Router, method: HttpMethod, path: string, middleware: RequestHandler[], routeHandler: RequestHandler) {
router[method](path, ...middleware, (req, res, next) => processRequest(req, res, routeHandler, next, method, path)); router[method](path, ...middleware, (req: Request, res: Response, next: NextFunction) => processRequest(req, res, routeHandler, next, method, path));
} }
function getAndCheckNote(noteId) { function getAndCheckNote(noteId: string) {
const note = becca.getNote(noteId); const note = becca.getNote(noteId);
if (note) { if (note) {
@ -81,7 +88,7 @@ function getAndCheckNote(noteId) {
} }
} }
function getAndCheckAttachment(attachmentId) { function getAndCheckAttachment(attachmentId: string) {
const attachment = becca.getAttachment(attachmentId, {includeContentLength: true}); const attachment = becca.getAttachment(attachmentId, {includeContentLength: true});
if (attachment) { if (attachment) {
@ -92,7 +99,7 @@ function getAndCheckAttachment(attachmentId) {
} }
} }
function getAndCheckBranch(branchId) { function getAndCheckBranch(branchId: string) {
const branch = becca.getBranch(branchId); const branch = becca.getBranch(branchId);
if (branch) { if (branch) {
@ -103,7 +110,7 @@ function getAndCheckBranch(branchId) {
} }
} }
function getAndCheckAttribute(attributeId) { function getAndCheckAttribute(attributeId: string) {
const attribute = becca.getAttribute(attributeId); const attribute = becca.getAttribute(attributeId);
if (attribute) { if (attribute) {
@ -114,7 +121,7 @@ function getAndCheckAttribute(attributeId) {
} }
} }
function validateAndPatch(target, source, allowedProperties) { function validateAndPatch(target: any, source: any, allowedProperties: ValidatorMap) {
for (const key of Object.keys(source)) { for (const key of Object.keys(source)) {
if (!(key in allowedProperties)) { if (!(key in allowedProperties)) {
throw new EtapiError(400, "PROPERTY_NOT_ALLOWED", `Property '${key}' is not allowed for this method.`); throw new EtapiError(400, "PROPERTY_NOT_ALLOWED", `Property '${key}' is not allowed for this method.`);
@ -136,7 +143,7 @@ function validateAndPatch(target, source, allowedProperties) {
} }
} }
module.exports = { export = {
EtapiError, EtapiError,
sendError, sendError,
route, route,

View File

@ -1,5 +1,9 @@
/** @param {BNote} note */ import BAttachment = require("../becca/entities/battachment");
function mapNoteToPojo(note) { import BAttribute = require("../becca/entities/battribute");
import BBranch = require("../becca/entities/bbranch");
import BNote = require("../becca/entities/bnote");
function mapNoteToPojo(note: BNote) {
return { return {
noteId: note.noteId, noteId: note.noteId,
isProtected: note.isProtected, isProtected: note.isProtected,
@ -19,8 +23,7 @@ function mapNoteToPojo(note) {
}; };
} }
/** @param {BBranch} branch */ function mapBranchToPojo(branch: BBranch) {
function mapBranchToPojo(branch) {
return { return {
branchId: branch.branchId, branchId: branch.branchId,
noteId: branch.noteId, noteId: branch.noteId,
@ -32,8 +35,7 @@ function mapBranchToPojo(branch) {
}; };
} }
/** @param {BAttribute} attr */ function mapAttributeToPojo(attr: BAttribute) {
function mapAttributeToPojo(attr) {
return { return {
attributeId: attr.attributeId, attributeId: attr.attributeId,
noteId: attr.noteId, noteId: attr.noteId,
@ -46,8 +48,7 @@ function mapAttributeToPojo(attr) {
}; };
} }
/** @param {BAttachment} attachment */ function mapAttachmentToPojo(attachment: BAttachment) {
function mapAttachmentToPojo(attachment) {
return { return {
attachmentId: attachment.attachmentId, attachmentId: attachment.attachmentId,
ownerId: attachment.ownerId, ownerId: attachment.ownerId,
@ -63,7 +64,7 @@ function mapAttachmentToPojo(attachment) {
}; };
} }
module.exports = { export = {
mapNoteToPojo, mapNoteToPojo,
mapBranchToPojo, mapBranchToPojo,
mapAttributeToPojo, mapAttributeToPojo,

View File

@ -1,20 +1,26 @@
const becca = require('../becca/becca'); import becca = require('../becca/becca');
const utils = require('../services/utils'); import utils = require('../services/utils');
const eu = require('./etapi_utils'); import eu = require('./etapi_utils');
const mappers = require('./mappers.js'); import mappers = require('./mappers');
const noteService = require('../services/notes'); import noteService = require('../services/notes');
const TaskContext = require('../services/task_context'); import TaskContext = require('../services/task_context');
const v = require('./validators.js'); import v = require('./validators');
const searchService = require('../services/search/services/search'); import searchService = require('../services/search/services/search');
const SearchContext = require('../services/search/search_context'); import SearchContext = require('../services/search/search_context');
const zipExportService = require('../services/export/zip'); import zipExportService = require('../services/export/zip');
const zipImportService = require('../services/import/zip'); import zipImportService = require('../services/import/zip');
import { Router } from 'express';
import { AppRequest } from '../routes/route-interface';
import { ParsedQs } from 'qs';
import { NoteParams } from '../services/note-interface';
import BNote = require('../becca/entities/bnote');
import { SearchParams } from '../services/search/services/types';
function register(router) { function register(router: Router) {
eu.route(router, 'get', '/etapi/notes', (req, res, next) => { eu.route(router, 'get', '/etapi/notes', (req, res, next) => {
const {search} = req.query; const { search } = req.query;
if (!search?.trim()) { if (typeof search !== "string" || !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.");
} }
@ -24,8 +30,8 @@ function register(router) {
const searchResults = searchService.findResultsWithQuery(search, searchContext); const searchResults = searchService.findResultsWithQuery(search, searchContext);
const foundNotes = searchResults.map(sr => becca.notes[sr.noteId]); const foundNotes = searchResults.map(sr => becca.notes[sr.noteId]);
const resp = { const resp: any = {
results: foundNotes.map(note => mappers.mapNoteToPojo(note)) results: foundNotes.map(note => mappers.mapNoteToPojo(note)),
}; };
if (searchContext.debugInfo) { if (searchContext.debugInfo) {
@ -41,7 +47,7 @@ function register(router) {
res.json(mappers.mapNoteToPojo(note)); res.json(mappers.mapNoteToPojo(note));
}); });
const ALLOWED_PROPERTIES_FOR_CREATE_NOTE = { const ALLOWED_PROPERTIES_FOR_CREATE_NOTE: ValidatorMap = {
'parentNoteId': [v.mandatory, v.notNull, v.isNoteId], 'parentNoteId': [v.mandatory, v.notNull, v.isNoteId],
'title': [v.mandatory, v.notNull, v.isString], 'title': [v.mandatory, v.notNull, v.isString],
'type': [v.mandatory, v.notNull, v.isNoteType], 'type': [v.mandatory, v.notNull, v.isNoteType],
@ -55,10 +61,10 @@ function register(router) {
'utcDateCreated': [v.notNull, v.isString, v.isUtcDateTime] 'utcDateCreated': [v.notNull, v.isString, v.isUtcDateTime]
}; };
eu.route(router, 'post' ,'/etapi/create-note', (req, res, next) => { eu.route(router, 'post', '/etapi/create-note', (req, res, next) => {
const params = {}; const _params = {};
eu.validateAndPatch(_params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_NOTE);
eu.validateAndPatch(params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_NOTE); const params = _params as NoteParams;
try { try {
const resp = noteService.createNewNote(params); const resp = noteService.createNewNote(params);
@ -68,7 +74,7 @@ function register(router) {
branch: mappers.mapBranchToPojo(resp.branch) branch: mappers.mapBranchToPojo(resp.branch)
}); });
} }
catch (e) { catch (e: any) {
return eu.sendError(res, 500, eu.GENERIC_CODE, e.message); return eu.sendError(res, 500, eu.GENERIC_CODE, e.message);
} }
}); });
@ -81,7 +87,7 @@ function register(router) {
'utcDateCreated': [v.notNull, v.isString, v.isUtcDateTime] 'utcDateCreated': [v.notNull, v.isString, v.isUtcDateTime]
}; };
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) {
@ -94,8 +100,8 @@ function register(router) {
res.json(mappers.mapNoteToPojo(note)); res.json(mappers.mapNoteToPojo(note));
}); });
eu.route(router, 'delete' ,'/etapi/notes/:noteId', (req, res, next) => { eu.route(router, 'delete', '/etapi/notes/:noteId', (req, res, next) => {
const {noteId} = req.params; const { noteId } = req.params;
const note = becca.getNote(noteId); const note = becca.getNote(noteId);
@ -139,11 +145,11 @@ function register(router) {
return res.sendStatus(204); return res.sendStatus(204);
}); });
eu.route(router, 'get' ,'/etapi/notes/:noteId/export', (req, res, next) => { eu.route(router, 'get', '/etapi/notes/:noteId/export', (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId); const note = eu.getAndCheckNote(req.params.noteId);
const format = req.query.format || "html"; const format = req.query.format || "html";
if (!["html", "markdown"].includes(format)) { if (typeof format !== "string" || !["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'.`);
} }
@ -153,10 +159,10 @@ function register(router) {
// (e.g. branchIds are not seen in UI), that we export "note export" instead. // (e.g. branchIds are not seen in UI), that we export "note export" instead.
const branch = note.getParentBranches()[0]; const branch = note.getParentBranches()[0];
zipExportService.exportToZip(taskContext, branch, format, res); zipExportService.exportToZip(taskContext, branch, format as "html" | "markdown", res);
}); });
eu.route(router, 'post' ,'/etapi/notes/:noteId/import', (req, res, next) => { eu.route(router, 'post', '/etapi/notes/:noteId/import', (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId); const note = eu.getAndCheckNote(req.params.noteId);
const taskContext = new TaskContext('no-progress-reporting'); const taskContext = new TaskContext('no-progress-reporting');
@ -168,7 +174,7 @@ function register(router) {
}); // we need better error handling here, async errors won't be properly processed. }); // we need better error handling here, async errors won't be properly processed.
}); });
eu.route(router, 'post' ,'/etapi/notes/:noteId/revision', (req, res, next) => { eu.route(router, 'post', '/etapi/notes/:noteId/revision', (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId); const note = eu.getAndCheckNote(req.params.noteId);
note.saveRevision(); note.saveRevision();
@ -178,7 +184,7 @@ function register(router) {
eu.route(router, 'get', '/etapi/notes/:noteId/attachments', (req, res, next) => { eu.route(router, 'get', '/etapi/notes/:noteId/attachments', (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId); const note = eu.getAndCheckNote(req.params.noteId);
const attachments = note.getAttachments({includeContentLength: true}) const attachments = note.getAttachments({ includeContentLength: true })
res.json( res.json(
attachments.map(attachment => mappers.mapAttachmentToPojo(attachment)) attachments.map(attachment => mappers.mapAttachmentToPojo(attachment))
@ -186,23 +192,24 @@ function register(router) {
}); });
} }
function parseSearchParams(req) { function parseSearchParams(req: AppRequest) {
const rawSearchParams = { const rawSearchParams: SearchParams = {
fastSearch: parseBoolean(req.query, 'fastSearch'), fastSearch: parseBoolean(req.query, 'fastSearch'),
includeArchivedNotes: parseBoolean(req.query, 'includeArchivedNotes'), includeArchivedNotes: parseBoolean(req.query, 'includeArchivedNotes'),
ancestorNoteId: req.query['ancestorNoteId'], ancestorNoteId: parseString(req.query['ancestorNoteId']),
ancestorDepth: req.query['ancestorDepth'], // e.g. "eq5" ancestorDepth: parseString(req.query['ancestorDepth']), // e.g. "eq5"
orderBy: req.query['orderBy'], orderBy: parseString(req.query['orderBy']),
orderDirection: parseOrderDirection(req.query, 'orderDirection'), // TODO: Check why the order direction was provided as a number, but it's a string everywhere else.
orderDirection: parseOrderDirection(req.query, 'orderDirection') as unknown as string,
limit: parseInteger(req.query, 'limit'), limit: parseInteger(req.query, 'limit'),
debug: parseBoolean(req.query, 'debug') debug: parseBoolean(req.query, 'debug')
}; };
const searchParams = {}; const searchParams: SearchParams = {};
for (const paramName of Object.keys(rawSearchParams)) { for (const paramName of Object.keys(rawSearchParams) as (keyof SearchParams)[]) {
if (rawSearchParams[paramName] !== undefined) { if (rawSearchParams[paramName] !== undefined) {
searchParams[paramName] = rawSearchParams[paramName]; (searchParams as any)[paramName] = rawSearchParams[paramName];
} }
} }
@ -211,7 +218,15 @@ function parseSearchParams(req) {
const SEARCH_PARAM_ERROR = "SEARCH_PARAM_VALIDATION_ERROR"; const SEARCH_PARAM_ERROR = "SEARCH_PARAM_VALIDATION_ERROR";
function parseBoolean(obj, name) { function parseString(value: string | ParsedQs | string[] | ParsedQs[] | undefined): string | undefined {
if (typeof value === "string") {
return value;
}
return undefined;
}
function parseBoolean(obj: any, name: string) {
if (!(name in obj)) { if (!(name in obj)) {
return undefined; return undefined;
} }
@ -223,7 +238,7 @@ function parseBoolean(obj, name) {
return obj[name] === 'true'; return obj[name] === 'true';
} }
function parseOrderDirection(obj, name) { function parseOrderDirection(obj: any, name: string) {
if (!(name in obj)) { if (!(name in obj)) {
return undefined; return undefined;
} }
@ -237,7 +252,7 @@ function parseOrderDirection(obj, name) {
return integer; return integer;
} }
function parseInteger(obj, name) { function parseInteger(obj: any, name: string) {
if (!(name in obj)) { if (!(name in obj)) {
return undefined; return undefined;
} }
@ -251,6 +266,6 @@ function parseInteger(obj, name) {
return integer; return integer;
} }
module.exports = { export = {
register register
}; };

View File

@ -1,10 +1,12 @@
const fs = require('fs'); import { Router } from "express";
const path = require('path');
import fs = require('fs');
import path = require('path');
const specPath = path.join(__dirname, 'etapi.openapi.yaml'); const specPath = path.join(__dirname, 'etapi.openapi.yaml');
let spec = null; let spec: string | null = null;
function register(router) { function register(router: Router) {
router.get('/etapi/etapi.openapi.yaml', (req, res, next) => { router.get('/etapi/etapi.openapi.yaml', (req, res, next) => {
if (!spec) { if (!spec) {
spec = fs.readFileSync(specPath, 'utf8'); spec = fs.readFileSync(specPath, 'utf8');
@ -15,6 +17,6 @@ function register(router) {
}); });
} }
module.exports = { export = {
register register
}; };

View File

@ -1,13 +1,14 @@
const specialNotesService = require('../services/special_notes'); import specialNotesService = require('../services/special_notes');
const dateNotesService = require('../services/date_notes'); import dateNotesService = require('../services/date_notes');
const eu = require('./etapi_utils'); import eu = require('./etapi_utils');
const mappers = require('./mappers.js'); import mappers = require('./mappers');
import { Router } from 'express';
const getDateInvalidError = date => new eu.EtapiError(400, "DATE_INVALID", `Date "${date}" is not valid.`); const getDateInvalidError = (date: string) => new eu.EtapiError(400, "DATE_INVALID", `Date "${date}" is not valid.`);
const getMonthInvalidError = month => new eu.EtapiError(400, "MONTH_INVALID", `Month "${month}" is not valid.`); const getMonthInvalidError = (month: string)=> new eu.EtapiError(400, "MONTH_INVALID", `Month "${month}" is not valid.`);
const getYearInvalidError = year => new eu.EtapiError(400, "YEAR_INVALID", `Year "${year}" is not valid.`); const getYearInvalidError = (year: string) => new eu.EtapiError(400, "YEAR_INVALID", `Year "${year}" is not valid.`);
function isValidDate(date) { function isValidDate(date: string) {
if (!/[0-9]{4}-[0-9]{2}-[0-9]{2}/.test(date)) { if (!/[0-9]{4}-[0-9]{2}-[0-9]{2}/.test(date)) {
return false; return false;
} }
@ -15,7 +16,7 @@ function isValidDate(date) {
return !!Date.parse(date); return !!Date.parse(date);
} }
function register(router) { function register(router: Router) {
eu.route(router, 'get', '/etapi/inbox/:date', (req, res, next) => { eu.route(router, 'get', '/etapi/inbox/:date', (req, res, next) => {
const { date } = req.params; const { date } = req.params;
@ -72,6 +73,6 @@ function register(router) {
}); });
} }
module.exports = { export = {
register register
}; };

View File

@ -1,19 +1,19 @@
const noteTypeService = require('../services/note_types'); import noteTypeService = require('../services/note_types');
const dateUtils = require('../services/date_utils'); import dateUtils = require('../services/date_utils');
function mandatory(obj) { function mandatory(obj: unknown) {
if (obj === undefined ) { if (obj === undefined) {
return `mandatory, but not set`; return `mandatory, but not set`;
} }
} }
function notNull(obj) { function notNull(obj: unknown) {
if (obj === null) { if (obj === null) {
return `cannot be null`; return `cannot be null`;
} }
} }
function isString(obj) { function isString(obj: unknown) {
if (obj === undefined || obj === null) { if (obj === undefined || obj === null) {
return; return;
} }
@ -23,23 +23,23 @@ function isString(obj) {
} }
} }
function isLocalDateTime(obj) { function isLocalDateTime(obj: unknown) {
if (obj === undefined || obj === null) { if (typeof obj !== "string") {
return; return;
} }
return dateUtils.validateLocalDateTime(obj); return dateUtils.validateLocalDateTime(obj);
} }
function isUtcDateTime(obj) { function isUtcDateTime(obj: unknown) {
if (obj === undefined || obj === null) { if (typeof obj !== "string") {
return; return;
} }
return dateUtils.validateUtcDateTime(obj); return dateUtils.validateUtcDateTime(obj);
} }
function isBoolean(obj) { function isBoolean(obj: unknown) {
if (obj === undefined || obj === null) { if (obj === undefined || obj === null) {
return; return;
} }
@ -49,7 +49,7 @@ function isBoolean(obj) {
} }
} }
function isInteger(obj) { function isInteger(obj: unknown) {
if (obj === undefined || obj === null) { if (obj === undefined || obj === null) {
return; return;
} }
@ -59,7 +59,7 @@ function isInteger(obj) {
} }
} }
function isNoteId(obj) { function isNoteId(obj: unknown) {
if (obj === undefined || obj === null) { if (obj === undefined || obj === null) {
return; return;
} }
@ -75,29 +75,29 @@ function isNoteId(obj) {
} }
} }
function isNoteType(obj) { function isNoteType(obj: unknown) {
if (obj === undefined || obj === null) { if (obj === undefined || obj === null) {
return; return;
} }
const noteTypes = noteTypeService.getNoteTypeNames(); const noteTypes = noteTypeService.getNoteTypeNames();
if (!noteTypes.includes(obj)) { if (typeof obj !== "string" || !noteTypes.includes(obj)) {
return `'${obj}' is not a valid note type, allowed types are: ${noteTypes.join(", ")}`; return `'${obj}' is not a valid note type, allowed types are: ${noteTypes.join(", ")}`;
} }
} }
function isAttributeType(obj) { function isAttributeType(obj: unknown) {
if (obj === undefined || obj === null) { if (obj === undefined || obj === null) {
return; return;
} }
if (!['label', 'relation'].includes(obj)) { if (typeof obj !== "string" || !['label', 'relation'].includes(obj)) {
return `'${obj}' is not a valid attribute type, allowed types are: label, relation`; return `'${obj}' is not a valid attribute type, allowed types are: label, relation`;
} }
} }
function isValidEntityId(obj) { function isValidEntityId(obj: unknown) {
if (obj === undefined || obj === null) { if (obj === undefined || obj === null) {
return; return;
} }
@ -107,7 +107,7 @@ function isValidEntityId(obj) {
} }
} }
module.exports = { export = {
mandatory, mandatory,
notNull, notNull,
isString, isString,

View File

@ -20,6 +20,13 @@ function updateFile(req: AppRequest) {
const note = becca.getNoteOrThrow(req.params.noteId); const note = becca.getNoteOrThrow(req.params.noteId);
const file = req.file; const file = req.file;
if (!file) {
return {
uploaded: false,
message: `Missing file.`
};
}
note.saveRevision(); note.saveRevision();
note.mime = file.mimetype.toLowerCase(); note.mime = file.mimetype.toLowerCase();
@ -39,6 +46,12 @@ function updateFile(req: AppRequest) {
function updateAttachment(req: AppRequest) { function updateAttachment(req: AppRequest) {
const attachment = becca.getAttachmentOrThrow(req.params.attachmentId); const attachment = becca.getAttachmentOrThrow(req.params.attachmentId);
const file = req.file; const file = req.file;
if (!file) {
return {
uploaded: false,
message: `Missing file.`
};
}
attachment.getNote().saveRevision(); attachment.getNote().saveRevision();

View File

@ -88,6 +88,13 @@ function updateImage(req: AppRequest) {
const note = becca.getNoteOrThrow(noteId); const note = becca.getNoteOrThrow(noteId);
if (!file) {
return {
uploaded: false,
message: `Missing image data.`
};
}
if (!["image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"].includes(file.mimetype)) { if (!["image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"].includes(file.mimetype)) {
return { return {
uploaded: false, uploaded: false,

View File

@ -11,6 +11,13 @@ import { AppRequest } from '../route-interface';
function uploadImage(req: AppRequest) { function uploadImage(req: AppRequest) {
const file = req.file; const file = req.file;
if (!file) {
return {
uploaded: false,
message: `Missing image data.`
};
}
if (!["image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"].includes(file.mimetype)) { if (!["image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"].includes(file.mimetype)) {
return [400, `Unknown image type: ${file.mimetype}`]; return [400, `Unknown image type: ${file.mimetype}`];
} }

View File

@ -67,7 +67,7 @@ function login(req: AppRequest, res: Response) {
if (rememberMe) { if (rememberMe) {
req.session.cookie.maxAge = 21 * 24 * 3600000; // 3 weeks req.session.cookie.maxAge = 21 * 24 * 3600000; // 3 weeks
} else { } else {
req.session.cookie.expires = false; req.session.cookie.expires = null;
} }
req.session.loggedIn = true; req.session.loggedIn = true;

View File

@ -1,5 +1,5 @@
import { Request } from "express"; import { NextFunction, Request, Response } from "express";
import { File } from "../services/import/common"; import { Session, SessionData } from "express-session";
export interface AppRequest extends Request { export interface AppRequest extends Request {
headers: { headers: {
@ -7,14 +7,15 @@ export interface AppRequest extends Request {
"trilium-cred"?: string; "trilium-cred"?: string;
"x-local-date"?: string; "x-local-date"?: string;
"x-labels"?: string; "x-labels"?: string;
"trilium-local-now-datetime"?: string;
} }
session: { session: Session & Partial<SessionData> & {
loggedIn: boolean; loggedIn: boolean;
cookie: {
maxAge: number;
expires: boolean
};
regenerate: (callback: () => void) => void;
} }
file: File; }
}
export type AppRequestHandler = (
req: AppRequest,
res: Response,
next: NextFunction
) => void;

View File

@ -61,14 +61,14 @@ const relationMapApiRoute = require('./api/relation-map');
const otherRoute = require('./api/other'); const otherRoute = require('./api/other');
const shareRoutes = require('../share/routes.js'); const shareRoutes = require('../share/routes.js');
const etapiAuthRoutes = require('../etapi/auth.js'); const etapiAuthRoutes = require('../etapi/auth');
const etapiAppInfoRoutes = require('../etapi/app_info'); const etapiAppInfoRoutes = require('../etapi/app_info');
const etapiAttachmentRoutes = require('../etapi/attachments.js'); const etapiAttachmentRoutes = require('../etapi/attachments');
const etapiAttributeRoutes = require('../etapi/attributes'); const etapiAttributeRoutes = require('../etapi/attributes');
const etapiBranchRoutes = require('../etapi/branches.js'); const etapiBranchRoutes = require('../etapi/branches');
const etapiNoteRoutes = require('../etapi/notes.js'); const etapiNoteRoutes = require('../etapi/notes');
const etapiSpecialNoteRoutes = require('../etapi/special_notes'); const etapiSpecialNoteRoutes = require('../etapi/special_notes');
const etapiSpecRoute = require('../etapi/spec.js'); const etapiSpecRoute = require('../etapi/spec');
const etapiBackupRoute = require('../etapi/backup'); const etapiBackupRoute = require('../etapi/backup');
const csrfMiddleware = csurf({ const csrfMiddleware = csurf({