Merge pull request #43 from TriliumNext/feature/typescript_backend_7

Convert backend to TypeScript (71% -> 80%)
This commit is contained in:
Elian Doran 2024-04-15 21:51:00 +03:00 committed by GitHub
commit 98d12901a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 615 additions and 444 deletions

View File

@ -161,6 +161,14 @@ export default class Becca {
return row ? new BRevision(row) : null;
}
getRevisionOrThrow(revisionId: string): BRevision {
const revision = this.getRevision(revisionId);
if (!revision) {
throw new NotFoundError(`Revision '${revisionId}' has not been found.`);
}
return revision;
}
getAttachment(attachmentId: string, opts: AttachmentOpts = {}): BAttachment | null {
opts.includeContentLength = !!opts.includeContentLength;
@ -246,7 +254,7 @@ export default class Becca {
return rows.map(row => new BRecentNote(row));
}
getRevisionsFromQuery(query: string, params = []): BRevision[] {
getRevisionsFromQuery(query: string, params: string[] = []): BRevision[] {
const rows = sql.getRows<RevisionRow>(query, params);
const BRevision = require('./entities/brevision'); // avoiding circular dependency problems
@ -288,4 +296,18 @@ export interface ConstructorData<T extends AbstractBeccaEntity<T>> {
primaryKeyName: string;
entityName: string;
hashedProperties: (keyof T)[];
}
export interface NotePojo {
noteId: string;
title?: string;
isProtected?: boolean;
type: string;
mime: string;
blobId?: string;
isDeleted: boolean;
dateCreated?: string;
dateModified?: string;
utcDateCreated: string;
utcDateModified?: string;
}

View File

@ -26,8 +26,8 @@ interface ContentOpts {
abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
utcDateModified?: string;
protected dateCreated?: string;
protected dateModified?: string;
dateCreated?: string;
dateModified?: string;
utcDateCreated!: string;

View File

@ -15,6 +15,7 @@ import eventService = require('../../services/events');
import { AttachmentRow, NoteRow, NoteType, RevisionRow } from './rows';
import BBranch = require('./bbranch');
import BAttribute = require('./battribute');
import { NotePojo } from '../becca-interface';
dayjs.extend(utc);
const LABEL = 'label';
@ -222,7 +223,7 @@ class BNote extends AbstractBeccaEntity<BNote> {
/**
* @throws Error in case of invalid JSON */
getJsonContent(): {} | null {
getJsonContent(): any | null {
const content = this.getContent();
if (typeof content !== "string" || !content || !content.trim()) {
@ -1679,7 +1680,7 @@ class BNote extends AbstractBeccaEntity<BNote> {
this.utcDateModified = dateUtils.utcNowDateTime();
}
getPojo() {
getPojo(): NotePojo {
return {
noteId: this.noteId,
title: this.title || undefined,

View File

@ -40,7 +40,7 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
utcDateLastEdited?: string;
utcDateCreated!: string;
contentLength?: number;
content?: string;
content?: string | Buffer;
constructor(row: RevisionRow, titleDecrypted = false) {
super();
@ -91,9 +91,8 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
*
* This is the same approach as is used for Note's content.
*/
// TODO: initial declaration included Buffer, but everywhere it's treated as a string.
getContent(): string {
return this._getContent() as string;
getContent(): string | Buffer {
return this._getContent();
}
/**
@ -101,7 +100,7 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
getJsonContent(): {} | null {
const content = this.getContent();
if (!content || !content.trim()) {
if (!content || typeof content !== "string" || !content.trim()) {
return null;
}

View File

@ -1,27 +1,26 @@
"use strict";
const imageService = require('../../services/image');
const becca = require('../../becca/becca');
import imageService = require('../../services/image');
import becca = require('../../becca/becca');
const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR;
const fs = require('fs');
import fs = require('fs');
import { Request, Response } from 'express';
import BNote = require('../../becca/entities/bnote');
import BRevision = require('../../becca/entities/brevision');
function returnImageFromNote(req, res) {
function returnImageFromNote(req: Request, res: Response) {
const image = becca.getNote(req.params.noteId);
return returnImageInt(image, res);
}
function returnImageFromRevision(req, res) {
function returnImageFromRevision(req: Request, res: Response) {
const image = becca.getRevision(req.params.revisionId);
return returnImageInt(image, res);
}
/**
* @param {BNote|BRevision} image
* @param res
*/
function returnImageInt(image, res) {
function returnImageInt(image: BNote | BRevision | null, res: Response) {
if (!image) {
res.set('Content-Type', 'image/png');
return res.send(fs.readFileSync(`${RESOURCE_DIR}/db/image-deleted.png`));
@ -40,12 +39,13 @@ function returnImageInt(image, res) {
}
}
function renderSvgAttachment(image, res, attachmentName) {
function renderSvgAttachment(image: BNote | BRevision, res: Response, attachmentName: string) {
let svgString = '<svg/>'
const attachment = image.getAttachmentByTitle(attachmentName);
if (attachment) {
svgString = attachment.getContent();
const content = attachment.getContent();
if (attachment && typeof content === "string") {
svgString = content;
} else {
// backwards compatibility, before attachments, the SVG was stored in the main note content as a separate key
const contentSvg = image.getJsonContentSafely()?.svg;
@ -62,7 +62,7 @@ function renderSvgAttachment(image, res, attachmentName) {
}
function returnAttachedImage(req, res) {
function returnAttachedImage(req: Request, res: Response) {
const attachment = becca.getAttachment(req.params.attachmentId);
if (!attachment) {
@ -81,9 +81,9 @@ function returnAttachedImage(req, res) {
res.send(attachment.getContent());
}
function updateImage(req) {
function updateImage(req: Request) {
const {noteId} = req.params;
const {file} = req;
const {file} = (req as any);
const note = becca.getNoteOrThrow(noteId);
@ -99,7 +99,7 @@ function updateImage(req) {
return { uploaded: true };
}
module.exports = {
export = {
returnImageFromNote,
returnImageFromRevision,
returnAttachedImage,

View File

@ -1,18 +1,20 @@
"use strict";
const enexImportService = require('../../services/import/enex');
const opmlImportService = require('../../services/import/opml');
const zipImportService = require('../../services/import/zip');
const singleImportService = require('../../services/import/single');
const cls = require('../../services/cls');
const path = require('path');
const becca = require('../../becca/becca');
const beccaLoader = require('../../becca/becca_loader');
const log = require('../../services/log');
const TaskContext = require('../../services/task_context');
const ValidationError = require('../../errors/validation_error');
import enexImportService = require('../../services/import/enex');
import opmlImportService = require('../../services/import/opml');
import zipImportService = require('../../services/import/zip');
import singleImportService = require('../../services/import/single');
import cls = require('../../services/cls');
import path = require('path');
import becca = require('../../becca/becca');
import beccaLoader = require('../../becca/becca_loader');
import log = require('../../services/log');
import TaskContext = require('../../services/task_context');
import ValidationError = require('../../errors/validation_error');
import { Request } from 'express';
import BNote = require('../../becca/entities/bnote');
async function importNotesToBranch(req) {
async function importNotesToBranch(req: Request) {
const { parentNoteId } = req.params;
const { taskId, last } = req.body;
@ -25,7 +27,7 @@ async function importNotesToBranch(req) {
replaceUnderscoresWithSpaces: req.body.replaceUnderscoresWithSpaces !== 'false'
};
const file = req.file;
const file = (req as any).file;
if (!file) {
throw new ValidationError("No file has been uploaded");
@ -42,7 +44,7 @@ async function importNotesToBranch(req) {
// eliminate flickering during import
cls.ignoreEntityChangeIds();
let note; // typically root of the import - client can show it after finishing the import
let note: BNote | null; // typically root of the import - client can show it after finishing the import
const taskContext = TaskContext.getInstance(taskId, 'importNotes', options);
@ -50,14 +52,24 @@ async function importNotesToBranch(req) {
if (extension === '.zip' && options.explodeArchives) {
note = await zipImportService.importZip(taskContext, file.buffer, parentNote);
} else if (extension === '.opml' && options.explodeArchives) {
note = await opmlImportService.importOpml(taskContext, file.buffer, parentNote);
const importResult = await opmlImportService.importOpml(taskContext, file.buffer, parentNote);
if (!Array.isArray(importResult)) {
note = importResult;
} else {
return importResult;
}
} else if (extension === '.enex' && options.explodeArchives) {
note = await enexImportService.importEnex(taskContext, file, parentNote);
const importResult = await enexImportService.importEnex(taskContext, file, parentNote);
if (!Array.isArray(importResult)) {
note = importResult;
} else {
return importResult;
}
} else {
note = await singleImportService.importSingleFile(taskContext, file, parentNote);
}
}
catch (e) {
catch (e: any) {
const message = `Import failed with following error: '${e.message}'. More details might be in the logs.`;
taskContext.reportError(message);
@ -66,11 +78,15 @@ async function importNotesToBranch(req) {
return [500, message];
}
if (!note) {
return [500, "No note was generated as a result of the import."];
}
if (last === "true") {
// small timeout to avoid race condition (the message is received before the transaction is committed)
setTimeout(() => taskContext.taskSucceeded({
parentNoteId: parentNoteId,
importedNoteId: note.noteId
importedNoteId: note?.noteId
}), 1000);
}
@ -80,7 +96,7 @@ async function importNotesToBranch(req) {
return note.getPojo();
}
async function importAttachmentsToNote(req) {
async function importAttachmentsToNote(req: Request) {
const { parentNoteId } = req.params;
const { taskId, last } = req.body;
@ -88,7 +104,7 @@ async function importAttachmentsToNote(req) {
shrinkImages: req.body.shrinkImages !== 'false',
};
const file = req.file;
const file = (req as any).file;
if (!file) {
throw new ValidationError("No file has been uploaded");
@ -102,7 +118,7 @@ async function importAttachmentsToNote(req) {
try {
await singleImportService.importAttachment(taskContext, file, parentNote);
}
catch (e) {
catch (e: any) {
const message = `Import failed with following error: '${e.message}'. More details might be in the logs.`;
taskContext.reportError(message);
@ -119,7 +135,7 @@ async function importAttachmentsToNote(req) {
}
}
module.exports = {
export = {
importNotesToBranch,
importAttachmentsToNote
};

View File

@ -1,7 +1,7 @@
"use strict";
const keyboardActions = require('../../services/keyboard_actions');
const becca = require('../../becca/becca');
import keyboardActions = require('../../services/keyboard_actions');
import becca = require('../../becca/becca');
function getKeyboardActions() {
return keyboardActions.getKeyboardActions();
@ -14,7 +14,7 @@ function getShortcutsForNotes() {
return labels.filter(attr => becca.getNote(attr.noteId)?.type !== 'launcher');
}
module.exports = {
export = {
getKeyboardActions,
getShortcutsForNotes
};

View File

@ -1,19 +1,20 @@
"use strict";
const options = require('../../services/options');
const utils = require('../../services/utils');
const dateUtils = require('../../services/date_utils');
const instanceId = require('../../services/instance_id');
const passwordEncryptionService = require('../../services/encryption/password_encryption');
const protectedSessionService = require('../../services/protected_session');
const appInfo = require('../../services/app_info');
const eventService = require('../../services/events');
const sqlInit = require('../../services/sql_init');
const sql = require('../../services/sql');
const ws = require('../../services/ws');
const etapiTokenService = require('../../services/etapi_tokens');
import options = require('../../services/options');
import utils = require('../../services/utils');
import dateUtils = require('../../services/date_utils');
import instanceId = require('../../services/instance_id');
import passwordEncryptionService = require('../../services/encryption/password_encryption');
import protectedSessionService = require('../../services/protected_session');
import appInfo = require('../../services/app_info');
import eventService = require('../../services/events');
import sqlInit = require('../../services/sql_init');
import sql = require('../../services/sql');
import ws = require('../../services/ws');
import etapiTokenService = require('../../services/etapi_tokens');
import { Request } from 'express';
function loginSync(req) {
function loginSync(req: Request) {
if (!sqlInit.schemaExists()) {
return [500, { message: "DB schema does not exist, can't sync." }];
}
@ -44,7 +45,7 @@ function loginSync(req) {
return [400, { message: "Sync login credentials are incorrect. It looks like you're trying to sync two different initialized documents which is not possible." }];
}
req.session.loggedIn = true;
(req as any).session.loggedIn = true;
return {
instanceId: instanceId,
@ -52,7 +53,7 @@ function loginSync(req) {
};
}
function loginToProtectedSession(req) {
function loginToProtectedSession(req: Request) {
const password = req.body.password;
if (!passwordEncryptionService.verifyPassword(password)) {
@ -63,6 +64,12 @@ function loginToProtectedSession(req) {
}
const decryptedDataKey = passwordEncryptionService.getDataKey(password);
if (!decryptedDataKey) {
return {
success: false,
message: "Unable to obtain data key."
}
}
protectedSessionService.setDataKey(decryptedDataKey);
@ -87,7 +94,7 @@ function touchProtectedSession() {
protectedSessionService.touchProtectedSession();
}
function token(req) {
function token(req: Request) {
const password = req.body.password;
if (!passwordEncryptionService.verifyPassword(password)) {
@ -102,7 +109,7 @@ function token(req) {
return { token: authToken };
}
module.exports = {
export = {
loginSync,
loginToProtectedSession,
logoutFromProtectedSession,

View File

@ -1,18 +1,25 @@
"use strict";
const becca = require('../../becca/becca');
const { JSDOM } = require("jsdom");
import becca = require('../../becca/becca');
import { JSDOM } from "jsdom";
import BNote = require('../../becca/entities/bnote');
import BAttribute = require('../../becca/entities/battribute');
import { Request } from 'express';
import ValidationError = require('../../errors/validation_error');
function buildDescendantCountMap(noteIdsToCount) {
function buildDescendantCountMap(noteIdsToCount: string[]) {
if (!Array.isArray(noteIdsToCount)) {
throw new Error('noteIdsToCount: type error');
}
const noteIdToCountMap = Object.create(null);
function getCount(noteId) {
function getCount(noteId: string) {
if (!(noteId in noteIdToCountMap)) {
const note = becca.getNote(noteId);
if (!note) {
return;
}
const hiddenImageNoteIds = note.getRelations('imageLink').map(rel => rel.value);
const childNoteIds = note.children.map(child => child.noteId);
@ -33,19 +40,14 @@ function buildDescendantCountMap(noteIdsToCount) {
return noteIdToCountMap;
}
/**
* @param {BNote} note
* @param {int} depth
* @returns {string[]} noteIds
*/
function getNeighbors(note, depth) {
function getNeighbors(note: BNote, depth: number): string[] {
if (depth === 0) {
return [];
}
const retNoteIds = [];
function isIgnoredRelation(relation) {
function isIgnoredRelation(relation: BAttribute) {
return ['relationMapLink', 'template', 'inherit', 'image', 'ancestor'].includes(relation.name);
}
@ -90,8 +92,9 @@ function getNeighbors(note, depth) {
return retNoteIds;
}
function getLinkMap(req) {
const mapRootNote = becca.getNote(req.params.noteId);
function getLinkMap(req: Request) {
const mapRootNote = becca.getNoteOrThrow(req.params.noteId);
// if the map root itself has "excludeFromNoteMap" attribute (journal typically) then there wouldn't be anything
// to display, so we'll just ignore it
const ignoreExcludeFromNoteMap = mapRootNote.isLabelTruthy('excludeFromNoteMap');
@ -125,7 +128,7 @@ function getLinkMap(req) {
const noteIdsArray = Array.from(noteIds)
const notes = noteIdsArray.map(noteId => {
const note = becca.getNote(noteId);
const note = becca.getNoteOrThrow(noteId);
return [
note.noteId,
@ -144,6 +147,9 @@ function getLinkMap(req) {
}
else if (rel.name === 'imageLink') {
const parentNote = becca.getNote(rel.noteId);
if (!parentNote) {
return false;
}
return !parentNote.getChildNotes().find(childNote => childNote.noteId === rel.value);
}
@ -165,8 +171,8 @@ function getLinkMap(req) {
};
}
function getTreeMap(req) {
const mapRootNote = becca.getNote(req.params.noteId);
function getTreeMap(req: Request) {
const mapRootNote = becca.getNoteOrThrow(req.params.noteId);
// if the map root itself has "excludeFromNoteMap" (journal typically) then there wouldn't be anything to display,
// so we'll just ignore it
const ignoreExcludeFromNoteMap = mapRootNote.isLabelTruthy('excludeFromNoteMap');
@ -198,8 +204,8 @@ function getTreeMap(req) {
note.getLabelValue('color')
]);
const noteIds = new Set();
notes.forEach(([noteId]) => noteIds.add(noteId));
const noteIds = new Set<string>();
notes.forEach(([noteId]) => noteId && noteIds.add(noteId));
const links = [];
@ -225,7 +231,7 @@ function getTreeMap(req) {
};
}
function updateDescendantCountMapForSearch(noteIdToDescendantCountMap, relationships) {
function updateDescendantCountMapForSearch(noteIdToDescendantCountMap: Record<string, number>, relationships: { parentNoteId: string, childNoteId: string }[]) {
for (const {parentNoteId, childNoteId} of relationships) {
const parentNote = becca.notes[parentNoteId];
if (!parentNote || parentNote.type !== 'search') {
@ -237,16 +243,17 @@ function updateDescendantCountMapForSearch(noteIdToDescendantCountMap, relations
}
}
function removeImages(document) {
function removeImages(document: Document) {
const images = document.getElementsByTagName('img');
while (images.length > 0) {
images[0].parentNode.removeChild(images[0]);
while (images && images.length > 0) {
images[0]?.parentNode?.removeChild(images[0]);
}
}
const EXCERPT_CHAR_LIMIT = 200;
type ElementOrText = (Element | Text);
function findExcerpts(sourceNote, referencedNoteId) {
function findExcerpts(sourceNote: BNote, referencedNoteId: string) {
const html = sourceNote.getContent();
const document = new JSDOM(html).window.document;
@ -263,25 +270,24 @@ function findExcerpts(sourceNote, referencedNoteId) {
linkEl.classList.add("backlink-link");
let centerEl = linkEl;
let centerEl: HTMLElement = linkEl;
while (centerEl.tagName !== 'BODY' && centerEl.parentElement?.textContent?.length <= EXCERPT_CHAR_LIMIT) {
while (centerEl.tagName !== 'BODY' && centerEl.parentElement && (centerEl.parentElement?.textContent?.length || 0) <= EXCERPT_CHAR_LIMIT) {
centerEl = centerEl.parentElement;
}
/** @var {HTMLElement[]} */
const excerptEls = [centerEl];
let excerptLength = centerEl.textContent.length;
let left = centerEl;
let right = centerEl;
const excerptEls: ElementOrText[] = [centerEl];
let excerptLength = centerEl.textContent?.length || 0;
let left: ElementOrText = centerEl;
let right: ElementOrText = centerEl;
while (excerptLength < EXCERPT_CHAR_LIMIT) {
let added = false;
const prev = left.previousElementSibling;
const prev: Element | null = left.previousElementSibling;
if (prev) {
const prevText = prev.textContent;
const prevText = prev.textContent || "";
if (prevText.length + excerptLength > EXCERPT_CHAR_LIMIT) {
const prefix = prevText.substr(prevText.length - (EXCERPT_CHAR_LIMIT - excerptLength));
@ -298,12 +304,12 @@ function findExcerpts(sourceNote, referencedNoteId) {
added = true;
}
const next = right.nextElementSibling;
const next: Element | null = right.nextElementSibling;
if (next) {
const nextText = next.textContent;
if (nextText.length + excerptLength > EXCERPT_CHAR_LIMIT) {
if (nextText && nextText.length + excerptLength > EXCERPT_CHAR_LIMIT) {
const suffix = nextText.substr(nextText.length - (EXCERPT_CHAR_LIMIT - excerptLength));
const textNode = document.createTextNode(`${suffix}`);
@ -314,7 +320,7 @@ function findExcerpts(sourceNote, referencedNoteId) {
right = next;
excerptEls.push(right);
excerptLength += nextText.length;
excerptLength += nextText?.length || 0;
added = true;
}
@ -336,13 +342,13 @@ function findExcerpts(sourceNote, referencedNoteId) {
return excerpts;
}
function getFilteredBacklinks(note) {
function getFilteredBacklinks(note: BNote) {
return note.getTargetRelations()
// search notes have "ancestor" relations which are not interesting
.filter(relation => !!relation.getNote() && relation.getNote().type !== 'search');
}
function getBacklinkCount(req) {
function getBacklinkCount(req: Request) {
const {noteId} = req.params;
const note = becca.getNoteOrThrow(noteId);
@ -352,7 +358,7 @@ function getBacklinkCount(req) {
};
}
function getBacklinks(req) {
function getBacklinks(req: Request) {
const {noteId} = req.params;
const note = becca.getNoteOrThrow(noteId);
@ -379,7 +385,7 @@ function getBacklinks(req) {
});
}
module.exports = {
export = {
getLinkMap,
getTreeMap,
getBacklinkCount,

View File

@ -1,25 +1,28 @@
"use strict";
const noteService = require('../../services/notes');
const eraseService = require('../../services/erase');
const treeService = require('../../services/tree');
const sql = require('../../services/sql');
const utils = require('../../services/utils');
const log = require('../../services/log');
const TaskContext = require('../../services/task_context');
const becca = require('../../becca/becca');
const ValidationError = require('../../errors/validation_error');
const blobService = require('../../services/blob');
import noteService = require('../../services/notes');
import eraseService = require('../../services/erase');
import treeService = require('../../services/tree');
import sql = require('../../services/sql');
import utils = require('../../services/utils');
import log = require('../../services/log');
import TaskContext = require('../../services/task_context');
import becca = require('../../becca/becca');
import ValidationError = require('../../errors/validation_error');
import blobService = require('../../services/blob');
import { Request } from 'express';
import BBranch = require('../../becca/entities/bbranch');
import { AttributeRow } from '../../becca/entities/rows';
function getNote(req) {
function getNote(req: Request) {
return becca.getNoteOrThrow(req.params.noteId);
}
function getNoteBlob(req) {
function getNoteBlob(req: Request) {
return blobService.getBlobPojo('notes', req.params.noteId);
}
function getNoteMetadata(req) {
function getNoteMetadata(req: Request) {
const note = becca.getNoteOrThrow(req.params.noteId);
return {
@ -30,12 +33,20 @@ function getNoteMetadata(req) {
};
}
function createNote(req) {
function createNote(req: Request) {
const params = Object.assign({}, req.body); // clone
params.parentNoteId = req.params.parentNoteId;
const { target, targetBranchId } = req.query;
if (target !== "into" && target !== "after") {
throw new ValidationError("Invalid target type.");
}
if (targetBranchId && typeof targetBranchId !== "string") {
throw new ValidationError("Missing or incorrect type for target branch ID.");
}
const { note, branch } = noteService.createNewNoteWithTarget(target, targetBranchId, params);
return {
@ -44,14 +55,14 @@ function createNote(req) {
};
}
function updateNoteData(req) {
function updateNoteData(req: Request) {
const {content, attachments} = req.body;
const {noteId} = req.params;
return noteService.updateNoteData(noteId, content, attachments);
}
function deleteNote(req) {
function deleteNote(req: Request) {
const noteId = req.params.noteId;
const taskId = req.query.taskId;
const eraseNotes = req.query.eraseNotes === 'true';
@ -60,8 +71,11 @@ function deleteNote(req) {
// note how deleteId is separate from taskId - single taskId produces separate deleteId for each "top level" deleted note
const deleteId = utils.randomString(10);
const note = becca.getNote(noteId);
const note = becca.getNoteOrThrow(noteId);
if (typeof taskId !== "string") {
throw new ValidationError("Missing or incorrect type for task ID.");
}
const taskContext = TaskContext.getInstance(taskId, 'deleteNotes');
note.deleteNote(deleteId, taskContext);
@ -75,7 +89,7 @@ function deleteNote(req) {
}
}
function undeleteNote(req) {
function undeleteNote(req: Request) {
const taskContext = TaskContext.getInstance(utils.randomString(10), 'undeleteNotes');
noteService.undeleteNote(req.params.noteId, taskContext);
@ -83,7 +97,7 @@ function undeleteNote(req) {
taskContext.taskSucceeded();
}
function sortChildNotes(req) {
function sortChildNotes(req: Request) {
const noteId = req.params.noteId;
const {sortBy, sortDirection, foldersFirst, sortNatural, sortLocale} = req.body;
@ -94,11 +108,11 @@ function sortChildNotes(req) {
treeService.sortNotes(noteId, sortBy, reverse, foldersFirst, sortNatural, sortLocale);
}
function protectNote(req) {
function protectNote(req: Request) {
const noteId = req.params.noteId;
const note = becca.notes[noteId];
const protect = !!parseInt(req.params.isProtected);
const includingSubTree = !!parseInt(req.query.subtree);
const includingSubTree = !!parseInt(req.query?.subtree as string);
const taskContext = new TaskContext(utils.randomString(10), 'protectNotes', {protect});
@ -107,18 +121,18 @@ function protectNote(req) {
taskContext.taskSucceeded();
}
function setNoteTypeMime(req) {
function setNoteTypeMime(req: Request) {
// can't use [] destructuring because req.params is not iterable
const {noteId} = req.params;
const {type, mime} = req.body;
const note = becca.getNote(noteId);
const note = becca.getNoteOrThrow(noteId);
note.type = type;
note.mime = mime;
note.save();
}
function changeTitle(req) {
function changeTitle(req: Request) {
const noteId = req.params.noteId;
const title = req.body.title;
@ -145,7 +159,7 @@ function changeTitle(req) {
return note;
}
function duplicateSubtree(req) {
function duplicateSubtree(req: Request) {
const {noteId, parentNoteId} = req.params;
return noteService.duplicateSubtree(noteId, parentNoteId);
@ -159,14 +173,14 @@ function eraseUnusedAttachmentsNow() {
eraseService.eraseUnusedAttachmentsNow();
}
function getDeleteNotesPreview(req) {
function getDeleteNotesPreview(req: Request) {
const {branchIdsToDelete, deleteAllClones} = req.body;
const noteIdsToBeDeleted = new Set();
const strongBranchCountToDelete = {}; // noteId => count (integer)
const noteIdsToBeDeleted = new Set<string>();
const strongBranchCountToDelete: Record<string, number> = {}; // noteId => count
function branchPreviewDeletion(branch) {
if (branch.isWeak) {
function branchPreviewDeletion(branch: BBranch) {
if (branch.isWeak || !branch.branchId) {
return;
}
@ -196,18 +210,18 @@ function getDeleteNotesPreview(req) {
branchPreviewDeletion(branch);
}
let brokenRelations = [];
let brokenRelations: AttributeRow[] = [];
if (noteIdsToBeDeleted.size > 0) {
sql.fillParamList(noteIdsToBeDeleted);
// FIXME: No need to do this in database, can be done with becca data
brokenRelations = sql.getRows(`
brokenRelations = sql.getRows<AttributeRow>(`
SELECT attr.noteId, attr.name, attr.value
FROM attributes attr
JOIN param_list ON param_list.paramId = attr.value
WHERE attr.isDeleted = 0
AND attr.type = 'relation'`).filter(attr => !noteIdsToBeDeleted.has(attr.noteId));
AND attr.type = 'relation'`).filter(attr => attr.noteId && !noteIdsToBeDeleted.has(attr.noteId));
}
return {
@ -216,7 +230,7 @@ function getDeleteNotesPreview(req) {
};
}
function forceSaveRevision(req) {
function forceSaveRevision(req: Request) {
const {noteId} = req.params;
const note = becca.getNoteOrThrow(noteId);
@ -227,7 +241,7 @@ function forceSaveRevision(req) {
note.saveRevision();
}
function convertNoteToAttachment(req) {
function convertNoteToAttachment(req: Request) {
const {noteId} = req.params;
const note = becca.getNoteOrThrow(noteId);
@ -236,7 +250,7 @@ function convertNoteToAttachment(req) {
};
}
module.exports = {
export = {
getNote,
getNoteBlob,
getNoteMetadata,

View File

@ -1,9 +1,10 @@
"use strict";
const optionService = require('../../services/options');
const log = require('../../services/log');
const searchService = require('../../services/search/services/search');
const ValidationError = require('../../errors/validation_error');
import optionService = require('../../services/options');
import log = require('../../services/log');
import searchService = require('../../services/search/services/search');
import ValidationError = require('../../errors/validation_error');
import { Request } from 'express';
// options allowed to be updated directly in the Options dialog
const ALLOWED_OPTIONS = new Set([
@ -62,7 +63,7 @@ const ALLOWED_OPTIONS = new Set([
function getOptions() {
const optionMap = optionService.getOptionMap();
const resultMap = {};
const resultMap: Record<string, string> = {};
for (const optionName in optionMap) {
if (isAllowed(optionName)) {
@ -75,7 +76,7 @@ function getOptions() {
return resultMap;
}
function updateOption(req) {
function updateOption(req: Request) {
const {name, value} = req.params;
if (!update(name, value)) {
@ -83,7 +84,7 @@ function updateOption(req) {
}
}
function updateOptions(req) {
function updateOptions(req: Request) {
for (const optionName in req.body) {
if (!update(optionName, req.body[optionName])) {
// this should be improved
@ -93,7 +94,7 @@ function updateOptions(req) {
}
}
function update(name, value) {
function update(name: string, value: string) {
if (!isAllowed(name)) {
return false;
}
@ -128,14 +129,14 @@ function getUserThemes() {
return ret;
}
function isAllowed(name) {
function isAllowed(name: string) {
return ALLOWED_OPTIONS.has(name)
|| name.startsWith("keyboardShortcuts")
|| name.endsWith("Collapsed")
|| name.startsWith("hideArchivedNotes");
}
module.exports = {
export = {
getOptions,
updateOption,
updateOptions,

View File

@ -1,8 +1,10 @@
const becca = require('../../becca/becca');
const markdownService = require('../../services/import/markdown');
import { Request } from "express";
import becca = require('../../becca/becca');
import markdownService = require('../../services/import/markdown');
function getIconUsage() {
const iconClassToCountMap = {};
const iconClassToCountMap: Record<string, number> = {};
for (const {value: iconClass, noteId} of becca.findAttributes('label', 'iconClass')) {
if (noteId.startsWith("_")) {
@ -25,7 +27,7 @@ function getIconUsage() {
return { iconClassToCountMap };
}
function renderMarkdown(req) {
function renderMarkdown(req: Request) {
const { markdownContent } = req.body;
return {
@ -33,7 +35,7 @@ function renderMarkdown(req) {
};
}
module.exports = {
export = {
getIconUsage,
renderMarkdown
};

View File

@ -1,9 +1,10 @@
"use strict";
const passwordService = require('../../services/encryption/password');
const ValidationError = require('../../errors/validation_error');
import passwordService = require('../../services/encryption/password');
import ValidationError = require('../../errors/validation_error');
import { Request } from 'express';
function changePassword(req) {
function changePassword(req: Request) {
if (passwordService.isPasswordSet()) {
return passwordService.changePassword(req.body.current_password, req.body.new_password);
}
@ -12,7 +13,7 @@ function changePassword(req) {
}
}
function resetPassword(req) {
function resetPassword(req: Request) {
// protection against accidental call (not a security measure)
if (req.query.really !== "yesIReallyWantToResetPasswordAndLoseAccessToMyProtectedNotes") {
throw new ValidationError("Incorrect password reset confirmation");
@ -21,7 +22,7 @@ function resetPassword(req) {
return passwordService.resetPassword();
}
module.exports = {
export = {
changePassword,
resetPassword
};

View File

@ -1,16 +1,30 @@
"use strict";
const sql = require('../../services/sql');
const protectedSessionService = require('../../services/protected_session');
const noteService = require('../../services/notes');
const becca = require('../../becca/becca');
import sql = require('../../services/sql');
import protectedSessionService = require('../../services/protected_session');
import noteService = require('../../services/notes');
import becca = require('../../becca/becca');
import { Request } from 'express';
import { RevisionRow } from '../../becca/entities/rows';
function getRecentChanges(req) {
interface RecentChangeRow {
noteId: string;
current_isDeleted: boolean;
current_deleteId: string;
current_title: string;
current_isProtected: boolean,
title: string;
utcDate: string;
date: string;
canBeUndeleted?: boolean;
}
function getRecentChanges(req: Request) {
const {ancestorNoteId} = req.params;
let recentChanges = [];
const revisionRows = sql.getRows(`
const revisionRows = sql.getRows<RecentChangeRow>(`
SELECT
notes.noteId,
notes.isDeleted AS current_isDeleted,
@ -36,7 +50,7 @@ function getRecentChanges(req) {
// now we need to also collect date points not represented in note revisions:
// 1. creation for all notes (dateCreated)
// 2. deletion for deleted notes (dateModified)
const noteRows = sql.getRows(`
const noteRows = sql.getRows<RecentChangeRow>(`
SELECT
notes.noteId,
notes.isDeleted AS current_isDeleted,
@ -76,8 +90,8 @@ function getRecentChanges(req) {
for (const change of recentChanges) {
if (change.current_isProtected) {
if (protectedSessionService.isProtectedSessionAvailable()) {
change.title = protectedSessionService.decryptString(change.title);
change.current_title = protectedSessionService.decryptString(change.current_title);
change.title = protectedSessionService.decryptString(change.title) || "[protected]";
change.current_title = protectedSessionService.decryptString(change.current_title) || "[protected]";
}
else {
change.title = change.current_title = "[protected]";
@ -97,6 +111,6 @@ function getRecentChanges(req) {
return recentChanges;
}
module.exports = {
export = {
getRecentChanges
};

View File

@ -1,10 +1,11 @@
"use strict";
const BRecentNote = require('../../becca/entities/brecent_note');
const sql = require('../../services/sql');
const dateUtils = require('../../services/date_utils');
import BRecentNote = require('../../becca/entities/brecent_note');
import sql = require('../../services/sql');
import dateUtils = require('../../services/date_utils');
import { Request } from 'express';
function addRecentNote(req) {
function addRecentNote(req: Request) {
new BRecentNote({
noteId: req.body.noteId,
notePath: req.body.notePath
@ -18,6 +19,6 @@ function addRecentNote(req) {
}
}
module.exports = {
export = {
addRecentNote
};

View File

@ -1,10 +1,22 @@
const becca = require('../../becca/becca');
const sql = require('../../services/sql');
import { Request } from 'express';
import becca = require('../../becca/becca');
import sql = require('../../services/sql');
function getRelationMap(req) {
interface ResponseData {
noteTitles: Record<string, string>;
relations: {
attributeId: string,
sourceNoteId: string,
targetNoteId: string,
name: string
}[];
inverseRelations: Record<string, string>;
}
function getRelationMap(req: Request) {
const {relationMapNoteId, noteIds} = req.body;
const resp = {
const resp: ResponseData = {
// noteId => title
noteTitles: {},
relations: [],
@ -14,13 +26,13 @@ function getRelationMap(req) {
}
};
if (noteIds.length === 0) {
if (!Array.isArray(noteIds) || noteIds.length === 0) {
return resp;
}
const questionMarks = noteIds.map(noteId => '?').join(',');
const relationMapNote = becca.getNote(relationMapNoteId);
const relationMapNote = becca.getNoteOrThrow(relationMapNoteId);
const displayRelationsVal = relationMapNote.getLabelValue('displayRelations');
const displayRelations = !displayRelationsVal ? [] : displayRelationsVal
@ -32,7 +44,7 @@ function getRelationMap(req) {
.split(",")
.map(token => token.trim());
const foundNoteIds = sql.getColumn(`SELECT noteId FROM notes WHERE isDeleted = 0 AND noteId IN (${questionMarks})`, noteIds);
const foundNoteIds = sql.getColumn<string>(`SELECT noteId FROM notes WHERE isDeleted = 0 AND noteId IN (${questionMarks})`, noteIds);
const notes = becca.getNotes(foundNoteIds);
for (const note of notes) {
@ -64,6 +76,6 @@ function getRelationMap(req) {
return resp;
}
module.exports = {
export = {
getRelationMap
};

View File

@ -1,22 +1,38 @@
"use strict";
const beccaService = require('../../becca/becca_service');
const revisionService = require('../../services/revisions');
const utils = require('../../services/utils');
const sql = require('../../services/sql');
const cls = require('../../services/cls');
const path = require('path');
const becca = require('../../becca/becca');
const blobService = require('../../services/blob');
const eraseService = require("../../services/erase");
import beccaService = require('../../becca/becca_service');
import revisionService = require('../../services/revisions');
import utils = require('../../services/utils');
import sql = require('../../services/sql');
import cls = require('../../services/cls');
import path = require('path');
import becca = require('../../becca/becca');
import blobService = require('../../services/blob');
import eraseService = require("../../services/erase");
import { Request, Response } from 'express';
import BRevision = require('../../becca/entities/brevision');
import BNote = require('../../becca/entities/bnote');
import { NotePojo } from '../../becca/becca-interface';
function getRevisionBlob(req) {
interface NotePath {
noteId: string;
branchId?: string;
title: string;
notePath: string[];
path: string;
}
interface NotePojoWithNotePath extends NotePojo {
notePath?: string[] | null;
}
function getRevisionBlob(req: Request) {
const preview = req.query.preview === 'true';
return blobService.getBlobPojo('revisions', req.params.revisionId, { preview });
}
function getRevisions(req) {
function getRevisions(req: Request) {
return becca.getRevisionsFromQuery(`
SELECT revisions.*,
LENGTH(blobs.content) AS contentLength
@ -26,12 +42,12 @@ function getRevisions(req) {
ORDER BY revisions.utcDateCreated DESC`, [req.params.noteId]);
}
function getRevision(req) {
const revision = becca.getRevision(req.params.revisionId);
function getRevision(req: Request) {
const revision = becca.getRevisionOrThrow(req.params.revisionId);
if (revision.type === 'file') {
if (revision.hasStringContent()) {
revision.content = revision.getContent().substr(0, 10000);
revision.content = (revision.getContent() as string).substr(0, 10000);
}
}
else {
@ -45,11 +61,7 @@ function getRevision(req) {
return revision;
}
/**
* @param {BRevision} revision
* @returns {string}
*/
function getRevisionFilename(revision) {
function getRevisionFilename(revision: BRevision) {
let filename = utils.formatDownloadTitle(revision.title, revision.type, revision.mime);
const extension = path.extname(filename);
@ -68,8 +80,8 @@ function getRevisionFilename(revision) {
return filename;
}
function downloadRevision(req, res) {
const revision = becca.getRevision(req.params.revisionId);
function downloadRevision(req: Request, res: Response) {
const revision = becca.getRevisionOrThrow(req.params.revisionId);
if (!revision.isContentAvailable()) {
return res.setHeader("Content-Type", "text/plain")
@ -85,18 +97,18 @@ function downloadRevision(req, res) {
res.send(revision.getContent());
}
function eraseAllRevisions(req) {
const revisionIdsToErase = sql.getColumn('SELECT revisionId FROM revisions WHERE noteId = ?',
function eraseAllRevisions(req: Request) {
const revisionIdsToErase = sql.getColumn<string>('SELECT revisionId FROM revisions WHERE noteId = ?',
[req.params.noteId]);
eraseService.eraseRevisions(revisionIdsToErase);
}
function eraseRevision(req) {
function eraseRevision(req: Request) {
eraseService.eraseRevisions([req.params.revisionId]);
}
function restoreRevision(req) {
function restoreRevision(req: Request) {
const revision = becca.getRevision(req.params.revisionId);
if (revision) {
@ -117,7 +129,9 @@ function restoreRevision(req) {
noteAttachment.setContent(revisionAttachment.getContent(), { forceSave: true });
// content is rewritten to point to the restored revision attachments
revisionContent = revisionContent.replaceAll(`attachments/${revisionAttachment.attachmentId}`, `attachments/${noteAttachment.attachmentId}`);
if (typeof revisionContent === "string") {
revisionContent = revisionContent.replaceAll(`attachments/${revisionAttachment.attachmentId}`, `attachments/${noteAttachment.attachmentId}`);
}
}
note.title = revision.title;
@ -126,8 +140,8 @@ function restoreRevision(req) {
}
}
function getEditedNotesOnDate(req) {
const noteIds = sql.getColumn(`
function getEditedNotesOnDate(req: Request) {
const noteIds = sql.getColumn<string>(`
SELECT notes.*
FROM notes
WHERE noteId IN (
@ -152,7 +166,7 @@ function getEditedNotesOnDate(req) {
return notes.map(note => {
const notePath = getNotePathData(note);
const notePojo = note.getPojo();
const notePojo: NotePojoWithNotePath = note.getPojo();
notePojo.notePath = notePath ? notePath.notePath : null;
return notePojo;
@ -160,7 +174,7 @@ function getEditedNotesOnDate(req) {
}
function getNotePathData(note) {
function getNotePathData(note: BNote): NotePath | undefined {
const retPath = note.getBestNotePath();
if (retPath) {
@ -173,7 +187,7 @@ function getNotePathData(note) {
}
else {
const parentNote = note.parents[0];
branchId = becca.getBranchFromChildAndParent(note.noteId, parentNote.noteId).branchId;
branchId = becca.getBranchFromChildAndParent(note.noteId, parentNote.noteId)?.branchId;
}
return {
@ -186,7 +200,7 @@ function getNotePathData(note) {
}
}
module.exports = {
export = {
getRevisionBlob,
getRevisions,
getRevision,

View File

@ -1,19 +1,30 @@
"use strict";
const scriptService = require('../../services/script');
const attributeService = require('../../services/attributes');
const becca = require('../../becca/becca');
const syncService = require('../../services/sync');
const sql = require('../../services/sql');
import scriptService = require('../../services/script');
import attributeService = require('../../services/attributes');
import becca = require('../../becca/becca');
import syncService = require('../../services/sync');
import sql = require('../../services/sql');
import { Request } from 'express';
interface ScriptBody {
script: string;
params: any[];
startNoteId: string;
currentNoteId: string;
originEntityName: string;
originEntityId: string;
transactional: boolean;
}
// The async/await here is very confusing, because the body.script may, but may not be async. If it is async, then we
// need to await it and make the complete response including metadata available in a Promise, so that the route detects
// this and does result.then().
async function exec(req) {
async function exec(req: Request) {
try {
const { body } = req;
const body = (req.body as ScriptBody);
const execute = body => scriptService.executeScript(
const execute = (body: ScriptBody) => scriptService.executeScript(
body.script,
body.params,
body.startNoteId,
@ -32,20 +43,20 @@ async function exec(req) {
maxEntityChangeId: syncService.getMaxEntityChangeId()
};
}
catch (e) {
catch (e: any) {
return { success: false, error: e.message };
}
}
function run(req) {
const note = becca.getNote(req.params.noteId);
function run(req: Request) {
const note = becca.getNoteOrThrow(req.params.noteId);
const result = scriptService.executeNote(note, { originEntity: note });
return { executionResult: result };
}
function getBundlesWithLabel(label, value) {
function getBundlesWithLabel(label: string, value?: string) {
const notes = attributeService.getNotesWithLabel(label, value);
const bundles = [];
@ -61,7 +72,7 @@ function getBundlesWithLabel(label, value) {
return bundles;
}
function getStartupBundles(req) {
function getStartupBundles(req: Request) {
if (!process.env.TRILIUM_SAFE_MODE) {
if (req.query.mobile === "true") {
return getBundlesWithLabel("run", "mobileStartup");
@ -84,9 +95,9 @@ function getWidgetBundles() {
}
}
function getRelationBundles(req) {
function getRelationBundles(req: Request) {
const noteId = req.params.noteId;
const note = becca.getNote(noteId);
const note = becca.getNoteOrThrow(noteId);
const relationName = req.params.relationName;
const attributes = note.getAttributes();
@ -97,7 +108,7 @@ function getRelationBundles(req) {
const bundles = [];
for (const noteId of uniqueNoteIds) {
const note = becca.getNote(noteId);
const note = becca.getNoteOrThrow(noteId);
if (!note.isJavaScript() || note.getScriptEnv() !== 'frontend') {
continue;
@ -113,14 +124,14 @@ function getRelationBundles(req) {
return bundles;
}
function getBundle(req) {
const note = becca.getNote(req.params.noteId);
function getBundle(req: Request) {
const note = becca.getNoteOrThrow(req.params.noteId);
const { script, params } = req.body;
return scriptService.getScriptBundleForFrontend(note, script, params);
}
module.exports = {
export = {
exec,
run,
getStartupBundles,

View File

@ -1,14 +1,17 @@
"use strict";
const becca = require('../../becca/becca');
const SearchContext = require('../../services/search/search_context');
const searchService = require('../../services/search/services/search');
const bulkActionService = require('../../services/bulk_actions');
const cls = require('../../services/cls');
const {formatAttrForSearch} = require('../../services/attribute_formatter');
const ValidationError = require('../../errors/validation_error');
import { Request } from "express";
function searchFromNote(req) {
import becca = require('../../becca/becca');
import SearchContext = require('../../services/search/search_context');
import searchService = require('../../services/search/services/search');
import bulkActionService = require('../../services/bulk_actions');
import cls = require('../../services/cls');
import attributeFormatter = require('../../services/attribute_formatter');
import ValidationError = require('../../errors/validation_error');
import SearchResult = require("../../services/search/search_result");
function searchFromNote(req: Request) {
const note = becca.getNoteOrThrow(req.params.noteId);
if (!note) {
@ -23,7 +26,7 @@ function searchFromNote(req) {
return searchService.searchFromNote(note);
}
function searchAndExecute(req) {
function searchAndExecute(req: Request) {
const note = becca.getNoteOrThrow(req.params.noteId);
if (!note) {
@ -40,7 +43,7 @@ function searchAndExecute(req) {
bulkActionService.executeActions(note, searchResultNoteIds);
}
function quickSearch(req) {
function quickSearch(req: Request) {
const {searchString} = req.params;
const searchContext = new SearchContext({
@ -58,7 +61,7 @@ function quickSearch(req) {
};
}
function search(req) {
function search(req: Request) {
const {searchString} = req.params;
const searchContext = new SearchContext({
@ -72,7 +75,7 @@ function search(req) {
.map(sr => sr.noteId);
}
function getRelatedNotes(req) {
function getRelatedNotes(req: Request) {
const attr = req.body;
const searchSettings = {
@ -81,10 +84,10 @@ function getRelatedNotes(req) {
fuzzyAttributeSearch: false
};
const matchingNameAndValue = searchService.findResultsWithQuery(formatAttrForSearch(attr, true), new SearchContext(searchSettings));
const matchingName = searchService.findResultsWithQuery(formatAttrForSearch(attr, false), new SearchContext(searchSettings));
const matchingNameAndValue = searchService.findResultsWithQuery(attributeFormatter.formatAttrForSearch(attr, true), new SearchContext(searchSettings));
const matchingName = searchService.findResultsWithQuery(attributeFormatter.formatAttrForSearch(attr, false), new SearchContext(searchSettings));
const results = [];
const results: SearchResult[] = [];
const allResults = matchingNameAndValue.concat(matchingName);
@ -123,7 +126,7 @@ function searchTemplates() {
}).map(note => note.noteId);
}
module.exports = {
export = {
searchFromNote,
searchAndExecute,
getRelatedNotes,

View File

@ -1,66 +0,0 @@
"use strict";
const imageType = require('image-type');
const imageService = require('../../services/image');
const noteService = require('../../services/notes');
const { sanitizeAttributeName } = require('../../services/sanitize_attribute_name');
const specialNotesService = require('../../services/special_notes');
function uploadImage(req) {
const file = req.file;
if (!["image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"].includes(file.mimetype)) {
return [400, `Unknown image type: ${file.mimetype}`];
}
const originalName = `Sender image.${imageType(file.buffer).ext}`;
const parentNote = specialNotesService.getInboxNote(req.headers['x-local-date']);
const { note, noteId } = imageService.saveImage(parentNote.noteId, file.buffer, originalName, true);
const labelsStr = req.headers['x-labels'];
if (labelsStr?.trim()) {
const labels = JSON.parse(labelsStr);
for (const { name, value } of labels) {
note.setLabel(sanitizeAttributeName(name), value);
}
}
note.setLabel("sentFromSender");
return {
noteId: noteId
};
}
function saveNote(req) {
const parentNote = specialNotesService.getInboxNote(req.headers['x-local-date']);
const { note, branch } = noteService.createNewNote({
parentNoteId: parentNote.noteId,
title: req.body.title,
content: req.body.content,
isProtected: false,
type: 'text',
mime: 'text/html'
});
if (req.body.labels) {
for (const { name, value } of req.body.labels) {
note.setLabel(sanitizeAttributeName(name), value);
}
}
return {
noteId: note.noteId,
branchId: branch.branchId
};
}
module.exports = {
uploadImage,
saveNote
};

83
src/routes/api/sender.ts Normal file
View File

@ -0,0 +1,83 @@
"use strict";
import imageType = require('image-type');
import imageService = require('../../services/image');
import noteService = require('../../services/notes');
import sanitize_attribute_name = require('../../services/sanitize_attribute_name');
import specialNotesService = require('../../services/special_notes');
import { Request } from 'express';
function uploadImage(req: Request) {
const file = (req as any).file;
if (!["image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"].includes(file.mimetype)) {
return [400, `Unknown image type: ${file.mimetype}`];
}
const uploadedImageType = imageType(file.buffer);
if (!uploadedImageType) {
return [400, "Unable to determine image type."];
}
const originalName = `Sender image.${uploadedImageType.ext}`;
if (!req.headers["x-local-date"] || Array.isArray(req.headers["x-local-date"])) {
return [400, "Invalid local date"];
}
if (Array.isArray(req.headers["x-labels"])) {
return [400, "Invalid value type."];
}
const parentNote = specialNotesService.getInboxNote(req.headers['x-local-date']);
const { note, noteId } = imageService.saveImage(parentNote.noteId, file.buffer, originalName, true);
const labelsStr = req.headers['x-labels'];
if (labelsStr?.trim()) {
const labels = JSON.parse(labelsStr);
for (const { name, value } of labels) {
note.setLabel(sanitize_attribute_name.sanitizeAttributeName(name), value);
}
}
note.setLabel("sentFromSender");
return {
noteId: noteId
};
}
function saveNote(req: Request) {
if (!req.headers["x-local-date"] || Array.isArray(req.headers["x-local-date"])) {
return [400, "Invalid local date"];
}
const parentNote = specialNotesService.getInboxNote(req.headers['x-local-date']);
const { note, branch } = noteService.createNewNote({
parentNoteId: parentNote.noteId,
title: req.body.title,
content: req.body.content,
isProtected: false,
type: 'text',
mime: 'text/html'
});
if (req.body.labels) {
for (const { name, value } of req.body.labels) {
note.setLabel(sanitize_attribute_name.sanitizeAttributeName(name), value);
}
}
return {
noteId: note.noteId,
branchId: branch.branchId
};
}
export = {
uploadImage,
saveNote
};

View File

@ -1,9 +1,10 @@
"use strict";
const sqlInit = require('../../services/sql_init');
const setupService = require('../../services/setup');
const log = require('../../services/log');
const appInfo = require('../../services/app_info');
import sqlInit = require('../../services/sql_init');
import setupService = require('../../services/setup');
import log = require('../../services/log');
import appInfo = require('../../services/app_info');
import { Request } from 'express';
function getStatus() {
return {
@ -17,13 +18,13 @@ async function setupNewDocument() {
await sqlInit.createInitialDatabase();
}
function setupSyncFromServer(req) {
function setupSyncFromServer(req: Request) {
const { syncServerHost, syncProxy, password } = req.body;
return setupService.setupSyncFromSyncServer(syncServerHost, syncProxy, password);
}
function saveSyncSeed(req) {
function saveSyncSeed(req: Request) {
const { options, syncVersion } = req.body;
if (appInfo.syncVersion !== syncVersion) {
@ -50,7 +51,7 @@ function getSyncSeed() {
};
}
module.exports = {
export = {
getStatus,
setupNewDocument,
setupSyncFromServer,

View File

@ -1,16 +0,0 @@
"use strict";
const similarityService = require('../../becca/similarity');
const becca = require('../../becca/becca');
async function getSimilarNotes(req) {
const noteId = req.params.noteId;
const note = becca.getNoteOrThrow(noteId);
return await similarityService.findSimilarNotes(noteId);
}
module.exports = {
getSimilarNotes
};

View File

@ -0,0 +1,18 @@
"use strict";
import { Request } from "express";
import similarityService = require('../../becca/similarity');
import becca = require('../../becca/becca');
async function getSimilarNotes(req: Request) {
const noteId = req.params.noteId;
const note = becca.getNoteOrThrow(noteId);
return await similarityService.findSimilarNotes(noteId);
}
export = {
getSimilarNotes
};

View File

@ -1,32 +1,33 @@
"use strict";
const dateNoteService = require('../../services/date_notes');
const sql = require('../../services/sql');
const cls = require('../../services/cls');
const specialNotesService = require('../../services/special_notes');
const becca = require('../../becca/becca');
import dateNoteService = require('../../services/date_notes');
import sql = require('../../services/sql');
import cls = require('../../services/cls');
import specialNotesService = require('../../services/special_notes');
import becca = require('../../becca/becca');
import { Request } from 'express';
function getInboxNote(req) {
function getInboxNote(req: Request) {
return specialNotesService.getInboxNote(req.params.date);
}
function getDayNote(req) {
function getDayNote(req: Request) {
return dateNoteService.getDayNote(req.params.date);
}
function getWeekNote(req) {
function getWeekNote(req: Request) {
return dateNoteService.getWeekNote(req.params.date);
}
function getMonthNote(req) {
function getMonthNote(req: Request) {
return dateNoteService.getMonthNote(req.params.month);
}
function getYearNote(req) {
function getYearNote(req: Request) {
return dateNoteService.getYearNote(req.params.year);
}
function getDayNotesForMonth(req) {
function getDayNotesForMonth(req: Request) {
const month = req.params.month;
return sql.getMap(`
@ -42,7 +43,7 @@ function getDayNotesForMonth(req) {
AND attr.value LIKE '${month}%'`);
}
function saveSqlConsole(req) {
function saveSqlConsole(req: Request) {
return specialNotesService.saveSqlConsole(req.body.sqlConsoleNoteId);
}
@ -50,14 +51,14 @@ function createSqlConsole() {
return specialNotesService.createSqlConsole();
}
function saveSearchNote(req) {
function saveSearchNote(req: Request) {
return specialNotesService.saveSearchNote(req.body.searchNoteId);
}
function createSearchNote(req) {
function createSearchNote(req: Request) {
const hoistedNote = getHoistedNote();
const searchString = req.body.searchString || "";
const ancestorNoteId = req.body.ancestorNoteId || hoistedNote.noteId;
const ancestorNoteId = req.body.ancestorNoteId || hoistedNote?.noteId;
return specialNotesService.createSearchNote(searchString, ancestorNoteId);
}
@ -66,22 +67,22 @@ function getHoistedNote() {
return becca.getNote(cls.getHoistedNoteId());
}
function createLauncher(req) {
function createLauncher(req: Request) {
return specialNotesService.createLauncher({
parentNoteId: req.params.parentNoteId,
launcherType: req.params.launcherType
});
}
function resetLauncher(req) {
function resetLauncher(req: Request) {
return specialNotesService.resetLauncher(req.params.noteId);
}
function createOrUpdateScriptLauncherFromApi(req) {
function createOrUpdateScriptLauncherFromApi(req: Request) {
return specialNotesService.createOrUpdateScriptLauncherFromApi(req.body);
}
module.exports = {
export = {
getInboxNote,
getDayNote,
getWeekNote,

View File

@ -1,7 +1,9 @@
"use strict";
const sql = require('../../services/sql');
const becca = require('../../becca/becca');
import sql = require('../../services/sql');
import becca = require('../../becca/becca');
import { Request } from 'express';
import ValidationError = require('../../errors/validation_error');
function getSchema() {
const tableNames = sql.getColumn(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name`);
@ -17,10 +19,15 @@ function getSchema() {
return tables;
}
function execute(req) {
function execute(req: Request) {
const note = becca.getNoteOrThrow(req.params.noteId);
const queries = note.getContent().split("\n---");
const content = note.getContent();
if (typeof content !== "string") {
throw new ValidationError("Invalid note type.");
}
const queries = content.split("\n---");
try {
const results = [];
@ -51,7 +58,7 @@ function execute(req) {
results
};
}
catch (e) {
catch (e: any) {
return {
success: false,
error: e.message
@ -59,7 +66,7 @@ function execute(req) {
}
}
module.exports = {
export = {
getSchema,
execute
};

View File

@ -1,10 +1,11 @@
const sql = require('../../services/sql');
const becca = require('../../becca/becca');
import sql = require('../../services/sql');
import becca = require('../../becca/becca');
import { Request } from 'express';
function getNoteSize(req) {
function getNoteSize(req: Request) {
const {noteId} = req.params;
const blobSizes = sql.getMap(`
const blobSizes = sql.getMap<string, number>(`
SELECT blobs.blobId, LENGTH(content)
FROM blobs
LEFT JOIN notes ON notes.blobId = blobs.blobId AND notes.noteId = ? AND notes.isDeleted = 0
@ -21,14 +22,14 @@ function getNoteSize(req) {
};
}
function getSubtreeSize(req) {
function getSubtreeSize(req: Request) {
const note = becca.getNoteOrThrow(req.params.noteId);
const subTreeNoteIds = note.getSubtreeNoteIds();
sql.fillParamList(subTreeNoteIds);
const blobSizes = sql.getMap(`
const blobSizes = sql.getMap<string, number>(`
SELECT blobs.blobId, LENGTH(content)
FROM param_list
JOIN notes ON notes.noteId = param_list.paramId AND notes.isDeleted = 0
@ -44,7 +45,7 @@ function getSubtreeSize(req) {
};
}
module.exports = {
export = {
getNoteSize,
getSubtreeSize
};

View File

@ -1,16 +1,19 @@
"use strict";
const syncService = require('../../services/sync');
const syncUpdateService = require('../../services/sync_update');
const entityChangesService = require('../../services/entity_changes');
const sql = require('../../services/sql');
const sqlInit = require('../../services/sql_init');
const optionService = require('../../services/options');
const contentHashService = require('../../services/content_hash');
const log = require('../../services/log');
const syncOptions = require('../../services/sync_options');
const utils = require('../../services/utils');
const ws = require('../../services/ws');
import syncService = require('../../services/sync');
import syncUpdateService = require('../../services/sync_update');
import entityChangesService = require('../../services/entity_changes');
import sql = require('../../services/sql');
import sqlInit = require('../../services/sql_init');
import optionService = require('../../services/options');
import contentHashService = require('../../services/content_hash');
import log = require('../../services/log');
import syncOptions = require('../../services/sync_options');
import utils = require('../../services/utils');
import ws = require('../../services/ws');
import { Request } from 'express';
import { EntityChange, EntityChangeRecord } from '../../services/entity_changes_interface';
import ValidationError = require('../../errors/validation_error');
async function testSync() {
try {
@ -26,7 +29,7 @@ async function testSync() {
return { success: true, message: "Sync server handshake has been successful, sync has been started." };
}
catch (e) {
catch (e: any) {
return {
success: false,
message: e.message
@ -82,15 +85,19 @@ function forceFullSync() {
syncService.sync();
}
function getChanged(req) {
function getChanged(req: Request) {
const startTime = Date.now();
let lastEntityChangeId = parseInt(req.query.lastEntityChangeId);
if (typeof req.query.lastEntityChangeId !== "string") {
throw new ValidationError("Missing or invalid last entity change ID.");
}
let lastEntityChangeId: number | null | undefined = parseInt(req.query.lastEntityChangeId);
const clientInstanceId = req.query.instanceId;
let filteredEntityChanges = [];
let filteredEntityChanges: EntityChange[] = [];
do {
const entityChanges = sql.getRows(`
const entityChanges: EntityChange[] = sql.getRows<EntityChange>(`
SELECT *
FROM entity_changes
WHERE isSynced = 1
@ -129,16 +136,22 @@ function getChanged(req) {
};
}
const partialRequests = {};
const partialRequests: Record<string, {
createdAt: number,
payload: string
}> = {};
function update(req) {
function update(req: Request) {
let { body } = req;
const pageCount = parseInt(req.get('pageCount'));
const pageIndex = parseInt(req.get('pageIndex'));
const pageCount = parseInt(req.get('pageCount') as string);
const pageIndex = parseInt(req.get('pageIndex') as string);
if (pageCount !== 1) {
const requestId = req.get('requestId');
if (!requestId) {
throw new Error("Missing request ID.");
}
if (pageIndex === 0) {
partialRequests[requestId] = {
@ -185,7 +198,7 @@ function syncFinished() {
sqlInit.setDbAsInitialized();
}
function queueSector(req) {
function queueSector(req: Request) {
const entityName = utils.sanitizeSqlIdentifier(req.params.entityName);
const sector = utils.sanitizeSqlIdentifier(req.params.sector);
@ -196,7 +209,7 @@ function checkEntityChanges() {
require('../../services/consistency_checks').runEntityChangesChecks();
}
module.exports = {
export = {
testSync,
checkSync,
syncNow,

View File

@ -1,16 +1,18 @@
"use strict";
const becca = require('../../becca/becca');
const log = require('../../services/log');
const NotFoundError = require('../../errors/not_found_error');
import becca = require('../../becca/becca');
import log = require('../../services/log');
import NotFoundError = require('../../errors/not_found_error');
import { Request } from 'express';
import BNote = require('../../becca/entities/bnote');
function getNotesAndBranchesAndAttributes(noteIds) {
noteIds = new Set(noteIds);
const collectedNoteIds = new Set();
const collectedAttributeIds = new Set();
const collectedBranchIds = new Set();
function getNotesAndBranchesAndAttributes(_noteIds: string[] | Set<string>) {
const noteIds = new Set(_noteIds);
const collectedNoteIds = new Set<string>();
const collectedAttributeIds = new Set<string>();
const collectedBranchIds = new Set<string>();
function collectEntityIds(note) {
function collectEntityIds(note?: BNote) {
if (!note || collectedNoteIds.has(note.noteId)) {
return;
}
@ -18,15 +20,18 @@ function getNotesAndBranchesAndAttributes(noteIds) {
collectedNoteIds.add(note.noteId);
for (const branch of note.getParentBranches()) {
collectedBranchIds.add(branch.branchId);
if (branch.branchId) {
collectedBranchIds.add(branch.branchId);
}
collectEntityIds(branch.parentNote);
}
for (const childNote of note.children) {
const childBranch = becca.getBranchFromChildAndParent(childNote.noteId, note.noteId);
collectedBranchIds.add(childBranch.branchId);
if (childBranch && childBranch.branchId) {
collectedBranchIds.add(childBranch.branchId);
}
}
for (const attr of note.ownedAttributes) {
@ -122,11 +127,11 @@ function getNotesAndBranchesAndAttributes(noteIds) {
};
}
function getTree(req) {
const subTreeNoteId = req.query.subTreeNoteId || 'root';
const collectedNoteIds = new Set([subTreeNoteId]);
function getTree(req: Request) {
const subTreeNoteId = typeof req.query.subTreeNoteId === "string" ? req.query.subTreeNoteId : 'root';
const collectedNoteIds = new Set<string>([subTreeNoteId]);
function collect(parentNote) {
function collect(parentNote: BNote) {
if (!parentNote) {
console.trace(parentNote);
}
@ -136,7 +141,7 @@ function getTree(req) {
const childBranch = becca.getBranchFromChildAndParent(childNote.noteId, parentNote.noteId);
if (childBranch.isExpanded) {
if (childBranch?.isExpanded) {
collect(childBranch.childNote);
}
}
@ -151,11 +156,11 @@ function getTree(req) {
return getNotesAndBranchesAndAttributes(collectedNoteIds);
}
function load(req) {
function load(req: Request) {
return getNotesAndBranchesAndAttributes(req.body.noteIds);
}
module.exports = {
export = {
getTree,
load
};

View File

@ -17,48 +17,48 @@ const NotFoundError = require('../errors/not_found_error');
const ValidationError = require('../errors/validation_error');
// page routes
const setupRoute = require('./setup.js');
const setupRoute = require('./setup');
const loginRoute = require('./login.js');
const indexRoute = require('./index.js');
// API routes
const treeApiRoute = require('./api/tree.js');
const notesApiRoute = require('./api/notes.js');
const treeApiRoute = require('./api/tree');
const notesApiRoute = require('./api/notes');
const branchesApiRoute = require('./api/branches');
const attachmentsApiRoute = require('./api/attachments');
const autocompleteApiRoute = require('./api/autocomplete');
const cloningApiRoute = require('./api/cloning');
const revisionsApiRoute = require('./api/revisions');
const recentChangesApiRoute = require('./api/recent_changes.js');
const optionsApiRoute = require('./api/options.js');
const recentChangesApiRoute = require('./api/recent_changes');
const optionsApiRoute = require('./api/options');
const passwordApiRoute = require('./api/password');
const syncApiRoute = require('./api/sync');
const loginApiRoute = require('./api/login.js');
const recentNotesRoute = require('./api/recent_notes.js');
const loginApiRoute = require('./api/login');
const recentNotesRoute = require('./api/recent_notes');
const appInfoRoute = require('./api/app_info');
const exportRoute = require('./api/export');
const importRoute = require('./api/import.js');
const setupApiRoute = require('./api/setup.js');
const importRoute = require('./api/import');
const setupApiRoute = require('./api/setup');
const sqlRoute = require('./api/sql');
const databaseRoute = require('./api/database');
const imageRoute = require('./api/image');
const attributesRoute = require('./api/attributes');
const scriptRoute = require('./api/script.js');
const senderRoute = require('./api/sender.js');
const scriptRoute = require('./api/script');
const senderRoute = require('./api/sender');
const filesRoute = require('./api/files');
const searchRoute = require('./api/search');
const bulkActionRoute = require('./api/bulk_action');
const specialNotesRoute = require('./api/special_notes');
const noteMapRoute = require('./api/note_map.js');
const noteMapRoute = require('./api/note_map');
const clipperRoute = require('./api/clipper');
const similarNotesRoute = require('./api/similar_notes.js');
const keysRoute = require('./api/keys.js');
const similarNotesRoute = require('./api/similar_notes');
const keysRoute = require('./api/keys');
const backendLogRoute = require('./api/backend_log');
const statsRoute = require('./api/stats.js');
const statsRoute = require('./api/stats');
const fontsRoute = require('./api/fonts');
const etapiTokensApiRoutes = require('./api/etapi_tokens');
const relationMapApiRoute = require('./api/relation-map');
const otherRoute = require('./api/other.js');
const otherRoute = require('./api/other');
const shareRoutes = require('../share/routes.js');
const etapiAuthRoutes = require('../etapi/auth.js');

View File

@ -44,7 +44,7 @@ function subscribeBeccaLoader(eventTypes: EventType, listener: EventListener) {
}
}
function emit(eventType: string, data: any) {
function emit(eventType: string, data?: any) {
const listeners = eventListeners[eventType];
if (listeners) {

View File

@ -55,7 +55,7 @@ interface Note {
let note: Partial<Note> = {};
let resource: Resource;
function importEnex(taskContext: TaskContext, file: File, parentNote: BNote) {
function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Promise<BNote> {
const saxStream = sax.createStream(true);
const rootNoteTitle = file.originalname.toLowerCase().endsWith(".enex")

View File

@ -251,7 +251,7 @@ function createNewNote(params: NoteParams): {
});
}
function createNewNoteWithTarget(target: ("into" | "after"), targetBranchId: string, params: NoteParams) {
function createNewNoteWithTarget(target: ("into" | "after"), targetBranchId: string | undefined, params: NoteParams) {
if (!params.type) {
const parentNote = becca.notes[params.parentNoteId];
@ -263,7 +263,7 @@ function createNewNoteWithTarget(target: ("into" | "after"), targetBranchId: str
if (target === 'into') {
return createNewNote(params);
}
else if (target === 'after') {
else if (target === 'after' && targetBranchId) {
const afterBranch = becca.branches[targetBranchId];
// not updating utcDateModified to avoid having to sync whole rows

View File

@ -106,7 +106,7 @@ function execute(ctx: ScriptContext, script: string) {
return function () { return eval(`const apiContext = this;\r\n(${script}\r\n)()`); }.call(ctx);
}
function getParams(params: ScriptParams) {
function getParams(params?: ScriptParams) {
if (!params) {
return params;
}
@ -121,7 +121,7 @@ function getParams(params: ScriptParams) {
}).join(",");
}
function getScriptBundleForFrontend(note: BNote, script: string, params: ScriptParams) {
function getScriptBundleForFrontend(note: BNote, script?: string, params?: ScriptParams) {
let overrideContent = null;
if (script) {

View File

@ -110,7 +110,7 @@ function getSyncSeedOptions() {
];
}
module.exports = {
export = {
hasSyncServerSchemaAndSeed,
triggerSync,
sendSeedToSyncServer,

View File

@ -166,7 +166,7 @@ function createScriptLauncher(parentNoteId: string, forceNoteId?: string) {
interface LauncherConfig {
parentNoteId: string;
launcherType: string;
noteId: string;
noteId?: string;
}
function createLauncher({ parentNoteId, launcherType, noteId }: LauncherConfig) {

View File

@ -269,8 +269,8 @@ function transactional<T>(func: (statement: Statement) => T) {
}
}
function fillParamList(paramIds: string[], truncate = true) {
if (paramIds.length === 0) {
function fillParamList(paramIds: string[] | Set<string>, truncate = true) {
if ("length" in paramIds && paramIds.length === 0) {
return;
}

View File

@ -66,7 +66,7 @@ class TaskContext {
});
}
taskSucceeded(result?: string) {
taskSucceeded(result?: string | Record<string, string | undefined>) {
ws.sendMessageToAllClients({
type: 'taskSucceeded',
taskId: this.taskId,

View File

@ -41,7 +41,7 @@ interface Message {
taskType?: string | null;
message?: string;
reason?: string;
result?: string;
result?: string | Record<string, string | undefined>;
script?: string;
params?: any[];