diff --git a/electron.ts b/electron.ts index c3f3b0bed..754b73bac 100644 --- a/electron.ts +++ b/electron.ts @@ -1,6 +1,8 @@ "use strict"; import electron from "electron"; +import electronDebug from "electron-debug"; +import electronDl from "electron-dl"; import sqlInit from "./src/services/sql_init.js"; import appIconService from "./src/services/app_icon.js"; import windowService from "./src/services/window.js"; @@ -12,11 +14,11 @@ if (require('electron-squirrel-startup')) { } // Adds debug features like hotkeys for triggering dev tools and reload -require('electron-debug')(); +electronDebug(); appIconService.installLocalAppIcon(); -require('electron-dl')({ saveAs: true }); +electronDl({ saveAs: true }); // needed for excalidraw export https://github.com/zadam/trilium/issues/4271 electron.app.commandLine.appendSwitch( diff --git a/package-lock.json b/package-lock.json index 9da67da3e..ca6ba038f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -111,6 +111,7 @@ "@types/sax": "^1.2.7", "@types/semver": "^7.5.8", "@types/serve-favicon": "^2.5.7", + "@types/session-file-store": "^1.2.5", "@types/stream-throttle": "^0.1.4", "@types/tmp": "^0.2.6", "@types/turndown": "^5.0.4", @@ -2614,6 +2615,16 @@ "@types/node": "*" } }, + "node_modules/@types/session-file-store": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/session-file-store/-/session-file-store-1.2.5.tgz", + "integrity": "sha512-xjIyh40IznXLrvbAY/nmxu5cMcPcE3ZoDrSDvd02m6p8UjUgOtZAGI7Os5DDd6THuxClLWNhFo/awy1tYp64Bg==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/express-session": "*" + } + }, "node_modules/@types/stream-throttle": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/@types/stream-throttle/-/stream-throttle-0.1.4.tgz", diff --git a/package.json b/package.json index 3a1eb4354..4a1efa8cf 100644 --- a/package.json +++ b/package.json @@ -144,6 +144,7 @@ "@types/sax": "^1.2.7", "@types/semver": "^7.5.8", "@types/serve-favicon": "^2.5.7", + "@types/session-file-store": "^1.2.5", "@types/stream-throttle": "^0.1.4", "@types/tmp": "^0.2.6", "@types/turndown": "^5.0.4", diff --git a/spec/search/parens.spec.ts b/spec/search/parens.spec.ts index ae1ac5b2f..bf1482356 100644 --- a/spec/search/parens.spec.ts +++ b/spec/search/parens.spec.ts @@ -1,24 +1,26 @@ -const handleParens = require('../../src/services/search/services/handle_parens'); +import handleParens from "../../src/services/search/services/handle_parens"; +import { TokenStructure } from "../../src/services/search/services/types"; describe("Parens handler", () => { it("handles parens", () => { const input = ["(", "hello", ")", "and", "(", "(", "pick", "one", ")", "and", "another", ")"] .map(token => ({token})); - expect(handleParens(input)) - .toEqual([ + const actual: TokenStructure = [ + [ + {token: "hello"} + ], + {token: "and"}, + [ [ - {token: "hello"} + {token: "pick"}, + {token: "one"} ], {token: "and"}, - [ - [ - {token: "pick"}, - {token: "one"} - ], - {token: "and"}, - {token: "another"} - ] - ]); + {token: "another"} + ] + ]; + + expect(handleParens(input)).toEqual(actual); }); }); diff --git a/src/becca/becca-interface.ts b/src/becca/becca-interface.ts index 09ae01959..562ec85db 100644 --- a/src/becca/becca-interface.ts +++ b/src/becca/becca-interface.ts @@ -155,9 +155,7 @@ export default class Becca { } getRevision(revisionId: string): BRevision | null { - const row = sql.getRow("SELECT * FROM revisions WHERE revisionId = ?", [revisionId]); - - const BRevision = require('./entities/brevision'); // avoiding circular dependency problems + const row = sql.getRow("SELECT * FROM revisions WHERE revisionId = ?", [revisionId]); return row ? new BRevision(row) : null; } @@ -179,9 +177,7 @@ export default class Becca { WHERE attachmentId = ? AND isDeleted = 0` : `SELECT * FROM attachments WHERE attachmentId = ? AND isDeleted = 0`; - const BAttachment = require('./entities/battachment'); // avoiding circular dependency problems - - return sql.getRows(query, [attachmentId]) + return sql.getRows(query, [attachmentId]) .map(row => new BAttachment(row))[0]; } @@ -194,7 +190,6 @@ export default class Becca { } getAttachments(attachmentIds: string[]): BAttachment[] { - const BAttachment = require('./entities/battachment'); // avoiding circular dependency problems return sql.getManyRows("SELECT * FROM attachments WHERE attachmentId IN (???) AND isDeleted = 0", attachmentIds) .map(row => new BAttachment(row)); } @@ -204,9 +199,7 @@ export default class Becca { return null; } - const row = sql.getRow("SELECT *, LENGTH(content) AS contentLength FROM blobs WHERE blobId = ?", [entity.blobId]); - - const BBlob = require('./entities/bblob'); // avoiding circular dependency problems + const row = sql.getRow("SELECT *, LENGTH(content) AS contentLength FROM blobs WHERE blobId = ?", [entity.blobId]); return row ? new BBlob(row) : null; } @@ -248,16 +241,12 @@ export default class Becca { } getRecentNotesFromQuery(query: string, params: string[] = []): BRecentNote[] { - const rows = sql.getRows(query, params); - - const BRecentNote = require('./entities/brecent_note'); // avoiding circular dependency problems + const rows = sql.getRows(query, params); return rows.map(row => new BRecentNote(row)); } getRevisionsFromQuery(query: string, params: string[] = []): BRevision[] { const rows = sql.getRows(query, params); - - const BRevision = require('./entities/brevision'); // avoiding circular dependency problems return rows.map(row => new BRevision(row)); } diff --git a/src/becca/becca_loader.ts b/src/becca/becca_loader.ts index dd751bf9a..1bfab31da 100644 --- a/src/becca/becca_loader.ts +++ b/src/becca/becca_loader.ts @@ -14,13 +14,15 @@ import cls from "../services/cls.js"; import entityConstructor from "../becca/entity_constructor.js"; import { AttributeRow, BranchRow, EtapiTokenRow, NoteRow, OptionRow } from './entities/rows'; import AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; +import options_init from "../services/options_init.js"; +import ws from "../services/ws.js"; const beccaLoaded = new Promise((res, rej) => { sqlInit.dbReady.then(() => { cls.init(() => { load(); - require('../services/options_init').initStartupOptions(); + options_init.initStartupOptions(); res(); }); @@ -73,7 +75,7 @@ function load() { function reload(reason: string) { load(); - require('../services/ws').reloadFrontend(reason || "becca reloaded"); + ws.reloadFrontend(reason || "becca reloaded"); } eventService.subscribeBeccaLoader([eventService.ENTITY_CHANGE_SYNCED], ({ entityName, entityRow }) => { diff --git a/src/becca/entities/battachment.ts b/src/becca/entities/battachment.ts index 1fdbd1cb4..bef20acea 100644 --- a/src/becca/entities/battachment.ts +++ b/src/becca/entities/battachment.ts @@ -9,6 +9,7 @@ import log from "../../services/log.js"; import { AttachmentRow } from './rows'; import BNote from "./bnote.js"; import BBranch from "./bbranch.js"; +import noteService from "../../services/notes.js"; const attachmentRoleToNoteTypeMapping = { 'image': 'image', @@ -157,8 +158,6 @@ class BAttachment extends AbstractBeccaEntity { throw new Error(`Cannot convert protected attachment outside of protected session`); } - const noteService = require('../../services/notes'); - const { note, branch } = noteService.createNewNote({ parentNoteId: this.ownerId, title: this.title, diff --git a/src/becca/entities/bbranch.ts b/src/becca/entities/bbranch.ts index 363fb86f2..b0222f115 100644 --- a/src/becca/entities/bbranch.ts +++ b/src/becca/entities/bbranch.ts @@ -8,6 +8,7 @@ import TaskContext from "../../services/task_context.js"; import cls from "../../services/cls.js"; import log from "../../services/log.js"; import { BranchRow } from './rows'; +import handlers from "../../services/handlers.js"; /** * Branch represents a relationship between a child note and its parent note. Trilium allows a note to have multiple @@ -157,7 +158,6 @@ class BBranch extends AbstractBeccaEntity { if (parentBranches.length === 1 && parentBranches[0] === this) { // needs to be run before branches and attributes are deleted and thus attached relations disappear - const handlers = require('../../services/handlers'); handlers.runAttachedRelations(note, 'runOnNoteDeletion', note); } } diff --git a/src/becca/entities/bnote.ts b/src/becca/entities/bnote.ts index 8983ffc8b..b6e05fb10 100644 --- a/src/becca/entities/bnote.ts +++ b/src/becca/entities/bnote.ts @@ -12,10 +12,14 @@ import TaskContext from "../../services/task_context.js"; import dayjs from "dayjs"; import utc from "dayjs/plugin/utc"; import eventService from "../../services/events.js"; -import { AttachmentRow, NoteRow, NoteType, RevisionRow } from './rows'; +import { AttachmentRow, AttributeType, NoteRow, NoteType, RevisionRow } from './rows'; import BBranch from "./bbranch.js"; import BAttribute from "./battribute.js"; import { NotePojo } from '../becca-interface'; +import searchService from "../../services/search/services/search.js"; +import cloningService, { CloneResponse } from "../../services/cloning.js"; +import noteService from "../../services/notes.js"; +import handlers from "../../services/handlers.js"; dayjs.extend(utc); const LABEL = 'label'; @@ -890,11 +894,9 @@ class BNote extends AbstractBeccaEntity { } try { - const searchService = require('../../services/search/services/search'); - const {searchResultNoteIds} = searchService.searchFromNote(this); - + const result = searchService.searchFromNote(this); const becca = this.becca; - return (searchResultNoteIds as string[]) // TODO: remove cast once search is converted + return (result.searchResultNoteIds) .map(resultNoteId => becca.notes[resultNoteId]) .filter(note => !!note); } @@ -1261,7 +1263,7 @@ class BNote extends AbstractBeccaEntity { * @param name - attribute name * @param value - attribute value (optional) */ - setAttribute(type: string, name: string, value?: string) { + setAttribute(type: AttributeType, name: string, value?: string) { const attributes = this.getOwnedAttributes(); const attr = attributes.find(attr => attr.type === type && attr.name === name); @@ -1274,8 +1276,6 @@ class BNote extends AbstractBeccaEntity { } } else { - const BAttribute = require('./battribute'); - new BAttribute({ noteId: this.noteId, type: type, @@ -1310,9 +1310,7 @@ class BNote extends AbstractBeccaEntity { * @param name - name of the attribute, not including the leading ~/# * @param value - value of the attribute - text for labels, target note ID for relations; optional. */ - addAttribute(type: string, name: string, value: string = "", isInheritable: boolean = false, position: number | null = null): BAttribute { - const BAttribute = require('./battribute'); - + addAttribute(type: AttributeType, name: string, value: string = "", isInheritable: boolean = false, position: number | null = null): BAttribute { return new BAttribute({ noteId: this.noteId, type: type, @@ -1351,7 +1349,7 @@ class BNote extends AbstractBeccaEntity { * @param name - attribute name * @param value - attribute value (optional) */ - toggleAttribute(type: string, enabled: boolean, name: string, value?: string) { + toggleAttribute(type: AttributeType, enabled: boolean, name: string, value?: string) { if (enabled) { this.setAttribute(type, name, value); } @@ -1423,8 +1421,6 @@ class BNote extends AbstractBeccaEntity { } searchNotesInSubtree(searchString: string) { - const searchService = require('../../services/search/services/search'); - return searchService.searchNotes(searchString) as BNote[]; } @@ -1432,12 +1428,16 @@ class BNote extends AbstractBeccaEntity { return this.searchNotesInSubtree(searchString)[0]; } - cloneTo(parentNoteId: string) { - const cloningService = require('../../services/cloning'); - + cloneTo(parentNoteId: string): CloneResponse { const branch = this.becca.getNote(parentNoteId)?.getParentBranches()[0]; + if (!branch?.branchId) { + return { + success: false, + message: "Unable to find the branch ID to clone." + }; + } - return cloningService.cloneNoteToBranch(this.noteId, branch?.branchId); + return cloningService.cloneNoteToBranch(this.noteId, branch.branchId); } isEligibleForConversionToAttachment(opts: ConvertOpts = { autoConversion: false }) { @@ -1508,7 +1508,6 @@ class BNote extends AbstractBeccaEntity { parentNote.setContent(fixedContent); - const noteService = require('../../services/notes'); noteService.asyncPostProcessContent(parentNote, fixedContent); // to mark an unused attachment for deletion this.deleteNote(); @@ -1535,7 +1534,6 @@ class BNote extends AbstractBeccaEntity { } // needs to be run before branches and attributes are deleted and thus attached relations disappear - const handlers = require('../../services/handlers'); handlers.runAttachedRelations(this, 'runOnNoteDeletion', this); taskContext.noteDeletionHandlerTriggered = true; diff --git a/src/becca/entities/brevision.ts b/src/becca/entities/brevision.ts index ae3b68c70..43c7e1a3e 100644 --- a/src/becca/entities/brevision.ts +++ b/src/becca/entities/brevision.ts @@ -8,6 +8,7 @@ import AbstractBeccaEntity from "./abstract_becca_entity.js"; import sql from "../../services/sql.js"; import BAttachment from "./battachment.js"; import { AttachmentRow, RevisionRow } from './rows'; +import eraseService from "../../services/erase.js"; interface ContentOpts { /** will also save this BRevision entity */ @@ -164,7 +165,9 @@ class BRevision extends AbstractBeccaEntity { * Revisions are not soft-deletable, they are immediately hard-deleted (erased). */ eraseRevision() { - require('../../services/erase.js').eraseRevisions([this.revisionId]); + if (this.revisionId) { + eraseService.eraseRevisions([this.revisionId]); + } } beforeSaving() { diff --git a/src/becca/entities/rows.ts b/src/becca/entities/rows.ts index 119d392ec..6c46bf301 100644 --- a/src/becca/entities/rows.ts +++ b/src/becca/entities/rows.ts @@ -42,7 +42,7 @@ export interface OptionRow { name: string; value: string; isSynced: boolean; - utcDateModified: string; + utcDateModified?: string; } export interface EtapiTokenRow { @@ -69,7 +69,7 @@ export interface AttributeRow { noteId?: string; type: AttributeType; name: string; - position?: number; + position?: number | null; value?: string; isInheritable?: boolean; utcDateModified?: string; diff --git a/src/routes/api/image.ts b/src/routes/api/image.ts index b20b8c78d..890b4a88a 100644 --- a/src/routes/api/image.ts +++ b/src/routes/api/image.ts @@ -2,12 +2,12 @@ import imageService from "../../services/image.js"; import becca from "../../becca/becca.js"; -const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR; import fs from "fs"; import { Request, Response } from 'express'; import BNote from "../../becca/entities/bnote.js"; import BRevision from "../../becca/entities/brevision.js"; import { AppRequest } from '../route-interface'; +import { RESOURCE_DIR } from "../../services/resource_dir.js"; function returnImageFromNote(req: Request, res: Response) { const image = becca.getNote(req.params.noteId); diff --git a/src/routes/api/search.ts b/src/routes/api/search.ts index 0c2966dad..fbfe74766 100644 --- a/src/routes/api/search.ts +++ b/src/routes/api/search.ts @@ -4,19 +4,19 @@ import { Request } from "express"; import becca from "../../becca/becca.js"; import SearchContext from "../../services/search/search_context.js"; -import searchService from "../../services/search/services/search.js"; +import searchService, { EMPTY_RESULT, SearchNoteResult } from "../../services/search/services/search.js"; import bulkActionService from "../../services/bulk_actions.js"; import cls from "../../services/cls.js"; import attributeFormatter from "../../services/attribute_formatter.js"; import ValidationError from "../../errors/validation_error.js"; import SearchResult from "../../services/search/search_result.js"; -function searchFromNote(req: Request) { +function searchFromNote(req: Request): SearchNoteResult { const note = becca.getNoteOrThrow(req.params.noteId); if (!note) { // this can be triggered from recent changes, and it's harmless to return an empty list rather than fail - return []; + return EMPTY_RESULT; } if (note.type !== 'search') { diff --git a/src/routes/api/sync.ts b/src/routes/api/sync.ts index 183c79878..646a514f4 100644 --- a/src/routes/api/sync.ts +++ b/src/routes/api/sync.ts @@ -14,6 +14,7 @@ import ws from "../../services/ws.js"; import { Request } from 'express'; import { EntityChange, EntityChangeRecord } from '../../services/entity_changes_interface'; import ValidationError from "../../errors/validation_error.js"; +import consistencyChecksService from "../../services/consistency_checks.js"; async function testSync() { try { @@ -206,7 +207,7 @@ function queueSector(req: Request) { } function checkEntityChanges() { - require('../../services/consistency_checks').runEntityChangesChecks(); + consistencyChecksService.runEntityChangesChecks(); } export default { diff --git a/src/routes/session_parser.ts b/src/routes/session_parser.ts index b880a8915..ee9918152 100644 --- a/src/routes/session_parser.ts +++ b/src/routes/session_parser.ts @@ -1,7 +1,8 @@ import session from "express-session"; +import sessionFileStore from "session-file-store"; import sessionSecret from "../services/session_secret.js"; import dataDir from "../services/data_dir.js"; -const FileStore = require('session-file-store')(session); +const FileStore = sessionFileStore(session); const sessionParser = session({ secret: sessionSecret, diff --git a/src/services/bulk_actions.ts b/src/services/bulk_actions.ts index 2b81c634d..64c180405 100644 --- a/src/services/bulk_actions.ts +++ b/src/services/bulk_actions.ts @@ -111,7 +111,7 @@ const ACTION_HANDLERS: Record = { res = branchService.moveBranchToNote(note.getParentBranches()[0], action.targetParentNoteId); } - if (!res.success) { + if ("success" in res && !res.success) { log.info(`Moving/cloning note ${note.noteId} to ${action.targetParentNoteId} failed with error ${JSON.stringify(res)}`); } }, diff --git a/src/services/cloning.ts b/src/services/cloning.ts index 939b6bf42..1176ff37a 100644 --- a/src/services/cloning.ts +++ b/src/services/cloning.ts @@ -1,18 +1,28 @@ "use strict"; -const sql = require('./sql'); -const eventChangesService = require('./entity_changes'); -const treeService = require('./tree'); -const BBranch = require('../becca/entities/bbranch'); -const becca = require('../becca/becca'); -const log = require('./log'); +import sql from './sql'; +import eventChangesService from './entity_changes'; +import treeService from './tree'; +import BBranch from '../becca/entities/bbranch'; +import becca from '../becca/becca'; +import log from './log'; -function cloneNoteToParentNote(noteId: string, parentNoteId: string, prefix: string | null = null) { +export interface CloneResponse { + success: boolean; + message?: string; + branchId?: string; + notePath?: string; +} + +function cloneNoteToParentNote(noteId: string, parentNoteId: string, prefix: string | null = null): CloneResponse { if (!(noteId in becca.notes) || !(parentNoteId in becca.notes)) { return { success: false, message: 'Note cannot be cloned because either the cloned note or the intended parent is deleted.' }; } const parentNote = becca.getNote(parentNoteId); + if (!parentNote) { + return { success: false, message: 'Note cannot be cloned because the parent note could not be found.' }; + } if (parentNote.type === 'search') { return { @@ -31,7 +41,7 @@ function cloneNoteToParentNote(noteId: string, parentNoteId: string, prefix: str noteId: noteId, parentNoteId: parentNoteId, prefix: prefix, - isExpanded: 0 + isExpanded: false }).save(); log.info(`Cloned note '${noteId}' to a new parent note '${parentNoteId}' with prefix '${prefix}'`); @@ -43,7 +53,7 @@ function cloneNoteToParentNote(noteId: string, parentNoteId: string, prefix: str }; } -function cloneNoteToBranch(noteId: string, parentBranchId: string, prefix: string) { +function cloneNoteToBranch(noteId: string, parentBranchId: string, prefix?: string) { const parentBranch = becca.getBranch(parentBranchId); if (!parentBranch) { @@ -67,6 +77,9 @@ function ensureNoteIsPresentInParent(noteId: string, parentNoteId: string, prefi const parentNote = becca.getNote(parentNoteId); + if (!parentNote) { + return { branch: null, success: false, message: "Can't find parent note." }; + } if (parentNote.type === 'search') { return { branch: null, success: false, message: "Can't clone into a search note" }; } @@ -81,7 +94,7 @@ function ensureNoteIsPresentInParent(noteId: string, parentNoteId: string, prefi noteId: noteId, parentNoteId: parentNoteId, prefix: prefix, - isExpanded: 0 + isExpanded: false }).save(); log.info(`Ensured note '${noteId}' is in parent note '${parentNoteId}' with prefix '${branch.prefix}'`); @@ -90,7 +103,7 @@ function ensureNoteIsPresentInParent(noteId: string, parentNoteId: string, prefi } function ensureNoteIsAbsentFromParent(noteId: string, parentNoteId: string) { - const branchId = sql.getValue(`SELECT branchId FROM branches WHERE noteId = ? AND parentNoteId = ? AND isDeleted = 0`, [noteId, parentNoteId]); + const branchId = sql.getValue(`SELECT branchId FROM branches WHERE noteId = ? AND parentNoteId = ? AND isDeleted = 0`, [noteId, parentNoteId]); const branch = becca.getBranch(branchId); if (branch) { @@ -137,13 +150,13 @@ function cloneNoteAfter(noteId: string, afterBranchId: string) { if (!(noteId in becca.notes)) { return { success: false, message: `Note to be cloned '${noteId}' is deleted or does not exist.` }; - } else if (!(afterNote.parentNoteId in becca.notes)) { - return { success: false, message: `After note '${afterNote.parentNoteId}' is deleted or does not exist.` }; + } else if (!afterNote || !(afterNote.parentNoteId in becca.notes)) { + return { success: false, message: `After note '${afterNote?.parentNoteId}' is deleted or does not exist.` }; } const parentNote = becca.getNote(afterNote.parentNoteId); - if (parentNote.type === 'search') { + if (!parentNote || parentNote.type === 'search') { return { success: false, message: "Can't clone into a search note" @@ -167,7 +180,7 @@ function cloneNoteAfter(noteId: string, afterBranchId: string) { noteId: noteId, parentNoteId: afterNote.parentNoteId, notePosition: afterNote.notePosition + 10, - isExpanded: 0 + isExpanded: false }).save(); log.info(`Cloned note '${noteId}' into parent note '${afterNote.parentNoteId}' after note '${afterNote.noteId}', branch '${afterBranchId}'`); diff --git a/src/services/entity_changes.ts b/src/services/entity_changes.ts index ece4fb1b2..06c380f38 100644 --- a/src/services/entity_changes.ts +++ b/src/services/entity_changes.ts @@ -8,6 +8,7 @@ import becca from "../becca/becca.js"; import blobService from "../services/blob.js"; import { EntityChange } from './entity_changes_interface'; import type { Blob } from "./blob-interface"; +import eventService from "./events.js"; let maxEntityChangeId = 0; @@ -57,8 +58,6 @@ function putNoteReorderingEntityChange(parentNoteId: string, componentId?: strin instanceId }); - const eventService = require('./events'); - eventService.emit(eventService.ENTITY_CHANGED, { entityName: 'note_reordering', entity: sql.getMap(`SELECT branchId, notePosition FROM branches WHERE isDeleted = 0 AND parentNoteId = ?`, [parentNoteId]) diff --git a/src/services/events.ts b/src/services/events.ts index b87520d35..8b64d5af0 100644 --- a/src/services/events.ts +++ b/src/services/events.ts @@ -1,4 +1,4 @@ -const log = require('./log'); +import log from "./log"; const NOTE_TITLE_CHANGED = "NOTE_TITLE_CHANGED"; const ENTER_PROTECTED_SESSION = "ENTER_PROTECTED_SESSION"; diff --git a/src/services/export/zip.ts b/src/services/export/zip.ts index 4838cb957..d198b0c92 100644 --- a/src/services/export/zip.ts +++ b/src/services/export/zip.ts @@ -11,7 +11,6 @@ import protectedSessionService from "../protected_session.js"; import sanitize from "sanitize-filename"; import fs from "fs"; import becca from "../../becca/becca.js"; -const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR; import archiver from "archiver"; import log from "../log.js"; import TaskContext from "../task_context.js"; @@ -21,6 +20,7 @@ import AttachmentMeta from "../meta/attachment_meta.js"; import AttributeMeta from "../meta/attribute_meta.js"; import BBranch from "../../becca/entities/bbranch.js"; import { Response } from 'express'; +import { RESOURCE_DIR } from "../resource_dir.js"; async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "html" | "markdown", res: Response | fs.WriteStream, setHeaders = true) { if (!['html', 'markdown'].includes(format)) { diff --git a/src/services/image.ts b/src/services/image.ts index 85035e6fb..5bef79ac9 100644 --- a/src/services/image.ts +++ b/src/services/image.ts @@ -156,7 +156,6 @@ function saveImageToAttachment(noteId: string, uploadBuffer: Buffer, originalNam setTimeout(() => { sql.transactional(() => { const note = becca.getNoteOrThrow(noteId); - const noteService = require('../services/notes'); noteService.asyncPostProcessContent(note, note.getContent()); // to mark an unused attachment for deletion }); }, 5000); diff --git a/src/services/import/enex.ts b/src/services/import/enex.ts index e2ccaa78a..739b418c3 100644 --- a/src/services/import/enex.ts +++ b/src/services/import/enex.ts @@ -12,6 +12,7 @@ import sanitizeAttributeName from "../sanitize_attribute_name.js"; import TaskContext from "../task_context.js"; import BNote from "../../becca/entities/bnote.js"; import { File } from "./common"; +import { AttributeType } from "../../becca/entities/rows.js"; /** * date format is e.g. 20181121T193703Z or 2013-04-14T16:19:00.000Z (Mac evernote, see #3496) @@ -29,7 +30,7 @@ function parseDate(text: string) { } interface Attribute { - type: string; + type: AttributeType; name: string; value: string; } diff --git a/src/services/log.ts b/src/services/log.ts index 414dc27e1..9bd97a2a7 100644 --- a/src/services/log.ts +++ b/src/services/log.ts @@ -50,7 +50,7 @@ function checkDate(millisSinceMidnight: number) { return millisSinceMidnight; } -function log(str: string) { +function log(str: string | Error) { const bundleNoteId = cls.get("bundleNoteId"); if (bundleNoteId) { @@ -66,11 +66,11 @@ function log(str: string) { console.log(str); } -function info(message: string) { +function info(message: string | Error) { log(message); } -function error(message: string) { +function error(message: string | Error) { log(`ERROR: ${message}`); } diff --git a/src/services/notes.ts b/src/services/notes.ts index feefab45a..20df9948a 100644 --- a/src/services/notes.ts +++ b/src/services/notes.ts @@ -26,6 +26,7 @@ import html2plaintext from "html2plaintext"; import { AttachmentRow, AttributeRow, BranchRow, NoteRow, NoteType } from '../becca/entities/rows'; import TaskContext from "./task_context.js"; import { NoteParams } from './note-interface'; +import imageService from "./image.js"; interface FoundLink { name: "imageLink" | "internalLink" | "includeNoteLink" | "relationMapLink", @@ -466,7 +467,7 @@ async function downloadImage(noteId: string, imageUrl: string) { const unescapedUrl = utils.unescapeHtml(imageUrl); try { - let imageBuffer; + let imageBuffer: Buffer; if (imageUrl.toLowerCase().startsWith("file://")) { imageBuffer = await new Promise((res, rej) => { @@ -487,10 +488,13 @@ async function downloadImage(noteId: string, imageUrl: string) { const parsedUrl = url.parse(unescapedUrl); const title = path.basename(parsedUrl.pathname || ""); - const imageService = require('../services/image'); const attachment = imageService.saveImageToAttachment(noteId, imageBuffer, title, true, true); - - imageUrlToAttachmentIdMapping[imageUrl] = attachment.attachmentId; + + if (attachment.attachmentId) { + imageUrlToAttachmentIdMapping[imageUrl] = attachment.attachmentId; + } else { + log.error(`Download of '${imageUrl}' due to no attachment ID.`); + } log.info(`Download of '${imageUrl}' succeeded and was saved as image attachment '${attachment.attachmentId}' of note '${noteId}'`); } @@ -520,7 +524,6 @@ function downloadImages(noteId: string, content: string) { const imageBase64 = url.substr(inlineImageMatch[0].length); const imageBuffer = Buffer.from(imageBase64, 'base64'); - const imageService = require('../services/image'); const attachment = imageService.saveImageToAttachment(noteId, imageBuffer, "inline image", true, true); const encodedTitle = encodeURIComponent(attachment.title); diff --git a/src/services/options.ts b/src/services/options.ts index 0f2a81b57..bd9b289ad 100644 --- a/src/services/options.ts +++ b/src/services/options.ts @@ -1,4 +1,5 @@ import becca from "../becca/becca.js"; +import BOption from "../becca/entities/boption.js"; import { OptionRow } from '../becca/entities/rows'; import sql from "./sql.js"; @@ -68,10 +69,7 @@ function setOption(name: string, value: string | number | boolean) { } } -function createOption(name: string, value: string | number, isSynced: boolean) { - // to avoid circular dependency, need to find a better solution - const BOption = require('../becca/entities/boption'); - +function createOption(name: string, value: string, isSynced: boolean) { new BOption({ name: name, value: value, diff --git a/src/services/request.ts b/src/services/request.ts index 56aea5b1e..fe057024c 100644 --- a/src/services/request.ts +++ b/src/services/request.ts @@ -137,7 +137,7 @@ function exec(opts: ExecOpts): Promise { }); } -function getImage(imageUrl: string) { +function getImage(imageUrl: string): Promise { const proxyConf = syncOptions.getSyncProxy(); const opts: ClientOpts = { method: 'GET', @@ -149,7 +149,7 @@ function getImage(imageUrl: string) { const proxyAgent = getProxyAgent(opts); const parsedTargetUrl = url.parse(opts.url); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { try { const request = client.request({ method: opts.method, @@ -181,8 +181,7 @@ function getImage(imageUrl: string) { }); request.end(undefined); - } - catch (e: any) { + } catch (e: any) { reject(generateError(opts, e.message)); } }); diff --git a/src/services/resource_dir.ts b/src/services/resource_dir.ts index 14f286fd7..7e194dc67 100644 --- a/src/services/resource_dir.ts +++ b/src/services/resource_dir.ts @@ -2,7 +2,7 @@ import log from "./log.js"; import path from "path"; import fs from "fs"; -const RESOURCE_DIR = path.resolve(__dirname, "../.."); +export const RESOURCE_DIR = path.resolve(__dirname, "../.."); // where the "trilium" executable is const ELECTRON_APP_ROOT_DIR = path.resolve(RESOURCE_DIR, "../.."); diff --git a/src/services/search/services/handle_parens.ts b/src/services/search/services/handle_parens.ts index c33bde37f..a5fb1f227 100644 --- a/src/services/search/services/handle_parens.ts +++ b/src/services/search/services/handle_parens.ts @@ -1,9 +1,9 @@ -import { TokenData } from "./types"; +import { TokenData, TokenStructure } from "./types"; /** * This will create a recursive object from a list of tokens - tokens between parenthesis are grouped in a single array */ -function handleParens(tokens: (TokenData | TokenData[])[]) { +function handleParens(tokens: TokenStructure) { if (tokens.length === 0) { return []; } diff --git a/src/services/search/services/parse.ts b/src/services/search/services/parse.ts index 65a923dc7..f78e6a55a 100644 --- a/src/services/search/services/parse.ts +++ b/src/services/search/services/parse.ts @@ -21,7 +21,7 @@ import utils from "../../utils.js"; import TrueExp from "../expressions/true.js"; import IsHiddenExp from "../expressions/is_hidden.js"; import SearchContext from "../search_context.js"; -import { TokenData } from "./types"; +import { TokenData, TokenStructure } from "./types"; import Expression from "../expressions/expression.js"; function getFulltext(_tokens: TokenData[], searchContext: SearchContext) { @@ -448,7 +448,7 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level function parse({fulltextTokens, expressionTokens, searchContext}: { fulltextTokens: TokenData[], - expressionTokens: (TokenData | TokenData[])[], + expressionTokens: TokenStructure, searchContext: SearchContext, originalQuery: string }) { diff --git a/src/services/search/services/search.ts b/src/services/search/services/search.ts index 7be34cbaf..18d70be34 100644 --- a/src/services/search/services/search.ts +++ b/src/services/search/services/search.ts @@ -13,11 +13,24 @@ import log from "../../log.js"; import hoistedNoteService from "../../hoisted_note.js"; import BNote from "../../../becca/entities/bnote.js"; import BAttribute from "../../../becca/entities/battribute.js"; -import { SearchParams, TokenData } from "./types"; +import { SearchParams, TokenStructure } from "./types"; import Expression from "../expressions/expression.js"; import sql from "../../sql.js"; +import scriptService from "../../script.js"; -function searchFromNote(note: BNote) { +export interface SearchNoteResult { + searchResultNoteIds: string[]; + highlightedTokens: string[]; + error: string | null; +} + +export const EMPTY_RESULT: SearchNoteResult = { + searchResultNoteIds: [], + highlightedTokens: [], + error: null +}; + +function searchFromNote(note: BNote): SearchNoteResult { let searchResultNoteIds; let highlightedTokens: string[]; @@ -78,7 +91,6 @@ function searchFromRelation(note: BNote, relationName: string) { return []; } - const scriptService = require('../../script'); // TODO: to avoid circular dependency const result = scriptService.executeNote(scriptNote, {originEntity: note}); if (!Array.isArray(result)) { @@ -273,7 +285,7 @@ function parseQueryToExpression(query: string, searchContext: SearchContext) { const {fulltextQuery, fulltextTokens, expressionTokens} = lex(query); searchContext.fulltextQuery = fulltextQuery; - let structuredExpressionTokens: (TokenData | TokenData[])[]; + let structuredExpressionTokens: TokenStructure; try { structuredExpressionTokens = handleParens(expressionTokens); diff --git a/src/services/search/services/types.ts b/src/services/search/services/types.ts index 09450f760..2b12a3c65 100644 --- a/src/services/search/services/types.ts +++ b/src/services/search/services/types.ts @@ -1,3 +1,5 @@ +export type TokenStructure = (TokenData | TokenStructure)[]; + export interface TokenData { token: string; inQuotes?: boolean; diff --git a/src/services/sql.ts b/src/services/sql.ts index df9742d3b..0d92e6209 100644 --- a/src/services/sql.ts +++ b/src/services/sql.ts @@ -10,6 +10,9 @@ import dataDir from "./data_dir.js"; import cls from "./cls.js"; import fs from "fs-extra"; import Database from "better-sqlite3"; +import ws from "./ws.js"; +import becca_loader from "../becca/becca_loader.js"; +import entity_changes from "./entity_changes.js"; const dbConnection: DatabaseType = new Database(dataDir.DOCUMENT_PATH); dbConnection.pragma('journal_mode = WAL'); @@ -248,7 +251,7 @@ function transactional(func: (statement: Statement) => T) { const ret = (dbConnection.transaction(func) as any).deferred(); if (!dbConnection.inTransaction) { // i.e. transaction was really committed (and not just savepoint released) - require('./ws').sendTransactionEntityChangesToAllClients(); + ws.sendTransactionEntityChangesToAllClients(); } return ret; @@ -259,11 +262,11 @@ function transactional(func: (statement: Statement) => T) { if (entityChangeIds.length > 0) { log.info("Transaction rollback dirtied the becca, forcing reload."); - require('../becca/becca_loader').load(); + becca_loader.load(); } // the maxEntityChangeId has been incremented during failed transaction, need to recalculate - require('./entity_changes').recalculateMaxEntityChangeId(); + entity_changes.recalculateMaxEntityChangeId(); throw e; } diff --git a/src/services/sql_init.ts b/src/services/sql_init.ts index 1c66723a0..2c10d97ab 100644 --- a/src/services/sql_init.ts +++ b/src/services/sql_init.ts @@ -11,6 +11,10 @@ import migrationService from "./migration.js"; import cls from "./cls.js"; import config from "./config.js"; import { OptionRow } from '../becca/entities/rows'; +import optionsInitService from "./options_init.js"; +import BNote from "../becca/entities/bnote.js"; +import BBranch from "../becca/entities/bbranch.js"; +import zipImportService from "./import/zip.js"; const dbReady = utils.deferred(); @@ -54,7 +58,7 @@ async function createInitialDatabase() { const schema = fs.readFileSync(`${resourceDir.DB_INIT_DIR}/schema.sql`, "utf-8"); const demoFile = fs.readFileSync(`${resourceDir.DB_INIT_DIR}/demo.zip`); - let rootNote; + let rootNote!: BNote; sql.transactional(() => { log.info("Creating database schema ..."); @@ -63,9 +67,6 @@ async function createInitialDatabase() { require('../becca/becca_loader').load(); - const BNote = require('../becca/entities/bnote'); - const BBranch = require('../becca/entities/bbranch'); - log.info("Creating root note ..."); rootNote = new BNote({ @@ -84,8 +85,6 @@ async function createInitialDatabase() { notePosition: 10 }).save(); - const optionsInitService = require('./options_init'); - optionsInitService.initDocumentOptions(); optionsInitService.initNotSyncedOptions(true, {}); optionsInitService.initStartupOptions(); @@ -96,7 +95,6 @@ async function createInitialDatabase() { const dummyTaskContext = new TaskContext("no-progress-reporting", 'import', false); - const zipImportService = require('./import/zip'); await zipImportService.importZip(dummyTaskContext, demoFile, rootNote); sql.transactional(() => { @@ -106,7 +104,6 @@ async function createInitialDatabase() { const startNoteId = sql.getValue("SELECT noteId FROM branches WHERE parentNoteId = 'root' AND isDeleted = 0 ORDER BY notePosition"); - const optionService = require('./options'); optionService.setOption('openNoteContexts', JSON.stringify([ { notePath: startNoteId, diff --git a/src/services/sync.ts b/src/services/sync.ts index 7adea4206..9f6bdeb8a 100644 --- a/src/services/sync.ts +++ b/src/services/sync.ts @@ -19,6 +19,8 @@ import entityConstructor from "../becca/entity_constructor.js"; import becca from "../becca/becca.js"; import { EntityChange, EntityChangeRecord, EntityRow } from './entity_changes_interface'; import { CookieJar, ExecOpts } from './request_interface'; +import setupService from "./setup.js"; +import consistency_checks from "./consistency_checks.js"; let proxyToggle = true; @@ -107,8 +109,6 @@ async function sync() { } async function login() { - const setupService = require('./setup'); // circular dependency issue - if (!await setupService.hasSyncServerSchemaAndSeed()) { await setupService.sendSeedToSyncServer(); } @@ -282,8 +282,7 @@ async function checkContentHash(syncContext: SyncContext) { if (failedChecks.length > 0) { // before re-queuing sectors, make sure the entity changes are correct - const consistencyChecks = require('./consistency_checks'); - consistencyChecks.runEntityChangesChecks(); + consistency_checks.runEntityChangesChecks(); await syncRequest(syncContext, 'POST', `/api/sync/check-entity-changes`); } diff --git a/src/services/sync_mutex.ts b/src/services/sync_mutex.ts index 60bb1ba4f..5bdbaa36f 100644 --- a/src/services/sync_mutex.ts +++ b/src/services/sync_mutex.ts @@ -3,7 +3,7 @@ * (like consistency checks) can use this mutex to make sure sync isn't currently running. */ -const Mutex = require('async-mutex').Mutex; +import { Mutex } from "async-mutex"; const instance = new Mutex(); async function doExclusively(func: () => T) { diff --git a/src/services/tree.ts b/src/services/tree.ts index c3efb8ed0..0ab72e7f7 100644 --- a/src/services/tree.ts +++ b/src/services/tree.ts @@ -7,7 +7,13 @@ import entityChangesService from "./entity_changes.js"; import becca from "../becca/becca.js"; import BNote from "../becca/entities/bnote.js"; -function validateParentChild(parentNoteId: string, childNoteId: string, branchId: string | null = null) { +interface ValidationResponse { + branch: BBranch | null; + success: boolean; + message?: string; +} + +function validateParentChild(parentNoteId: string, childNoteId: string, branchId: string | null = null): ValidationResponse { if (['root', '_hidden', '_share', '_lbRoot', '_lbAvailableLaunchers', '_lbVisibleLaunchers'].includes(childNoteId)) { return { branch: null, success: false, message: `Cannot change this note's location.` }; } diff --git a/src/services/utils.ts b/src/services/utils.ts index b14ce5c71..1b75046d0 100644 --- a/src/services/utils.ts +++ b/src/services/utils.ts @@ -1,13 +1,15 @@ "use strict"; import crypto from "crypto"; -const randtoken = require('rand-token').generator({source: 'crypto'}); +import { generator } from "rand-token"; import unescape from "unescape"; import escape from "escape-html"; import sanitize from "sanitize-filename"; import mimeTypes from "mime-types"; import path from "path"; +const randtoken = generator({source: 'crypto'}); + function newEntityId() { return randomString(12); } @@ -173,7 +175,7 @@ function replaceAll(string: string, replaceWhat: string, replaceWith: string) { return string.replace(new RegExp(quotedReplaceWhat, "g"), replaceWith); } -function formatDownloadTitle(fileName: string, type: string, mime: string) { +function formatDownloadTitle(fileName: string, type: string | null, mime: string) { if (!fileName) { fileName = "untitled"; } @@ -182,7 +184,7 @@ function formatDownloadTitle(fileName: string, type: string, mime: string) { if (type === 'text') { return `${fileName}.html`; - } else if (['relationMap', 'canvas', 'search'].includes(type)) { + } else if (type && ['relationMap', 'canvas', 'search'].includes(type)) { return `${fileName}.json`; } else { if (!mime) { diff --git a/src/share/routes.ts b/src/share/routes.ts index 4c892e53c..b491b5a7e 100644 --- a/src/share/routes.ts +++ b/src/share/routes.ts @@ -15,6 +15,7 @@ import log from "../services/log.js"; import SNote from "./shaca/entities/snote.js"; import SBranch from "./shaca/entities/sbranch.js"; import SAttachment from "./shaca/entities/sattachment.js"; +import utils from "../services/utils.js"; function getSharedSubTreeRoot(note: SNote): { note?: SNote; branch?: SBranch } { if (note.noteId === shareRoot.SHARE_ROOT_NOTE_ID) { @@ -249,8 +250,6 @@ function register(router: Router) { addNoIndexHeader(note, res); - const utils = require('../services/utils'); - const filename = utils.formatDownloadTitle(note.title, note.type, note.mime); res.setHeader('Content-Disposition', utils.getContentDisposition(filename)); @@ -317,8 +316,6 @@ function register(router: Router) { addNoIndexHeader(attachment.note, res); - const utils = require('../services/utils'); - const filename = utils.formatDownloadTitle(attachment.title, null, attachment.mime); res.setHeader('Content-Disposition', utils.getContentDisposition(filename)); diff --git a/src/share/shaca/entities/sattribute.ts b/src/share/shaca/entities/sattribute.ts index 73298ef2e..189d9e6a6 100644 --- a/src/share/shaca/entities/sattribute.ts +++ b/src/share/shaca/entities/sattribute.ts @@ -1,10 +1,9 @@ "use strict"; +import AbstractShacaEntity from "./abstract_shaca_entity"; import { SAttributeRow } from "./rows"; import SNote from "./snote.js"; -const AbstractShacaEntity = require('./abstract_shaca_entity'); - class SAttribute extends AbstractShacaEntity { attributeId: string; diff --git a/src/share/shaca/entities/snote.ts b/src/share/shaca/entities/snote.ts index 923e35a29..755404b40 100644 --- a/src/share/shaca/entities/snote.ts +++ b/src/share/shaca/entities/snote.ts @@ -27,7 +27,7 @@ class SNote extends AbstractShacaEntity { parentBranches: SBranch[]; parents: SNote[]; children: SNote[]; - private ownedAttributes: SAttribute[]; + ownedAttributes: SAttribute[]; private __attributeCache: SAttribute[] | null; private __inheritableAttributeCache: SAttribute[] | null; targetRelations: SAttribute[]; diff --git a/src/share/shaca/shaca-interface.ts b/src/share/shaca/shaca-interface.ts index 43fbf9dfa..f568e9cec 100644 --- a/src/share/shaca/shaca-interface.ts +++ b/src/share/shaca/shaca-interface.ts @@ -8,7 +8,7 @@ export default class Shaca { notes!: Record; branches!: Record; childParentToBranch!: Record; - private attributes!: Record; + attributes!: Record; attachments!: Record; aliasToNote!: Record; shareRootNote!: SNote | null; diff --git a/src/www.ts b/src/www.ts index c1ec2f0f3..7f0b14aa5 100644 --- a/src/www.ts +++ b/src/www.ts @@ -1,23 +1,4 @@ #!/usr/bin/env node - -// setup basic error handling even before requiring dependencies, since those can produce errors as well - -process.on('unhandledRejection', error => { - // this makes sure that stacktrace of failed promise is printed out - console.log(error); - - // but also try to log it into file - require('./services/log').info(error); -}); - -function exit() { - console.log("Caught interrupt/termination signal. Exiting."); - process.exit(0); -} - -process.on('SIGINT', exit); -process.on('SIGTERM', exit); - import app from "./app.js"; import sessionParser from "./routes/session_parser.js"; import fs from "fs"; @@ -32,6 +13,25 @@ import port from "./services/port.js"; import host from "./services/host.js"; import semver from "semver"; +// setup basic error handling even before requiring dependencies, since those can produce errors as well + +process.on('unhandledRejection', (error: Error) => { + // this makes sure that stacktrace of failed promise is printed out + console.log(error); + + // but also try to log it into file + log.info(error); +}); + +function exit() { + console.log("Caught interrupt/termination signal. Exiting."); + process.exit(0); +} + +process.on('SIGINT', exit); +process.on('SIGTERM', exit); + + if (!semver.satisfies(process.version, ">=10.5.0")) { console.error("Trilium only supports node.js 10.5 and later"); process.exit(1);