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; 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 { getAttachment(attachmentId: string, opts: AttachmentOpts = {}): BAttachment | null {
opts.includeContentLength = !!opts.includeContentLength; opts.includeContentLength = !!opts.includeContentLength;
@ -246,7 +254,7 @@ export default class Becca {
return rows.map(row => new BRecentNote(row)); 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 rows = sql.getRows<RevisionRow>(query, params);
const BRevision = require('./entities/brevision'); // avoiding circular dependency problems const BRevision = require('./entities/brevision'); // avoiding circular dependency problems
@ -289,3 +297,17 @@ export interface ConstructorData<T extends AbstractBeccaEntity<T>> {
entityName: string; entityName: string;
hashedProperties: (keyof T)[]; 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>> { abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
utcDateModified?: string; utcDateModified?: string;
protected dateCreated?: string; dateCreated?: string;
protected dateModified?: string; dateModified?: string;
utcDateCreated!: string; utcDateCreated!: string;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,19 +1,20 @@
"use strict"; "use strict";
const options = require('../../services/options'); import options = require('../../services/options');
const utils = require('../../services/utils'); import utils = require('../../services/utils');
const dateUtils = require('../../services/date_utils'); import dateUtils = require('../../services/date_utils');
const instanceId = require('../../services/instance_id'); import instanceId = require('../../services/instance_id');
const passwordEncryptionService = require('../../services/encryption/password_encryption'); import passwordEncryptionService = require('../../services/encryption/password_encryption');
const protectedSessionService = require('../../services/protected_session'); import protectedSessionService = require('../../services/protected_session');
const appInfo = require('../../services/app_info'); import appInfo = require('../../services/app_info');
const eventService = require('../../services/events'); import eventService = require('../../services/events');
const sqlInit = require('../../services/sql_init'); import sqlInit = require('../../services/sql_init');
const sql = require('../../services/sql'); import sql = require('../../services/sql');
const ws = require('../../services/ws'); import ws = require('../../services/ws');
const etapiTokenService = require('../../services/etapi_tokens'); import etapiTokenService = require('../../services/etapi_tokens');
import { Request } from 'express';
function loginSync(req) { function loginSync(req: Request) {
if (!sqlInit.schemaExists()) { if (!sqlInit.schemaExists()) {
return [500, { message: "DB schema does not exist, can't sync." }]; 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." }]; 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 { return {
instanceId: instanceId, instanceId: instanceId,
@ -52,7 +53,7 @@ function loginSync(req) {
}; };
} }
function loginToProtectedSession(req) { function loginToProtectedSession(req: Request) {
const password = req.body.password; const password = req.body.password;
if (!passwordEncryptionService.verifyPassword(password)) { if (!passwordEncryptionService.verifyPassword(password)) {
@ -63,6 +64,12 @@ function loginToProtectedSession(req) {
} }
const decryptedDataKey = passwordEncryptionService.getDataKey(password); const decryptedDataKey = passwordEncryptionService.getDataKey(password);
if (!decryptedDataKey) {
return {
success: false,
message: "Unable to obtain data key."
}
}
protectedSessionService.setDataKey(decryptedDataKey); protectedSessionService.setDataKey(decryptedDataKey);
@ -87,7 +94,7 @@ function touchProtectedSession() {
protectedSessionService.touchProtectedSession(); protectedSessionService.touchProtectedSession();
} }
function token(req) { function token(req: Request) {
const password = req.body.password; const password = req.body.password;
if (!passwordEncryptionService.verifyPassword(password)) { if (!passwordEncryptionService.verifyPassword(password)) {
@ -102,7 +109,7 @@ function token(req) {
return { token: authToken }; return { token: authToken };
} }
module.exports = { export = {
loginSync, loginSync,
loginToProtectedSession, loginToProtectedSession,
logoutFromProtectedSession, logoutFromProtectedSession,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,19 +1,30 @@
"use strict"; "use strict";
const scriptService = require('../../services/script'); import scriptService = require('../../services/script');
const attributeService = require('../../services/attributes'); import attributeService = require('../../services/attributes');
const becca = require('../../becca/becca'); import becca = require('../../becca/becca');
const syncService = require('../../services/sync'); import syncService = require('../../services/sync');
const sql = require('../../services/sql'); 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 // 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 // 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(). // this and does result.then().
async function exec(req) { async function exec(req: Request) {
try { try {
const { body } = req; const body = (req.body as ScriptBody);
const execute = body => scriptService.executeScript( const execute = (body: ScriptBody) => scriptService.executeScript(
body.script, body.script,
body.params, body.params,
body.startNoteId, body.startNoteId,
@ -32,20 +43,20 @@ async function exec(req) {
maxEntityChangeId: syncService.getMaxEntityChangeId() maxEntityChangeId: syncService.getMaxEntityChangeId()
}; };
} }
catch (e) { catch (e: any) {
return { success: false, error: e.message }; return { success: false, error: e.message };
} }
} }
function run(req) { function run(req: Request) {
const note = becca.getNote(req.params.noteId); const note = becca.getNoteOrThrow(req.params.noteId);
const result = scriptService.executeNote(note, { originEntity: note }); const result = scriptService.executeNote(note, { originEntity: note });
return { executionResult: result }; return { executionResult: result };
} }
function getBundlesWithLabel(label, value) { function getBundlesWithLabel(label: string, value?: string) {
const notes = attributeService.getNotesWithLabel(label, value); const notes = attributeService.getNotesWithLabel(label, value);
const bundles = []; const bundles = [];
@ -61,7 +72,7 @@ function getBundlesWithLabel(label, value) {
return bundles; return bundles;
} }
function getStartupBundles(req) { function getStartupBundles(req: Request) {
if (!process.env.TRILIUM_SAFE_MODE) { if (!process.env.TRILIUM_SAFE_MODE) {
if (req.query.mobile === "true") { if (req.query.mobile === "true") {
return getBundlesWithLabel("run", "mobileStartup"); return getBundlesWithLabel("run", "mobileStartup");
@ -84,9 +95,9 @@ function getWidgetBundles() {
} }
} }
function getRelationBundles(req) { function getRelationBundles(req: Request) {
const noteId = req.params.noteId; const noteId = req.params.noteId;
const note = becca.getNote(noteId); const note = becca.getNoteOrThrow(noteId);
const relationName = req.params.relationName; const relationName = req.params.relationName;
const attributes = note.getAttributes(); const attributes = note.getAttributes();
@ -97,7 +108,7 @@ function getRelationBundles(req) {
const bundles = []; const bundles = [];
for (const noteId of uniqueNoteIds) { for (const noteId of uniqueNoteIds) {
const note = becca.getNote(noteId); const note = becca.getNoteOrThrow(noteId);
if (!note.isJavaScript() || note.getScriptEnv() !== 'frontend') { if (!note.isJavaScript() || note.getScriptEnv() !== 'frontend') {
continue; continue;
@ -113,14 +124,14 @@ function getRelationBundles(req) {
return bundles; return bundles;
} }
function getBundle(req) { function getBundle(req: Request) {
const note = becca.getNote(req.params.noteId); const note = becca.getNoteOrThrow(req.params.noteId);
const { script, params } = req.body; const { script, params } = req.body;
return scriptService.getScriptBundleForFrontend(note, script, params); return scriptService.getScriptBundleForFrontend(note, script, params);
} }
module.exports = { export = {
exec, exec,
run, run,
getStartupBundles, getStartupBundles,

View File

@ -1,14 +1,17 @@
"use strict"; "use strict";
const becca = require('../../becca/becca'); import { Request } from "express";
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');
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); const note = becca.getNoteOrThrow(req.params.noteId);
if (!note) { if (!note) {
@ -23,7 +26,7 @@ function searchFromNote(req) {
return searchService.searchFromNote(note); return searchService.searchFromNote(note);
} }
function searchAndExecute(req) { function searchAndExecute(req: Request) {
const note = becca.getNoteOrThrow(req.params.noteId); const note = becca.getNoteOrThrow(req.params.noteId);
if (!note) { if (!note) {
@ -40,7 +43,7 @@ function searchAndExecute(req) {
bulkActionService.executeActions(note, searchResultNoteIds); bulkActionService.executeActions(note, searchResultNoteIds);
} }
function quickSearch(req) { function quickSearch(req: Request) {
const {searchString} = req.params; const {searchString} = req.params;
const searchContext = new SearchContext({ const searchContext = new SearchContext({
@ -58,7 +61,7 @@ function quickSearch(req) {
}; };
} }
function search(req) { function search(req: Request) {
const {searchString} = req.params; const {searchString} = req.params;
const searchContext = new SearchContext({ const searchContext = new SearchContext({
@ -72,7 +75,7 @@ function search(req) {
.map(sr => sr.noteId); .map(sr => sr.noteId);
} }
function getRelatedNotes(req) { function getRelatedNotes(req: Request) {
const attr = req.body; const attr = req.body;
const searchSettings = { const searchSettings = {
@ -81,10 +84,10 @@ function getRelatedNotes(req) {
fuzzyAttributeSearch: false fuzzyAttributeSearch: false
}; };
const matchingNameAndValue = searchService.findResultsWithQuery(formatAttrForSearch(attr, true), new SearchContext(searchSettings)); const matchingNameAndValue = searchService.findResultsWithQuery(attributeFormatter.formatAttrForSearch(attr, true), new SearchContext(searchSettings));
const matchingName = searchService.findResultsWithQuery(formatAttrForSearch(attr, false), new SearchContext(searchSettings)); const matchingName = searchService.findResultsWithQuery(attributeFormatter.formatAttrForSearch(attr, false), new SearchContext(searchSettings));
const results = []; const results: SearchResult[] = [];
const allResults = matchingNameAndValue.concat(matchingName); const allResults = matchingNameAndValue.concat(matchingName);
@ -123,7 +126,7 @@ function searchTemplates() {
}).map(note => note.noteId); }).map(note => note.noteId);
} }
module.exports = { export = {
searchFromNote, searchFromNote,
searchAndExecute, searchAndExecute,
getRelatedNotes, 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"; "use strict";
const sqlInit = require('../../services/sql_init'); import sqlInit = require('../../services/sql_init');
const setupService = require('../../services/setup'); import setupService = require('../../services/setup');
const log = require('../../services/log'); import log = require('../../services/log');
const appInfo = require('../../services/app_info'); import appInfo = require('../../services/app_info');
import { Request } from 'express';
function getStatus() { function getStatus() {
return { return {
@ -17,13 +18,13 @@ async function setupNewDocument() {
await sqlInit.createInitialDatabase(); await sqlInit.createInitialDatabase();
} }
function setupSyncFromServer(req) { function setupSyncFromServer(req: Request) {
const { syncServerHost, syncProxy, password } = req.body; const { syncServerHost, syncProxy, password } = req.body;
return setupService.setupSyncFromSyncServer(syncServerHost, syncProxy, password); return setupService.setupSyncFromSyncServer(syncServerHost, syncProxy, password);
} }
function saveSyncSeed(req) { function saveSyncSeed(req: Request) {
const { options, syncVersion } = req.body; const { options, syncVersion } = req.body;
if (appInfo.syncVersion !== syncVersion) { if (appInfo.syncVersion !== syncVersion) {
@ -50,7 +51,7 @@ function getSyncSeed() {
}; };
} }
module.exports = { export = {
getStatus, getStatus,
setupNewDocument, setupNewDocument,
setupSyncFromServer, 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"; "use strict";
const dateNoteService = require('../../services/date_notes'); import dateNoteService = require('../../services/date_notes');
const sql = require('../../services/sql'); import sql = require('../../services/sql');
const cls = require('../../services/cls'); import cls = require('../../services/cls');
const specialNotesService = require('../../services/special_notes'); import specialNotesService = require('../../services/special_notes');
const becca = require('../../becca/becca'); import becca = require('../../becca/becca');
import { Request } from 'express';
function getInboxNote(req) { function getInboxNote(req: Request) {
return specialNotesService.getInboxNote(req.params.date); return specialNotesService.getInboxNote(req.params.date);
} }
function getDayNote(req) { function getDayNote(req: Request) {
return dateNoteService.getDayNote(req.params.date); return dateNoteService.getDayNote(req.params.date);
} }
function getWeekNote(req) { function getWeekNote(req: Request) {
return dateNoteService.getWeekNote(req.params.date); return dateNoteService.getWeekNote(req.params.date);
} }
function getMonthNote(req) { function getMonthNote(req: Request) {
return dateNoteService.getMonthNote(req.params.month); return dateNoteService.getMonthNote(req.params.month);
} }
function getYearNote(req) { function getYearNote(req: Request) {
return dateNoteService.getYearNote(req.params.year); return dateNoteService.getYearNote(req.params.year);
} }
function getDayNotesForMonth(req) { function getDayNotesForMonth(req: Request) {
const month = req.params.month; const month = req.params.month;
return sql.getMap(` return sql.getMap(`
@ -42,7 +43,7 @@ function getDayNotesForMonth(req) {
AND attr.value LIKE '${month}%'`); AND attr.value LIKE '${month}%'`);
} }
function saveSqlConsole(req) { function saveSqlConsole(req: Request) {
return specialNotesService.saveSqlConsole(req.body.sqlConsoleNoteId); return specialNotesService.saveSqlConsole(req.body.sqlConsoleNoteId);
} }
@ -50,14 +51,14 @@ function createSqlConsole() {
return specialNotesService.createSqlConsole(); return specialNotesService.createSqlConsole();
} }
function saveSearchNote(req) { function saveSearchNote(req: Request) {
return specialNotesService.saveSearchNote(req.body.searchNoteId); return specialNotesService.saveSearchNote(req.body.searchNoteId);
} }
function createSearchNote(req) { function createSearchNote(req: Request) {
const hoistedNote = getHoistedNote(); const hoistedNote = getHoistedNote();
const searchString = req.body.searchString || ""; const searchString = req.body.searchString || "";
const ancestorNoteId = req.body.ancestorNoteId || hoistedNote.noteId; const ancestorNoteId = req.body.ancestorNoteId || hoistedNote?.noteId;
return specialNotesService.createSearchNote(searchString, ancestorNoteId); return specialNotesService.createSearchNote(searchString, ancestorNoteId);
} }
@ -66,22 +67,22 @@ function getHoistedNote() {
return becca.getNote(cls.getHoistedNoteId()); return becca.getNote(cls.getHoistedNoteId());
} }
function createLauncher(req) { function createLauncher(req: Request) {
return specialNotesService.createLauncher({ return specialNotesService.createLauncher({
parentNoteId: req.params.parentNoteId, parentNoteId: req.params.parentNoteId,
launcherType: req.params.launcherType launcherType: req.params.launcherType
}); });
} }
function resetLauncher(req) { function resetLauncher(req: Request) {
return specialNotesService.resetLauncher(req.params.noteId); return specialNotesService.resetLauncher(req.params.noteId);
} }
function createOrUpdateScriptLauncherFromApi(req) { function createOrUpdateScriptLauncherFromApi(req: Request) {
return specialNotesService.createOrUpdateScriptLauncherFromApi(req.body); return specialNotesService.createOrUpdateScriptLauncherFromApi(req.body);
} }
module.exports = { export = {
getInboxNote, getInboxNote,
getDayNote, getDayNote,
getWeekNote, getWeekNote,

View File

@ -1,7 +1,9 @@
"use strict"; "use strict";
const sql = require('../../services/sql'); import sql = require('../../services/sql');
const becca = require('../../becca/becca'); import becca = require('../../becca/becca');
import { Request } from 'express';
import ValidationError = require('../../errors/validation_error');
function getSchema() { function getSchema() {
const tableNames = sql.getColumn(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name`); 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; return tables;
} }
function execute(req) { function execute(req: Request) {
const note = becca.getNoteOrThrow(req.params.noteId); 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 { try {
const results = []; const results = [];
@ -51,7 +58,7 @@ function execute(req) {
results results
}; };
} }
catch (e) { catch (e: any) {
return { return {
success: false, success: false,
error: e.message error: e.message
@ -59,7 +66,7 @@ function execute(req) {
} }
} }
module.exports = { export = {
getSchema, getSchema,
execute execute
}; };

View File

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

View File

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

View File

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

View File

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

View File

@ -55,7 +55,7 @@ interface Note {
let note: Partial<Note> = {}; let note: Partial<Note> = {};
let resource: Resource; 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 saxStream = sax.createStream(true);
const rootNoteTitle = file.originalname.toLowerCase().endsWith(".enex") 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) { if (!params.type) {
const parentNote = becca.notes[params.parentNoteId]; const parentNote = becca.notes[params.parentNoteId];
@ -263,7 +263,7 @@ function createNewNoteWithTarget(target: ("into" | "after"), targetBranchId: str
if (target === 'into') { if (target === 'into') {
return createNewNote(params); return createNewNote(params);
} }
else if (target === 'after') { else if (target === 'after' && targetBranchId) {
const afterBranch = becca.branches[targetBranchId]; const afterBranch = becca.branches[targetBranchId];
// not updating utcDateModified to avoid having to sync whole rows // 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); return function () { return eval(`const apiContext = this;\r\n(${script}\r\n)()`); }.call(ctx);
} }
function getParams(params: ScriptParams) { function getParams(params?: ScriptParams) {
if (!params) { if (!params) {
return params; return params;
} }
@ -121,7 +121,7 @@ function getParams(params: ScriptParams) {
}).join(","); }).join(",");
} }
function getScriptBundleForFrontend(note: BNote, script: string, params: ScriptParams) { function getScriptBundleForFrontend(note: BNote, script?: string, params?: ScriptParams) {
let overrideContent = null; let overrideContent = null;
if (script) { if (script) {

View File

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

View File

@ -166,7 +166,7 @@ function createScriptLauncher(parentNoteId: string, forceNoteId?: string) {
interface LauncherConfig { interface LauncherConfig {
parentNoteId: string; parentNoteId: string;
launcherType: string; launcherType: string;
noteId: string; noteId?: string;
} }
function createLauncher({ parentNoteId, launcherType, noteId }: LauncherConfig) { 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) { function fillParamList(paramIds: string[] | Set<string>, truncate = true) {
if (paramIds.length === 0) { if ("length" in paramIds && paramIds.length === 0) {
return; return;
} }

View File

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

View File

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