diff --git a/src/becca/entities/abstract_becca_entity.ts b/src/becca/entities/abstract_becca_entity.ts index 471cbf47f..519a4327f 100644 --- a/src/becca/entities/abstract_becca_entity.ts +++ b/src/becca/entities/abstract_becca_entity.ts @@ -104,13 +104,14 @@ abstract class AbstractBeccaEntity> { /** * Saves entity - executes SQL, but doesn't commit the transaction on its own */ - save(): this { + // FIXME: opts not used but called a few times, maybe should be used by derived classes or passed to beforeSaving. + save(opts?: {}): this { const constructorData = (this.constructor as unknown as ConstructorData); const entityName = constructorData.entityName; const primaryKeyName = constructorData.primaryKeyName; const isNewEntity = !(this as any)[primaryKeyName]; - + this.beforeSaving(); const pojo = this.getPojoToSave(); diff --git a/src/becca/entities/battachment.ts b/src/becca/entities/battachment.ts index 84f7de5c1..a5fc02698 100644 --- a/src/becca/entities/battachment.ts +++ b/src/becca/entities/battachment.ts @@ -45,7 +45,7 @@ class BAttachment extends AbstractBeccaEntity { blobId?: string; isProtected?: boolean; dateModified?: string; - utcDateScheduledForErasureSince?: string; + utcDateScheduledForErasureSince?: string | null; /** optionally added to the entity */ contentLength?: number; isDecrypted?: boolean; @@ -126,8 +126,8 @@ class BAttachment extends AbstractBeccaEntity { } } - getContent(): string | Buffer { - return this._getContent(); + getContent(): Buffer { + return this._getContent() as Buffer; } setContent(content: any, opts: ContentOpts) { @@ -170,6 +170,11 @@ class BAttachment extends AbstractBeccaEntity { if (this.role === 'image' && parentNote.type === 'text') { const origContent = parentNote.getContent(); + + if (typeof origContent !== "string") { + throw new Error(`Note with ID '${note.noteId} has a text type but non-string content.`); + } + const oldAttachmentUrl = `api/attachments/${this.attachmentId}/image/`; const newNoteUrl = `api/images/${note.noteId}/`; diff --git a/src/becca/entities/bnote.ts b/src/becca/entities/bnote.ts index 0a3fb87f1..a39d62706 100644 --- a/src/becca/entities/bnote.ts +++ b/src/becca/entities/bnote.ts @@ -216,9 +216,8 @@ class BNote extends AbstractBeccaEntity { * - changes in the note metadata or title should not trigger note content sync (so we keep separate utcDateModified and entity changes records) * - but to the user note content and title changes are one and the same - single dateModified (so all changes must go through Note and content is not a separate entity) */ - // FIXME: original declaration was (string | Buffer), but everywhere it's used as a string. - getContent(): string { - return this._getContent() as string; + getContent() { + return this._getContent(); } /** @@ -226,7 +225,7 @@ class BNote extends AbstractBeccaEntity { getJsonContent(): {} | null { const content = this.getContent(); - if (!content || !content.trim()) { + if (typeof content !== "string" || !content || !content.trim()) { return null; } @@ -243,7 +242,7 @@ class BNote extends AbstractBeccaEntity { } } - setContent(content: string, opts: ContentOpts = {}) { + setContent(content: Buffer | string, opts: ContentOpts = {}) { this._setContent(content, opts); eventService.emit(eventService.NOTE_CONTENT_CHANGE, { entity: this }); @@ -661,7 +660,7 @@ class BNote extends AbstractBeccaEntity { * @param name - relation name to filter * @returns all note's relations (attributes with type relation), including inherited ones */ - getRelations(name: string): BAttribute[] { + getRelations(name?: string): BAttribute[] { return this.getAttributes(RELATION, name); } @@ -1510,6 +1509,10 @@ class BNote extends AbstractBeccaEntity { const oldNoteUrl = `api/images/${this.noteId}/`; const newAttachmentUrl = `api/attachments/${attachment.attachmentId}/image/`; + if (typeof parentContent !== "string") { + throw new Error("Unable to convert image note into attachment because parent note does not have a string content."); + } + const fixedContent = utils.replaceAll(parentContent, oldNoteUrl, newAttachmentUrl); parentNote.setContent(fixedContent); @@ -1611,7 +1614,7 @@ class BNote extends AbstractBeccaEntity { revisionAttachment.ownerId = revision.revisionId; revisionAttachment.setContent(noteAttachment.getContent(), { forceSave: true }); - if (this.type === 'text') { + if (this.type === 'text' && typeof noteContent === "string") { // content is rewritten to point to the revision attachments noteContent = noteContent.replaceAll(`attachments/${noteAttachment.attachmentId}`, `attachments/${revisionAttachment.attachmentId}`); @@ -1654,7 +1657,6 @@ class BNote extends AbstractBeccaEntity { position }); - content = content || ""; attachment.setContent(content, {forceSave: true}); return attachment; diff --git a/src/becca/entities/rows.ts b/src/becca/entities/rows.ts index 3bd4cae67..331f150df 100644 --- a/src/becca/entities/rows.ts +++ b/src/becca/entities/rows.ts @@ -13,7 +13,7 @@ export interface AttachmentRow { utcDateModified?: string; utcDateScheduledForErasureSince?: string; contentLength?: number; - content?: string; + content?: Buffer | string; } export interface RevisionRow { @@ -69,9 +69,9 @@ export interface AttributeRow { noteId: string; type: AttributeType; name: string; - position: number; - value: string; - isInheritable: boolean; + position?: number; + value?: string; + isInheritable?: boolean; utcDateModified?: string; } @@ -79,9 +79,10 @@ export interface BranchRow { branchId?: string; noteId: string; parentNoteId: string; - prefix: string | null; - notePosition: number; - isExpanded: boolean; + prefix?: string | null; + notePosition: number | null; + isExpanded?: boolean; + isDeleted?: boolean; utcDateModified?: string; } @@ -94,13 +95,19 @@ export type NoteType = ("file" | "image" | "search" | "noteMap" | "launcher" | " export interface NoteRow { noteId: string; + deleteId: string; title: string; type: NoteType; mime: string; isProtected: boolean; + isDeleted: boolean; blobId: string; dateCreated: string; dateModified: string; utcDateCreated: string; utcDateModified: string; +} + +export interface AttributeRow { + } \ No newline at end of file diff --git a/src/errors/validation_error.ts b/src/errors/validation_error.ts index 0cabecc8e..8b872bcbe 100644 --- a/src/errors/validation_error.ts +++ b/src/errors/validation_error.ts @@ -6,4 +6,4 @@ class ValidationError { } } -module.exports = ValidationError; \ No newline at end of file +export = ValidationError; \ No newline at end of file diff --git a/src/etapi/notes.js b/src/etapi/notes.js index 76318a89d..0d96468d4 100644 --- a/src/etapi/notes.js +++ b/src/etapi/notes.js @@ -2,7 +2,7 @@ const becca = require('../becca/becca'); const utils = require('../services/utils'); const eu = require('./etapi_utils'); const mappers = require('./mappers.js'); -const noteService = require('../services/notes.js'); +const noteService = require('../services/notes'); const TaskContext = require('../services/task_context'); const v = require('./validators.js'); const searchService = require('../services/search/services/search.js'); diff --git a/src/routes/api/clipper.js b/src/routes/api/clipper.js index 1b8bee19b..b5cda8c31 100644 --- a/src/routes/api/clipper.js +++ b/src/routes/api/clipper.js @@ -2,7 +2,7 @@ const attributeService = require('../../services/attributes.js'); const cloneService = require('../../services/cloning.js'); -const noteService = require('../../services/notes.js'); +const noteService = require('../../services/notes'); const dateNoteService = require('../../services/date_notes.js'); const dateUtils = require('../../services/date_utils'); const imageService = require('../../services/image.js'); diff --git a/src/routes/api/files.js b/src/routes/api/files.js index 5e0c391d6..f368383d5 100644 --- a/src/routes/api/files.js +++ b/src/routes/api/files.js @@ -3,7 +3,7 @@ const protectedSessionService = require('../../services/protected_session'); const utils = require('../../services/utils'); const log = require('../../services/log'); -const noteService = require('../../services/notes.js'); +const noteService = require('../../services/notes'); const tmp = require('tmp'); const fs = require('fs'); const { Readable } = require('stream'); diff --git a/src/routes/api/notes.js b/src/routes/api/notes.js index 7c8ccf773..4f3f89176 100644 --- a/src/routes/api/notes.js +++ b/src/routes/api/notes.js @@ -1,6 +1,6 @@ "use strict"; -const noteService = require('../../services/notes.js'); +const noteService = require('../../services/notes'); const eraseService = require('../../services/erase'); const treeService = require('../../services/tree.js'); const sql = require('../../services/sql'); diff --git a/src/routes/api/recent_changes.js b/src/routes/api/recent_changes.js index 6fea4a105..44e964ecf 100644 --- a/src/routes/api/recent_changes.js +++ b/src/routes/api/recent_changes.js @@ -2,7 +2,7 @@ const sql = require('../../services/sql'); const protectedSessionService = require('../../services/protected_session'); -const noteService = require('../../services/notes.js'); +const noteService = require('../../services/notes'); const becca = require('../../becca/becca'); function getRecentChanges(req) { diff --git a/src/routes/api/sender.js b/src/routes/api/sender.js index 000d1eecb..3c405ecc1 100644 --- a/src/routes/api/sender.js +++ b/src/routes/api/sender.js @@ -2,7 +2,7 @@ const imageType = require('image-type'); const imageService = require('../../services/image.js'); -const noteService = require('../../services/notes.js'); +const noteService = require('../../services/notes'); const {sanitizeAttributeName} = require('../../services/sanitize_attribute_name'); const specialNotesService = require('../../services/special_notes.js'); diff --git a/src/services/backend_script_api.js b/src/services/backend_script_api.js index 66408f367..d8687b944 100644 --- a/src/services/backend_script_api.js +++ b/src/services/backend_script_api.js @@ -1,5 +1,5 @@ const log = require('./log'); -const noteService = require('./notes.js'); +const noteService = require('./notes'); const sql = require('./sql'); const utils = require('./utils'); const attributeService = require('./attributes.js'); diff --git a/src/services/date_notes.js b/src/services/date_notes.js index 5d74b2f47..628ed886f 100644 --- a/src/services/date_notes.js +++ b/src/services/date_notes.js @@ -1,6 +1,6 @@ "use strict"; -const noteService = require('./notes.js'); +const noteService = require('./notes'); const attributeService = require('./attributes.js'); const dateUtils = require('./date_utils'); const sql = require('./sql'); diff --git a/src/services/date_utils.ts b/src/services/date_utils.ts index 31c58c2e4..82347504c 100644 --- a/src/services/date_utils.ts +++ b/src/services/date_utils.ts @@ -65,7 +65,7 @@ function getDateTimeForFile() { return new Date().toISOString().substr(0, 19).replace(/:/g, ''); } -function validateLocalDateTime(str: string) { +function validateLocalDateTime(str: string | null | undefined) { if (!str) { return; } @@ -80,7 +80,7 @@ function validateLocalDateTime(str: string) { } } -function validateUtcDateTime(str: string) { +function validateUtcDateTime(str?: string) { if (!str) { return; } diff --git a/src/services/entity_changes.ts b/src/services/entity_changes.ts index bc75432b0..91f84f834 100644 --- a/src/services/entity_changes.ts +++ b/src/services/entity_changes.ts @@ -45,7 +45,7 @@ function putEntityChange(origEntityChange: EntityChange) { cls.putEntityChange(ec); } -function putNoteReorderingEntityChange(parentNoteId: string, componentId: string) { +function putNoteReorderingEntityChange(parentNoteId: string, componentId?: string) { putEntityChange({ entityName: "note_reordering", entityId: parentNoteId, diff --git a/src/services/handlers.js b/src/services/handlers.js index 1a9c8e353..a24e28dd6 100644 --- a/src/services/handlers.js +++ b/src/services/handlers.js @@ -1,10 +1,10 @@ const eventService = require('./events'); const scriptService = require('./script.js'); const treeService = require('./tree.js'); -const noteService = require('./notes.js'); +const noteService = require('./notes'); const becca = require('../becca/becca'); const BAttribute = require('../becca/entities/battribute'); -const hiddenSubtreeService = require('./hidden_subtree.js'); +const hiddenSubtreeService = require('./hidden_subtree'); const oneTimeTimer = require('./one_time_timer.js'); function runAttachedRelations(note, relationName, originEntity) { diff --git a/src/services/hidden_subtree.js b/src/services/hidden_subtree.ts similarity index 93% rename from src/services/hidden_subtree.js rename to src/services/hidden_subtree.ts index a2f6ec7bb..5478aed1c 100644 --- a/src/services/hidden_subtree.js +++ b/src/services/hidden_subtree.ts @@ -1,8 +1,10 @@ -const becca = require('../becca/becca'); -const noteService = require('./notes.js'); -const BAttribute = require('../becca/entities/battribute'); -const log = require('./log'); -const migrationService = require('./migration'); +import BAttribute = require("../becca/entities/battribute"); +import { AttributeType, NoteType } from "../becca/entities/rows"; + +import becca = require('../becca/becca'); +import noteService = require('./notes'); +import log = require('./log'); +import migrationService = require('./migration'); const LBTPL_ROOT = "_lbTplRoot"; const LBTPL_BASE = "_lbTplBase"; @@ -13,13 +15,36 @@ const LBTPL_BUILTIN_WIDGET = "_lbTplBuiltinWidget"; const LBTPL_SPACER = "_lbTplSpacer"; const LBTPL_CUSTOM_WIDGET = "_lbTplCustomWidget"; +interface Attribute { + type: AttributeType; + name: string; + isInheritable?: boolean; + value?: string +} + +interface Item { + notePosition?: number; + id: string; + title: string; + type: NoteType; + icon?: string; + attributes?: Attribute[]; + children?: Item[]; + isExpanded?: boolean; + baseSize?: string; + growthFactor?: string; + targetNoteId?: "_backendLog" | "_globalNoteMap"; + builtinWidget?: "bookmarks" | "spacer" | "backInHistoryButton" | "forwardInHistoryButton" | "syncStatus" | "protectedSession" | "todayInJournal" | "calendar"; + command?: "jumpToNote" | "searchNotes" | "createNoteIntoInbox" | "showRecentChanges"; +} + /* * Hidden subtree is generated as a "predictable structure" which means that it avoids generating random IDs to always * produce the same structure. This is needed because it is run on multiple instances in the sync cluster which might produce * duplicate subtrees. This way, all instances will generate the same structure with the same IDs. */ -const HIDDEN_SUBTREE_DEFINITION = { +const HIDDEN_SUBTREE_DEFINITION: Item = { id: '_hidden', title: 'Hidden Notes', type: 'doc', @@ -244,7 +269,7 @@ function checkHiddenSubtree(force = false) { checkHiddenSubtreeRecursively('root', HIDDEN_SUBTREE_DEFINITION); } -function checkHiddenSubtreeRecursively(parentNoteId, item) { +function checkHiddenSubtreeRecursively(parentNoteId: string, item: Item) { if (!item.id || !item.type || !item.title) { throw new Error(`Item does not contain mandatory properties: ${JSON.stringify(item)}`); } @@ -337,7 +362,7 @@ function checkHiddenSubtreeRecursively(parentNoteId, item) { } } -module.exports = { +export = { checkHiddenSubtree, LBTPL_ROOT, LBTPL_BASE, diff --git a/src/services/html_sanitizer.ts b/src/services/html_sanitizer.ts index 6e7d8b537..8e82edd3c 100644 --- a/src/services/html_sanitizer.ts +++ b/src/services/html_sanitizer.ts @@ -47,7 +47,7 @@ function sanitize(dirtyHtml: string) { }); } -module.exports = { +export = { sanitize, sanitizeUrl: (url: string) => { return sanitizeUrl.sanitizeUrl(url).trim(); diff --git a/src/services/image.js b/src/services/image.js index 8d7aec5c8..26c2fbdd3 100644 --- a/src/services/image.js +++ b/src/services/image.js @@ -3,7 +3,7 @@ const becca = require('../becca/becca'); const log = require('./log'); const protectedSessionService = require('./protected_session'); -const noteService = require('./notes.js'); +const noteService = require('./notes'); const optionService = require('./options'); const sql = require('./sql'); const jimp = require('jimp'); @@ -154,7 +154,7 @@ function saveImageToAttachment(noteId, uploadBuffer, originalName, shrinkImageSw setTimeout(() => { sql.transactional(() => { const note = becca.getNoteOrThrow(noteId); - const noteService = require('../services/notes.js'); + 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.js b/src/services/import/enex.js index 80903d9f6..c8379b951 100644 --- a/src/services/import/enex.js +++ b/src/services/import/enex.js @@ -4,7 +4,7 @@ const {Throttle} = require('stream-throttle'); const log = require('../log'); const utils = require('../utils'); const sql = require('../sql'); -const noteService = require('../notes.js'); +const noteService = require('../notes'); const imageService = require('../image.js'); const protectedSessionService = require('../protected_session'); const htmlSanitizer = require('../html_sanitizer'); diff --git a/src/services/import/opml.js b/src/services/import/opml.js index f71b41fbd..2b99dbd85 100644 --- a/src/services/import/opml.js +++ b/src/services/import/opml.js @@ -1,6 +1,6 @@ "use strict"; -const noteService = require('../../services/notes.js'); +const noteService = require('../../services/notes'); const parseString = require('xml2js').parseString; const protectedSessionService = require('../protected_session'); const htmlSanitizer = require('../html_sanitizer'); diff --git a/src/services/import/single.js b/src/services/import/single.js index b516a50e9..ec7aa735d 100644 --- a/src/services/import/single.js +++ b/src/services/import/single.js @@ -1,6 +1,6 @@ "use strict"; -const noteService = require('../../services/notes.js'); +const noteService = require('../../services/notes'); const imageService = require('../../services/image.js'); const protectedSessionService = require('../protected_session'); const markdownService = require('./markdown.js'); diff --git a/src/services/import/zip.js b/src/services/import/zip.js index db8565037..ee7a828d1 100644 --- a/src/services/import/zip.js +++ b/src/services/import/zip.js @@ -3,7 +3,7 @@ const BAttribute = require('../../becca/entities/battribute'); const utils = require('../../services/utils'); const log = require('../../services/log'); -const noteService = require('../../services/notes.js'); +const noteService = require('../../services/notes'); const attributeService = require('../../services/attributes.js'); const BBranch = require('../../becca/entities/bbranch'); const path = require('path'); diff --git a/src/services/notes.js b/src/services/notes.ts similarity index 81% rename from src/services/notes.js rename to src/services/notes.ts index 3b662e831..c03c96a1e 100644 --- a/src/services/notes.js +++ b/src/services/notes.ts @@ -1,52 +1,62 @@ -const sql = require('./sql'); -const optionService = require('./options'); -const dateUtils = require('./date_utils'); -const entityChangesService = require('./entity_changes'); -const eventService = require('./events'); -const cls = require('../services/cls'); -const protectedSessionService = require('../services/protected_session'); -const log = require('../services/log'); -const utils = require('../services/utils'); -const revisionService = require('./revisions'); -const request = require('./request'); -const path = require('path'); -const url = require('url'); -const becca = require('../becca/becca'); -const BBranch = require('../becca/entities/bbranch'); -const BNote = require('../becca/entities/bnote'); -const BAttribute = require('../becca/entities/battribute'); -const BAttachment = require('../becca/entities/battachment'); -const dayjs = require("dayjs"); -const htmlSanitizer = require('./html_sanitizer'); -const ValidationError = require('../errors/validation_error'); -const noteTypesService = require('./note_types'); -const fs = require("fs"); -const ws = require('./ws'); -const html2plaintext = require('html2plaintext') +import sql = require('./sql'); +import optionService = require('./options'); +import dateUtils = require('./date_utils'); +import entityChangesService = require('./entity_changes'); +import eventService = require('./events'); +import cls = require('../services/cls'); +import protectedSessionService = require('../services/protected_session'); +import log = require('../services/log'); +import utils = require('../services/utils'); +import revisionService = require('./revisions'); +import request = require('./request'); +import path = require('path'); +import url = require('url'); +import becca = require('../becca/becca'); +import BBranch = require('../becca/entities/bbranch'); +import BNote = require('../becca/entities/bnote'); +import BAttribute = require('../becca/entities/battribute'); +import BAttachment = require('../becca/entities/battachment'); +import dayjs = require("dayjs"); +import htmlSanitizer = require('./html_sanitizer'); +import ValidationError = require('../errors/validation_error'); +import noteTypesService = require('./note_types'); +import fs = require("fs"); +import ws = require('./ws'); +import html2plaintext = require('html2plaintext'); +import { AttachmentRow, AttributeRow, BranchRow, NoteRow, NoteType } from '../becca/entities/rows'; +import TaskContext = require('./task_context'); -/** @param {BNote} parentNote */ -function getNewNotePosition(parentNote) { +interface FoundLink { + name: "imageLink" | "internalLink" | "includeNoteLink" | "relationMapLink", + value: string +} + +interface Attachment { + attachmentId?: string; + title: string; +} + +function getNewNotePosition(parentNote: BNote) { if (parentNote.isLabelTruthy('newNotesOnTop')) { const minNotePos = parentNote.getChildBranches() - .filter(branch => branch.noteId !== '_hidden') // has "always last" note position - .reduce((min, note) => Math.min(min, note.notePosition), 0); + .filter(branch => branch?.noteId !== '_hidden') // has "always last" note position + .reduce((min, note) => Math.min(min, note?.notePosition || 0), 0); return minNotePos - 10; } else { const maxNotePos = parentNote.getChildBranches() - .filter(branch => branch.noteId !== '_hidden') // has "always last" note position - .reduce((max, note) => Math.max(max, note.notePosition), 0); + .filter(branch => branch?.noteId !== '_hidden') // has "always last" note position + .reduce((max, note) => Math.max(max, note?.notePosition || 0), 0); return maxNotePos + 10; } } -/** @param {BNote} note */ -function triggerNoteTitleChanged(note) { +function triggerNoteTitleChanged(note: BNote) { eventService.emit(eventService.NOTE_TITLE_CHANGED, note); } -function deriveMime(type, mime) { +function deriveMime(type: string, mime?: string) { if (!type) { throw new Error(`Note type is a required param`); } @@ -58,11 +68,7 @@ function deriveMime(type, mime) { return noteTypesService.getDefaultMimeForNoteType(type); } -/** - * @param {BNote} parentNote - * @param {BNote} childNote - */ -function copyChildAttributes(parentNote, childNote) { +function copyChildAttributes(parentNote: BNote, childNote: BNote) { for (const attr of parentNote.getAttributes()) { if (attr.name.startsWith("child:")) { const name = attr.name.substr(6); @@ -86,8 +92,7 @@ function copyChildAttributes(parentNote, childNote) { } } -/** @param {BNote} parentNote */ -function getNewNoteTitle(parentNote) { +function getNewNoteTitle(parentNote: BNote) { let title = "new note"; const titleTemplate = parentNote.getLabelValue('titleTemplate'); @@ -101,7 +106,7 @@ function getNewNoteTitle(parentNote) { // - parentNote title = eval(`\`${titleTemplate}\``); - } catch (e) { + } catch (e: any) { log.error(`Title template of note '${parentNote.noteId}' failed with: ${e.message}`); } } @@ -114,7 +119,13 @@ function getNewNoteTitle(parentNote) { return title; } -function getAndValidateParent(params) { +interface GetValidateParams { + parentNoteId: string; + type: string; + ignoreForbiddenParents?: boolean; +} + +function getAndValidateParent(params: GetValidateParams) { const parentNote = becca.notes[params.parentNoteId]; if (!parentNote) { @@ -141,24 +152,33 @@ function getAndValidateParent(params) { return parentNote; } -/** - * Following object properties are mandatory: - * - {string} parentNoteId - * - {string} title - * - {*} content - * - {string} type - text, code, file, image, search, book, relationMap, canvas, render - * - * The following are optional (have defaults) - * - {string} mime - value is derived from default mimes for type - * - {boolean} isProtected - default is false - * - {boolean} isExpanded - default is false - * - {string} prefix - default is empty string - * - {int} notePosition - default is the last existing notePosition in a parent + 10 - * - * @param params - * @returns {{note: BNote, branch: BBranch}} - */ -function createNewNote(params) { +interface NoteParams { + /** optionally can force specific noteId */ + noteId?: string; + parentNoteId: string; + templateNoteId?: string; + title: string; + content: string; + type: NoteType; + /** default value is derived from default mimes for type */ + mime?: string; + /** default is false */ + isProtected?: boolean; + /** default is false */ + isExpanded?: boolean; + /** default is empty string */ + prefix?: string; + /** default is the last existing notePosition in a parent + 10 */ + notePosition?: number; + dateCreated?: string; + utcDateCreated?: string; + ignoreForbiddenParents?: boolean; +} + +function createNewNote(params: NoteParams): { + note: BNote; + branch: BBranch; +} { const parentNote = getAndValidateParent(params); if (params.title === null || params.title === undefined) { @@ -209,7 +229,7 @@ function createNewNote(params) { noteId: note.noteId, parentNoteId: params.parentNoteId, notePosition: params.notePosition !== undefined ? params.notePosition : getNewNotePosition(parentNote), - prefix: params.prefix, + prefix: params.prefix || "", isExpanded: !!params.isExpanded }).save(); } @@ -253,7 +273,7 @@ function createNewNote(params) { }); } -function createNewNoteWithTarget(target, targetBranchId, params) { +function createNewNoteWithTarget(target: ("into" | "after"), targetBranchId: string, params: NoteParams) { if (!params.type) { const parentNote = becca.notes[params.parentNoteId]; @@ -285,13 +305,7 @@ function createNewNoteWithTarget(target, targetBranchId, params) { } } -/** - * @param {BNote} note - * @param {boolean} protect - * @param {boolean} includingSubTree - * @param {TaskContext} taskContext - */ -function protectNoteRecursively(note, protect, includingSubTree, taskContext) { +function protectNoteRecursively(note: BNote, protect: boolean, includingSubTree: boolean, taskContext: TaskContext) { protectNote(note, protect); taskContext.increaseProgressCount(); @@ -303,11 +317,7 @@ function protectNoteRecursively(note, protect, includingSubTree, taskContext) { } } -/** - * @param {BNote} note - * @param {boolean} protect - */ -function protectNote(note, protect) { +function protectNote(note: BNote, protect: boolean) { if (!protectedSessionService.isProtectedSessionAvailable()) { throw new Error(`Cannot (un)protect note '${note.noteId}' with protect flag '${protect}' without active protected session`); } @@ -345,8 +355,8 @@ function protectNote(note, protect) { } } -function checkImageAttachments(note, content) { - const foundAttachmentIds = new Set(); +function checkImageAttachments(note: BNote, content: string) { + const foundAttachmentIds = new Set(); let match; const imgRegExp = /src="[^"]*api\/attachments\/([a-zA-Z0-9_]+)\/image/g; @@ -362,7 +372,7 @@ function checkImageAttachments(note, content) { const attachments = note.getAttachments(); for (const attachment of attachments) { - const attachmentInContent = foundAttachmentIds.has(attachment.attachmentId); + const attachmentInContent = attachment.attachmentId && foundAttachmentIds.has(attachment.attachmentId); if (attachment.utcDateScheduledForErasureSince && attachmentInContent) { attachment.utcDateScheduledForErasureSince = null; @@ -373,7 +383,7 @@ function checkImageAttachments(note, content) { } } - const existingAttachmentIds = new Set(attachments.map(att => att.attachmentId)); + const existingAttachmentIds = new Set(attachments.map(att => att.attachmentId)); const unknownAttachmentIds = Array.from(foundAttachmentIds).filter(foundAttId => !existingAttachmentIds.has(foundAttId)); const unknownAttachments = becca.getAttachments(unknownAttachmentIds); @@ -412,7 +422,7 @@ function checkImageAttachments(note, content) { }; } -function findImageLinks(content, foundLinks) { +function findImageLinks(content: string, foundLinks: FoundLink[]) { const re = /src="[^"]*api\/images\/([a-zA-Z0-9_]+)\//g; let match; @@ -428,7 +438,7 @@ function findImageLinks(content, foundLinks) { return content.replace(/src="[^"]*\/api\/images\//g, 'src="api/images/'); } -function findInternalLinks(content, foundLinks) { +function findInternalLinks(content: string, foundLinks: FoundLink[]) { const re = /href="[^"]*#root[a-zA-Z0-9_\/]*\/([a-zA-Z0-9_]+)\/?"/g; let match; @@ -443,7 +453,7 @@ function findInternalLinks(content, foundLinks) { return content.replace(/href="[^"]*#root/g, 'href="#root'); } -function findIncludeNoteLinks(content, foundLinks) { +function findIncludeNoteLinks(content: string, foundLinks: FoundLink[]) { const re = /
]*>/g; let match; @@ -457,7 +467,7 @@ function findIncludeNoteLinks(content, foundLinks) { return content; } -function findRelationMapLinks(content, foundLinks) { +function findRelationMapLinks(content: string, foundLinks: FoundLink[]) { const obj = JSON.parse(content); for (const note of obj.notes) { @@ -468,9 +478,9 @@ function findRelationMapLinks(content, foundLinks) { } } -const imageUrlToAttachmentIdMapping = {}; +const imageUrlToAttachmentIdMapping: Record = {}; -async function downloadImage(noteId, imageUrl) { +async function downloadImage(noteId: string, imageUrl: string) { const unescapedUrl = utils.unescapeHtml(imageUrl); try { @@ -493,7 +503,7 @@ async function downloadImage(noteId, imageUrl) { } const parsedUrl = url.parse(unescapedUrl); - const title = path.basename(parsedUrl.pathname); + const title = path.basename(parsedUrl.pathname || ""); const imageService = require('../services/image.js'); const attachment = imageService.saveImageToAttachment(noteId, imageBuffer, title, true, true); @@ -502,21 +512,21 @@ async function downloadImage(noteId, imageUrl) { log.info(`Download of '${imageUrl}' succeeded and was saved as image attachment '${attachment.attachmentId}' of note '${noteId}'`); } - catch (e) { + catch (e: any) { log.error(`Download of '${imageUrl}' for note '${noteId}' failed with error: ${e.message} ${e.stack}`); } } /** url => download promise */ -const downloadImagePromises = {}; +const downloadImagePromises: Record> = {}; -function replaceUrl(content, url, attachment) { +function replaceUrl(content: string, url: string, attachment: Attachment) { const quotedUrl = utils.quoteRegex(url); return content.replace(new RegExp(`\\s+src=[\"']${quotedUrl}[\"']`, "ig"), ` src="api/attachments/${attachment.attachmentId}/image/${encodeURIComponent(attachment.title)}"`); } -function downloadImages(noteId, content) { +function downloadImages(noteId: string, content: string) { const imageRe = /]*?\ssrc=['"]([^'">]+)['"]/ig; let imageMatch; @@ -589,6 +599,11 @@ function downloadImages(noteId, content) { const origContent = origNote.getContent(); let updatedContent = origContent; + if (typeof updatedContent !== "string") { + log.error(`Note '${noteId}' has a non-string content, cannot replace image link.`); + return; + } + for (const url in imageUrlToAttachmentIdMapping) { const imageNote = imageNotes.find(note => note.noteId === imageUrlToAttachmentIdMapping[url]); @@ -612,11 +627,7 @@ function downloadImages(noteId, content) { return content; } -/** - * @param {BNote} note - * @param {string} content - */ -function saveAttachments(note, content) { +function saveAttachments(note: BNote, content: string) { const inlineAttachmentRe = /]*?\shref=['"]data:([^;'">]+);base64,([^'">]+)['"][^>]*>(.*?)<\/a>/igm; let attachmentMatch; @@ -645,11 +656,7 @@ function saveAttachments(note, content) { return content; } -/** - * @param {BNote} note - * @param {string} content - */ -function saveLinks(note, content) { +function saveLinks(note: BNote, content: string) { if ((note.type !== 'text' && note.type !== 'relationMap') || (note.isProtected && !protectedSessionService.isProtectedSessionAvailable())) { return { @@ -658,7 +665,7 @@ function saveLinks(note, content) { }; } - const foundLinks = []; + const foundLinks: FoundLink[] = []; let forceFrontendReload = false; if (note.type === 'text') { @@ -716,8 +723,7 @@ function saveLinks(note, content) { return { forceFrontendReload, content }; } -/** @param {BNote} note */ -function saveRevisionIfNeeded(note) { +function saveRevisionIfNeeded(note: BNote) { // files and images are versioned separately if (note.type === 'file' || note.type === 'image' || note.isLabelTruthy('disableVersioning')) { return; @@ -738,10 +744,10 @@ function saveRevisionIfNeeded(note) { } } -function updateNoteData(noteId, content, attachments = []) { +function updateNoteData(noteId: string, content: string, attachments: BAttachment[] = []) { const note = becca.getNote(noteId); - if (!note.isContentAvailable()) { + if (!note || !note.isContentAvailable()) { throw new Error(`Note '${noteId}' is not available for change!`); } @@ -752,10 +758,13 @@ function updateNoteData(noteId, content, attachments = []) { note.setContent(newContent, { forceFrontendReload }); if (attachments?.length > 0) { - /** @var {Object} */ const existingAttachmentsByTitle = utils.toMap(note.getAttachments({includeContentLength: false}), 'title'); - for (const {attachmentId, role, mime, title, content, position} of attachments) { + for (const attachment of attachments) { + // FIXME: The content property was extracted directly instead of `getContent`. To investigate. + const {attachmentId, role, mime, title, position} = attachment; + const content = attachment.getContent(); + if (attachmentId || !(title in existingAttachmentsByTitle)) { note.saveAttachment({attachmentId, role, mime, title, content, position}); } else { @@ -769,12 +778,8 @@ function updateNoteData(noteId, content, attachments = []) { } } -/** - * @param {string} noteId - * @param {TaskContext} taskContext - */ -function undeleteNote(noteId, taskContext) { - const noteRow = sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); +function undeleteNote(noteId: string, taskContext: TaskContext) { + const noteRow = sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); if (!noteRow.isDeleted) { log.error(`Note '${noteId}' is not deleted and thus cannot be undeleted.`); @@ -793,19 +798,14 @@ function undeleteNote(noteId, taskContext) { } } -/** - * @param {string} branchId - * @param {string} deleteId - * @param {TaskContext} taskContext - */ -function undeleteBranch(branchId, deleteId, taskContext) { - const branchRow = sql.getRow("SELECT * FROM branches WHERE branchId = ?", [branchId]) +function undeleteBranch(branchId: string, deleteId: string, taskContext: TaskContext) { + const branchRow = sql.getRow("SELECT * FROM branches WHERE branchId = ?", [branchId]) if (!branchRow.isDeleted) { return; } - const noteRow = sql.getRow("SELECT * FROM notes WHERE noteId = ?", [branchRow.noteId]); + const noteRow = sql.getRow("SELECT * FROM notes WHERE noteId = ?", [branchRow.noteId]); if (noteRow.isDeleted && noteRow.deleteId !== deleteId) { return; @@ -818,10 +818,14 @@ function undeleteBranch(branchId, deleteId, taskContext) { if (noteRow.isDeleted && noteRow.deleteId === deleteId) { // becca entity was already created as skeleton in "new Branch()" above const noteEntity = becca.getNote(noteRow.noteId); + if (!noteEntity) { + throw new Error("Unable to find the just restored branch."); + } + noteEntity.updateFromRow(noteRow); noteEntity.save(); - const attributeRows = sql.getRows(` + const attributeRows = sql.getRows(` SELECT * FROM attributes WHERE isDeleted = 1 AND deleteId = ? @@ -830,10 +834,11 @@ function undeleteBranch(branchId, deleteId, taskContext) { for (const attributeRow of attributeRows) { // relation might point to a note which hasn't been undeleted yet and would thus throw up + // FIXME: skipValidation is not used. new BAttribute(attributeRow).save({skipValidation: true}); } - const attachmentRows = sql.getRows(` + const attachmentRows = sql.getRows(` SELECT * FROM attachments WHERE isDeleted = 1 AND deleteId = ? @@ -843,7 +848,7 @@ function undeleteBranch(branchId, deleteId, taskContext) { new BAttachment(attachmentRow).save(); } - const childBranchIds = sql.getColumn(` + const childBranchIds = sql.getColumn(` SELECT branches.branchId FROM branches WHERE branches.isDeleted = 1 @@ -859,8 +864,8 @@ function undeleteBranch(branchId, deleteId, taskContext) { /** * @returns return deleted branchIds of an undeleted parent note */ -function getUndeletedParentBranchIds(noteId, deleteId) { - return sql.getColumn(` +function getUndeletedParentBranchIds(noteId: string, deleteId: string) { + return sql.getColumn(` SELECT branches.branchId FROM branches JOIN notes AS parentNote ON parentNote.noteId = branches.parentNoteId @@ -870,7 +875,7 @@ function getUndeletedParentBranchIds(noteId, deleteId) { AND parentNote.isDeleted = 0`, [noteId, deleteId]); } -function scanForLinks(note, content) { +function scanForLinks(note: BNote | null, content: string) { if (!note || !['text', 'relationMap'].includes(note.type)) { return; } @@ -884,17 +889,15 @@ function scanForLinks(note, content) { } }); } - catch (e) { + catch (e: any) { log.error(`Could not scan for links note '${note.noteId}': ${e.message} ${e.stack}`); } } /** - * @param {BNote} note - * @param {string} content * Things which have to be executed after updating content, but asynchronously (separate transaction) */ -async function asyncPostProcessContent(note, content) { +async function asyncPostProcessContent(note: BNote, content: string) { if (cls.isMigrationRunning()) { // this is rarely needed for migrations, but can cause trouble by e.g. triggering downloads return; @@ -908,7 +911,7 @@ async function asyncPostProcessContent(note, content) { } // all keys should be replaced by the corresponding values -function replaceByMap(str, mapObj) { +function replaceByMap(str: string, mapObj: Record) { if (!mapObj) { return str; } @@ -918,7 +921,7 @@ function replaceByMap(str, mapObj) { return str.replace(re, matched => mapObj[matched]); } -function duplicateSubtree(origNoteId, newParentNoteId) { +function duplicateSubtree(origNoteId: string, newParentNoteId: string) { if (origNoteId === 'root') { throw new Error('Duplicating root is not possible'); } @@ -931,6 +934,10 @@ function duplicateSubtree(origNoteId, newParentNoteId) { const noteIdMapping = getNoteIdMapping(origNote); + if (!origBranch) { + throw new Error("Unable to find original branch to duplicate."); + } + const res = duplicateSubtreeInner(origNote, origBranch, newParentNoteId, noteIdMapping); if (!res.note.title.endsWith('(dup)')) { @@ -942,20 +949,25 @@ function duplicateSubtree(origNoteId, newParentNoteId) { return res; } -function duplicateSubtreeWithoutRoot(origNoteId, newNoteId) { +function duplicateSubtreeWithoutRoot(origNoteId: string, newNoteId: string) { if (origNoteId === 'root') { throw new Error('Duplicating root is not possible'); } const origNote = becca.getNote(origNoteId); + if (origNote == null) { + throw new Error("Unable to find note to duplicate."); + } + const noteIdMapping = getNoteIdMapping(origNote); - for (const childBranch of origNote.getChildBranches()) { - duplicateSubtreeInner(childBranch.getNote(), childBranch, newNoteId, noteIdMapping); + if (childBranch) { + duplicateSubtreeInner(childBranch.getNote(), childBranch, newNoteId, noteIdMapping); + } } } -function duplicateSubtreeInner(origNote, origBranch, newParentNoteId, noteIdMapping) { +function duplicateSubtreeInner(origNote: BNote, origBranch: BBranch, newParentNoteId: string, noteIdMapping: Record) { if (origNote.isProtected && !protectedSessionService.isProtectedSessionAvailable()) { throw new Error(`Cannot duplicate note '${origNote.noteId}' because it is protected and protected session is not available. Enter protected session and try again.`); } @@ -981,7 +993,7 @@ function duplicateSubtreeInner(origNote, origBranch, newParentNoteId, noteIdMapp let content = origNote.getContent(); - if (['text', 'relationMap', 'search'].includes(origNote.type)) { + if (typeof content === "string" && ['text', 'relationMap', 'search'].includes(origNote.type)) { // fix links in the content content = replaceByMap(content, noteIdMapping); } @@ -1002,11 +1014,14 @@ function duplicateSubtreeInner(origNote, origBranch, newParentNoteId, noteIdMapp } // the relation targets may not be created yet, the mapping is pre-generated - attr.save({skipValidation: true}); + // FIXME: This used to be `attr.save({skipValidation: true});`, but skipValidation is in beforeSaving. + attr.save(); } for (const childBranch of origNote.getChildBranches()) { - duplicateSubtreeInner(childBranch.getNote(), childBranch, newNote.noteId, noteIdMapping); + if (childBranch) { + duplicateSubtreeInner(childBranch.getNote(), childBranch, newNote.noteId, noteIdMapping); + } } return newNote; @@ -1031,8 +1046,8 @@ function duplicateSubtreeInner(origNote, origBranch, newParentNoteId, noteIdMapp } } -function getNoteIdMapping(origNote) { - const noteIdMapping = {}; +function getNoteIdMapping(origNote: BNote) { + const noteIdMapping: Record = {}; // pregenerate new noteIds since we'll need to fix relation references even for not yet created notes for (const origNoteId of origNote.getDescendantNoteIds()) { @@ -1042,7 +1057,7 @@ function getNoteIdMapping(origNote) { return noteIdMapping; } -module.exports = { +export = { createNewNote, createNewNoteWithTarget, updateNoteData, diff --git a/src/services/revisions.ts b/src/services/revisions.ts index 62ba45cd8..9cd281c13 100644 --- a/src/services/revisions.ts +++ b/src/services/revisions.ts @@ -44,6 +44,6 @@ function protectRevisions(note: BNote) { } } -module.exports = { +export = { protectRevisions }; diff --git a/src/services/scheduler.js b/src/services/scheduler.js index 154671bfa..d4f40cfd7 100644 --- a/src/services/scheduler.js +++ b/src/services/scheduler.js @@ -5,7 +5,7 @@ const config = require('./config'); const log = require('./log'); const attributeService = require('../services/attributes.js'); const protectedSessionService = require('../services/protected_session'); -const hiddenSubtreeService = require('./hidden_subtree.js'); +const hiddenSubtreeService = require('./hidden_subtree'); /** * @param {BNote} note diff --git a/src/services/special_notes.js b/src/services/special_notes.js index fd229a876..749f43325 100644 --- a/src/services/special_notes.js +++ b/src/services/special_notes.js @@ -1,13 +1,13 @@ const attributeService = require('./attributes.js'); const dateNoteService = require('./date_notes.js'); const becca = require('../becca/becca'); -const noteService = require('./notes.js'); +const noteService = require('./notes'); const dateUtils = require('./date_utils'); const log = require('./log'); const hoistedNoteService = require('./hoisted_note.js'); const searchService = require('./search/services/search.js'); const SearchContext = require('./search/search_context.js'); -const {LBTPL_NOTE_LAUNCHER, LBTPL_CUSTOM_WIDGET, LBTPL_SPACER, LBTPL_SCRIPT} = require('./hidden_subtree.js'); +const {LBTPL_NOTE_LAUNCHER, LBTPL_CUSTOM_WIDGET, LBTPL_SPACER, LBTPL_SCRIPT} = require('./hidden_subtree'); function getInboxNote(date) { const workspaceNote = hoistedNoteService.getWorkspaceNote(); diff --git a/src/tools/generate_document.js b/src/tools/generate_document.js index 9f66e76c9..503fdc6ee 100644 --- a/src/tools/generate_document.js +++ b/src/tools/generate_document.js @@ -5,7 +5,7 @@ require('../becca/entity_constructor'); const sqlInit = require('../services/sql_init'); -const noteService = require('../services/notes.js'); +const noteService = require('../services/notes'); const attributeService = require('../services/attributes.js'); const cls = require('../services/cls'); const cloningService = require('../services/cloning.js'); diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 000000000..c4ccea844 --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1,14 @@ +/* + * This file contains type definitions for libraries that did not have one + * in its library or in `@types/*` packages. + */ + +declare module 'unescape' { + function unescape(str: string, type?: string): string; + export = unescape; +} + +declare module 'html2plaintext' { + function html2plaintext(htmlText: string): string; + export = html2plaintext; +} \ No newline at end of file diff --git a/src/types/unescape.d.ts b/src/types/unescape.d.ts deleted file mode 100644 index 465ebd9e9..000000000 --- a/src/types/unescape.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module 'unescape' { - function unescape(str: string, type?: string): string; - export = unescape; -} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 9b697666a..bfe575059 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,6 @@ "files": true }, "files": [ - "src/types/unescape.d.ts" + "src/types.d.ts" ] }