From 15dee4b9525197f0f5bad3af4fdc7e0a8a14857c Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 4 Apr 2024 22:00:20 +0300 Subject: [PATCH] server-ts: Convert services/backend_script_api --- src/becca/entities/bnote.ts | 2 +- ...nd_script_api.js => backend_script_api.ts} | 849 +++++++++--------- src/services/date_notes.ts | 4 +- src/services/import/zip.ts | 4 +- src/services/note-interface.ts | 2 + src/services/notes.ts | 26 +- src/services/script_context.js | 2 +- src/services/ws.ts | 9 + 8 files changed, 427 insertions(+), 471 deletions(-) rename src/services/{backend_script_api.js => backend_script_api.ts} (58%) diff --git a/src/becca/entities/bnote.ts b/src/becca/entities/bnote.ts index c3f8339e9..e72e43adf 100644 --- a/src/becca/entities/bnote.ts +++ b/src/becca/entities/bnote.ts @@ -1407,7 +1407,7 @@ class BNote extends AbstractBeccaEntity { * @param name - relation name * @param value - relation value (noteId) */ - setRelation(name: string, value: string) { + setRelation(name: string, value?: string) { return this.setAttribute(RELATION, name, value); } diff --git a/src/services/backend_script_api.js b/src/services/backend_script_api.ts similarity index 58% rename from src/services/backend_script_api.js rename to src/services/backend_script_api.ts index 5ee8082df..54b6c5e95 100644 --- a/src/services/backend_script_api.js +++ b/src/services/backend_script_api.ts @@ -1,27 +1,38 @@ -const log = require('./log'); -const noteService = require('./notes'); -const sql = require('./sql'); -const utils = require('./utils'); -const attributeService = require('./attributes'); -const dateNoteService = require('./date_notes'); -const treeService = require('./tree'); -const config = require('./config'); -const axios = require('axios'); -const dayjs = require('dayjs'); -const xml2js = require('xml2js'); -const cloningService = require('./cloning'); -const appInfo = require('./app_info'); -const searchService = require('./search/services/search'); -const SearchContext = require('./search/search_context'); -const becca = require('../becca/becca'); -const ws = require('./ws'); -const SpacedUpdate = require('./spaced_update'); -const specialNotesService = require('./special_notes'); -const branchService = require('./branches'); -const exportService = require('./export/zip'); -const syncMutex = require('./sync_mutex'); -const backupService = require('./backup'); -const optionsService = require('./options'); +import log = require('./log'); +import noteService = require('./notes'); +import sql = require('./sql'); +import utils = require('./utils'); +import attributeService = require('./attributes'); +import dateNoteService = require('./date_notes'); +import treeService = require('./tree'); +import config = require('./config'); +import axios = require('axios'); +import dayjs = require('dayjs'); +import xml2js = require('xml2js'); +import cloningService = require('./cloning'); +import appInfo = require('./app_info'); +import searchService = require('./search/services/search'); +import SearchContext = require('./search/search_context'); +import becca = require('../becca/becca'); +import ws = require('./ws'); +import SpacedUpdate = require('./spaced_update'); +import specialNotesService = require('./special_notes'); +import branchService = require('./branches'); +import exportService = require('./export/zip'); +import syncMutex = require('./sync_mutex'); +import backupService = require('./backup'); +import optionsService = require('./options'); +import BNote = require('../becca/entities/bnote'); +import AbstractBeccaEntity = require('../becca/entities/abstract_becca_entity'); +import BBranch = require('../becca/entities/bbranch'); +import BAttribute = require('../becca/entities/battribute'); +import BAttachment = require('../becca/entities/battachment'); +import BRevision = require('../becca/entities/brevision'); +import BEtapiToken = require('../becca/entities/betapi_token'); +import BOption = require('../becca/entities/boption'); +import { AttributeRow, AttributeType, NoteType } from '../becca/entities/rows'; +import Becca from '../becca/becca-interface'; +import { NoteParams } from './note-interface'; /** @@ -35,139 +46,376 @@ const optionsService = require('./options'); * @var {BackendScriptApi} api */ +interface ApiParams { + startNote: BNote; + originEntity: AbstractBeccaEntity; +} + +interface SearchParams { + includeArchivedNotes?: boolean; + ignoreHoistedNote?: boolean; +} + +interface NoteAndBranch { + note: BNote; + /** object having "note" and "branch" keys representing respective objects */ + branch: BBranch; +} + +interface Api { + /** + * Note where the script started executing (entrypoint). + * As an analogy, in C this would be the file which contains the main() function of the current process. + */ + startNote: BNote; + + /** + * Note where the script is currently executing. This comes into play when your script is spread in multiple code + * notes, the script starts in "startNote", but then through function calls may jump into another note (currentNote). + * A similar concept in C would be __FILE__ + * Don't mix this up with the concept of active note. + */ + currentNote: BNote; + + /** + * Entity whose event triggered this execution + */ + originEntity: AbstractBeccaEntity; + + /** + * Axios library for HTTP requests. See {@link https://axios-http.com} for documentation + * @type {axios} + * @deprecated use native (browser compatible) fetch() instead + */ + axios: typeof axios; + + /** + * day.js library for date manipulation. See {@link https://day.js.org} for documentation + */ + dayjs: typeof dayjs; + + /** + * xml2js library for XML parsing. See {@link https://github.com/Leonidas-from-XIV/node-xml2js} for documentation + */ + + xml2js: typeof xml2js; + + /** + * Instance name identifies particular Trilium instance. It can be useful for scripts + * if some action needs to happen on only one specific instance. + */ + getInstanceName(): string | null; + + getNote(noteId: string): BNote | null; + getBranch(branchId: string): BBranch | null; + getAttribute(attachmentId: string): BAttribute | null; + getAttachment(attachmentId: string): BAttachment | null; + getRevision(revisionId: string): BRevision | null; + getEtapiToken(etapiTokenId: string): BEtapiToken | null; + getEtapiTokens(): BEtapiToken[]; + getOption(optionName: string): BOption | null; + getOptions(): BOption[]; + getAttribute(attributeId: string): BAttribute | null; + + /** + * This is a powerful search method - you can search by attributes and their values, e.g.: + * "#dateModified =* MONTH AND #log". See {@link https://github.com/zadam/trilium/wiki/Search} for full documentation for all options + */ + searchForNotes(query: string, searchParams: SearchParams): BNote[]; + + /** + * This is a powerful search method - you can search by attributes and their values, e.g.: + * "#dateModified =* MONTH AND #log". See {@link https://github.com/zadam/trilium/wiki/Search} for full documentation for all options + * + * @param {string} query + * @param {Object} [searchParams] + */ + searchForNote(query: string, searchParams: SearchParams): BNote | null; + + /** + * Retrieves notes with given label name & value + * + * @param name - attribute name + * @param value - attribute value + */ + getNotesWithLabel(name: string, value?: string): BNote[]; + + /** + * Retrieves first note with given label name & value + * + * @param name - attribute name + * @param value - attribute value + */ + getNoteWithLabel(name: string, value?: string): BNote | null; + + /** + * If there's no branch between note and parent note, create one. Otherwise, do nothing. Returns the new or existing branch. + * + * @param prefix - if branch is created between note and parent note, set this prefix + */ + ensureNoteIsPresentInParent(noteId: string, parentNoteId: string, prefix: string): { + branch: BBranch | null + }; + + /** + * If there's a branch between note and parent note, remove it. Otherwise, do nothing. + */ + ensureNoteIsAbsentFromParent(noteId: string, parentNoteId: string): void; + + /** + * Based on the value, either create or remove branch between note and parent note. + * + * @param present - true if we want the branch to exist, false if we want it gone + * @param prefix - if branch is created between note and parent note, set this prefix + */ + toggleNoteInParent(present: true, noteId: string, parentNoteId: string, prefix: string): void; + + /** + * Create text note. See also createNewNote() for more options. + */ + createTextNote(parentNoteId: string, title: string, content: string): NoteAndBranch; + + /** + * Create data note - data in this context means object serializable to JSON. Created note will be of type 'code' and + * JSON MIME type. See also createNewNote() for more options. + */ + createDataNote(parentNoteId: string, title: string, content: {}): NoteAndBranch; + + /** + * @returns object contains newly created entities note and branch + */ + createNewNote(params: NoteParams): NoteAndBranch; + + /** + * @deprecated please use createTextNote() with similar API for simpler use cases or createNewNote() for more complex needs + * @param parentNoteId - create new note under this parent + * @returns object contains newly created entities note and branch + */ + createNote(parentNoteId: string, title: string, content: string, extraOptions: Omit & { + /** should the note be JSON */ + json?: boolean; + attributes?: AttributeRow[] + }): NoteAndBranch; + + logMessages: Record; + logSpacedUpdates: Record; + + /** + * Log given message to trilium logs and log pane in UI + */ + log(message: string): void; + + /** + * Returns root note of the calendar. + */ + getRootCalendarNote(): BNote | null; + + /** + * Returns day note for given date. If such note doesn't exist, it is created. + * + * @method + * @param date in YYYY-MM-DD format + * @param rootNote - specify calendar root note, normally leave empty to use the default calendar + */ + getDayNote(date: string, rootNote?: BNote): BNote | null; + + /** + * Returns today's day note. If such note doesn't exist, it is created. + * + * @param rootNote specify calendar root note, normally leave empty to use the default calendar + */ + getTodayNote(rootNote?: BNote): BNote | null; + + /** + * Returns note for the first date of the week of the given date. + * + * @param date in YYYY-MM-DD format + * @param rootNote - specify calendar root note, normally leave empty to use the default calendar + */ + getWeekNote(date: string, options: { + // TODO: Deduplicate type with date_notes.ts once ES modules are added. + /** either "monday" (default) or "sunday" */ + startOfTheWeek: "monday" | "sunday"; + }, rootNote: BNote): BNote | null; + + /** + * Returns month note for given date. If such a note doesn't exist, it is created. + * + * @param date in YYYY-MM format + * @param rootNote - specify calendar root note, normally leave empty to use the default calendar + */ + getMonthNote(date: string, rootNote: BNote): BNote | null; + + /** + * Returns year note for given year. If such a note doesn't exist, it is created. + * + * @param year in YYYY format + * @param rootNote - specify calendar root note, normally leave empty to use the default calendar + */ + getYearNote(year: string, rootNote?: BNote): BNote | null; + + /** + * Sort child notes of a given note. + */ + sortNotes(parentNoteId: string, sortConfig: { + /** 'title', 'dateCreated', 'dateModified' or a label name + * See {@link https://github.com/zadam/trilium/wiki/Sorting} for details. */ + sortBy?: string; + reverse?: boolean; + foldersFirst?: boolean; + }): void; + + /** + * This method finds note by its noteId and prefix and either sets it to the given parentNoteId + * or removes the branch (if parentNoteId is not given). + * + * This method looks similar to toggleNoteInParent() but differs because we're looking up branch by prefix. + * + * @deprecated this method is pretty confusing and serves specialized purpose only + */ + setNoteToParent(noteId: string, prefix: string, parentNoteId: string | null): void; + + /** + * This functions wraps code which is supposed to be running in transaction. If transaction already + * exists, then we'll use that transaction. + * + * @param func + * @returns result of func callback + */ + transactional(func: () => void): any; + + /** + * Return randomly generated string of given length. This random string generation is NOT cryptographically secure. + * + * @param length of the string + * @returns random string + */ + randomString(length: number): string; + + /** + * @param to escape + * @returns escaped string + */ + escapeHtml(string: string): string; + + /** + * @param string to unescape + * @returns unescaped string + */ + unescapeHtml(string: string): string; + + /** + * sql + * @type {module:sql} + */ + sql: any; + + getAppInfo(): typeof appInfo; + + /** + * Creates a new launcher to the launchbar. If the launcher (id) already exists, it will be updated. + */ + createOrUpdateLauncher(opts: { + /** id of the launcher, only alphanumeric at least 6 characters long */ + id: string; + /** one of + * - "note" - activating the launcher will navigate to the target note (specified in targetNoteId param) + * - "script" - activating the launcher will execute the script (specified in scriptNoteId param) + * - "customWidget" - the launcher will be rendered with a custom widget (specified in widgetNoteId param) + */ + type: "note" | "script" | "customWidget"; + title: string; + /** if true, will be created in the "Visible launchers", otherwise in "Available launchers" */ + isVisible: boolean; + /** name of the boxicon to be used (e.g. "bx-time") */ + icon: string; + /** will activate the target note/script upon pressing, e.g. "ctrl+e" */ + keyboardShortcut: string; + /** for type "note" */ + targetNoteId: string; + /** for type "script" */ + scriptNoteId: string; + /** for type "customWidget" */ + widgetNoteId?: string; + }): { note: BNote }; + + /** + * @param format - either 'html' or 'markdown' + */ + exportSubtreeToZipFile(noteId: string, format: "markdown" | "html", zipFilePath: string): Promise; + + /** + * Executes given anonymous function on the frontend(s). + * Internally, this serializes the anonymous function into string and sends it to frontend(s) via WebSocket. + * Note that there can be multiple connected frontend instances (e.g. in different tabs). In such case, all + * instances execute the given function. + * + * @param script - script to be executed on the frontend + * @param params - list of parameters to the anonymous function to be sent to frontend + * @returns no return value is provided. + */ + runOnFrontend(script: () => void | string, params: []): void; + + /** + * Sync process can make data intermittently inconsistent. Scripts which require strong data consistency + * can use this function to wait for a possible sync process to finish and prevent new sync process from starting + * while it is running. + * + * Because this is an async process, the inner callback doesn't have automatic transaction handling, so in case + * you need to make some DB changes, you need to surround your call with api.transactional(...) + * + * @param callback - function to be executed while sync process is not running + * @returns resolves once the callback is finished (callback is awaited) + */ + runOutsideOfSync(callback: () => void): Promise; + + /** + * @param backupName - If the backupName is e.g. "now", then the backup will be written to "backup-now.db" file + * @returns resolves once the backup is finished + */ + backupNow(backupName: string): Promise; + + /** + * This object contains "at your risk" and "no BC guarantees" objects for advanced use cases. + */ + __private: { + /** provides access to the backend in-memory object graph, see {@link https://github.com/zadam/trilium/blob/master/src/becca/becca.js} */ + becca: Becca; + }; +} + /** *

This is the main backend API interface for scripts. All the properties and methods are published in the "api" object * available in the JS backend notes. You can use e.g. api.log(api.startNote.title);

* * @constructor */ -function BackendScriptApi(currentNote, apiParams) { - /** - * Note where the script started executing (entrypoint). - * As an analogy, in C this would be the file which contains the main() function of the current process. - * @type {BNote} - */ +function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) { this.startNote = apiParams.startNote; - /** - * Note where the script is currently executing. This comes into play when your script is spread in multiple code - * notes, the script starts in "startNote", but then through function calls may jump into another note (currentNote). - * A similar concept in C would be __FILE__ - * Don't mix this up with the concept of active note. - * @type {BNote} - */ + this.currentNote = currentNote; - /** - * Entity whose event triggered this execution - * @type {AbstractBeccaEntity} - */ + this.originEntity = apiParams.originEntity; for (const key in apiParams) { - this[key] = apiParams[key]; + (this as any)[key] = apiParams[key as keyof ApiParams]; } - /** - * Axios library for HTTP requests. See {@link https://axios-http.com} for documentation - * @type {axios} - * @deprecated use native (browser compatible) fetch() instead - */ this.axios = axios; - /** - * day.js library for date manipulation. See {@link https://day.js.org} for documentation - * @type {dayjs} - */ this.dayjs = dayjs; - /** - * xml2js library for XML parsing. See {@link https://github.com/Leonidas-from-XIV/node-xml2js} for documentation - * @type {xml2js} - */ this.xml2js = xml2js; - - /** - * Instance name identifies particular Trilium instance. It can be useful for scripts - * if some action needs to happen on only one specific instance. - * - * @returns {string|null} - */ this.getInstanceName = () => config.General ? config.General.instanceName : null; - - /** - * @method - * @param {string} noteId - * @returns {BNote|null} - */ this.getNote = noteId => becca.getNote(noteId); - - /** - * @method - * @param {string} branchId - * @returns {BBranch|null} - */ this.getBranch = branchId => becca.getBranch(branchId); - - /** - * @method - * @param {string} attributeId - * @returns {BAttribute|null} - */ this.getAttribute = attributeId => becca.getAttribute(attributeId); - - /** - * @method - * @param {string} attachmentId - * @returns {BAttachment|null} - */ this.getAttachment = attachmentId => becca.getAttachment(attachmentId); - - /** - * @method - * @param {string} revisionId - * @returns {BRevision|null} - */ this.getRevision = revisionId => becca.getRevision(revisionId); - - /** - * @method - * @param {string} etapiTokenId - * @returns {BEtapiToken|null} - */ this.getEtapiToken = etapiTokenId => becca.getEtapiToken(etapiTokenId); - - /** - * @method - * @returns {BEtapiToken[]} - */ this.getEtapiTokens = () => becca.getEtapiTokens(); - - /** - * @method - * @param {string} optionName - * @returns {BOption|null} - */ this.getOption = optionName => becca.getOption(optionName); - - /** - * @method - * @returns {BOption[]} - */ this.getOptions = () => optionsService.getOptions(); - - /** - * @method - * @param {string} attributeId - * @returns {BAttribute|null} - */ this.getAttribute = attributeId => becca.getAttribute(attributeId); - - /** - * This is a powerful search method - you can search by attributes and their values, e.g.: - * "#dateModified =* MONTH AND #log". See {@link https://github.com/zadam/trilium/wiki/Search} for full documentation for all options - * - * @method - * @param {string} query - * @param {Object} [searchParams] - * @returns {BNote[]} - */ + this.searchForNotes = (query, searchParams = {}) => { if (searchParams.includeArchivedNotes === undefined) { searchParams.includeArchivedNotes = true; @@ -183,83 +431,18 @@ function BackendScriptApi(currentNote, apiParams) { return becca.getNotes(noteIds); }; - /** - * This is a powerful search method - you can search by attributes and their values, e.g.: - * "#dateModified =* MONTH AND #log". See {@link https://github.com/zadam/trilium/wiki/Search} for full documentation for all options - * - * @method - * @param {string} query - * @param {Object} [searchParams] - * @returns {BNote|null} - */ + this.searchForNote = (query, searchParams = {}) => { const notes = this.searchForNotes(query, searchParams); return notes.length > 0 ? notes[0] : null; }; - /** - * Retrieves notes with given label name & value - * - * @method - * @param {string} name - attribute name - * @param {string} [value] - attribute value - * @returns {BNote[]} - */ - this.getNotesWithLabel = attributeService.getNotesWithLabel; - - /** - * Retrieves first note with given label name & value - * - * @method - * @param {string} name - attribute name - * @param {string} [value] - attribute value - * @returns {BNote|null} - */ + this.getNotesWithLabel = attributeService.getNotesWithLabel; this.getNoteWithLabel = attributeService.getNoteWithLabel; - - /** - * If there's no branch between note and parent note, create one. Otherwise, do nothing. Returns the new or existing branch. - * - * @method - * @param {string} noteId - * @param {string} parentNoteId - * @param {string} prefix - if branch is created between note and parent note, set this prefix - * @returns {{branch: BBranch|null}} - */ this.ensureNoteIsPresentInParent = cloningService.ensureNoteIsPresentInParent; - - /** - * If there's a branch between note and parent note, remove it. Otherwise, do nothing. - * - * @method - * @param {string} noteId - * @param {string} parentNoteId - * @returns {void} - */ this.ensureNoteIsAbsentFromParent = cloningService.ensureNoteIsAbsentFromParent; - - /** - * Based on the value, either create or remove branch between note and parent note. - * - * @method - * @param {boolean} present - true if we want the branch to exist, false if we want it gone - * @param {string} noteId - * @param {string} parentNoteId - * @param {string} prefix - if branch is created between note and parent note, set this prefix - * @returns {void} - */ this.toggleNoteInParent = cloningService.toggleNoteInParent; - - /** - * Create text note. See also createNewNote() for more options. - * - * @method - * @param {string} parentNoteId - * @param {string} title - * @param {string} content - * @returns {{note: BNote, branch: BBranch}} - object having "note" and "branch" keys representing respective objects - */ this.createTextNote = (parentNoteId, title, content = '') => noteService.createNewNote({ parentNoteId, title, @@ -267,16 +450,6 @@ function BackendScriptApi(currentNote, apiParams) { type: 'text' }); - /** - * Create data note - data in this context means object serializable to JSON. Created note will be of type 'code' and - * JSON MIME type. See also createNewNote() for more options. - * - * @method - * @param {string} parentNoteId - * @param {string} title - * @param {object} content - * @returns {{note: BNote, branch: BBranch}} object having "note" and "branch" keys representing respective objects - */ this.createDataNote = (parentNoteId, title, content = {}) => noteService.createNewNote({ parentNoteId, title, @@ -284,53 +457,28 @@ function BackendScriptApi(currentNote, apiParams) { type: 'code', mime: 'application/json' }); - - /** - * @method - * - * @param {object} params - * @param {string} params.parentNoteId - * @param {string} params.title - * @param {string|Buffer} params.content - * @param {NoteType} params.type - text, code, file, image, search, book, relationMap, canvas, webView - * @param {string} [params.mime] - value is derived from default mimes for type - * @param {boolean} [params.isProtected=false] - * @param {boolean} [params.isExpanded=false] - * @param {string} [params.prefix=''] - * @param {int} [params.notePosition] - default is last existing notePosition in a parent + 10 - * @returns {{note: BNote, branch: BBranch}} object contains newly created entities note and branch - */ + this.createNewNote = noteService.createNewNote; - - /** - * @method - * @deprecated please use createTextNote() with similar API for simpler use cases or createNewNote() for more complex needs - * - * @param {string} parentNoteId - create new note under this parent - * @param {string} title - * @param {string} [content=""] - * @param {object} [extraOptions={}] - * @param {boolean} [extraOptions.json=false] - should the note be JSON - * @param {boolean} [extraOptions.isProtected=false] - should the note be protected - * @param {string} [extraOptions.type='text'] - note type - * @param {string} [extraOptions.mime='text/html'] - MIME type of the note - * @param {object[]} [extraOptions.attributes=[]] - attributes to be created for this note - * @param {AttributeType} extraOptions.attributes.type - attribute type - label, relation etc. - * @param {string} extraOptions.attributes.name - attribute name - * @param {string} [extraOptions.attributes.value] - attribute value - * @returns {{note: BNote, branch: BBranch}} object contains newly created entities note and branch - */ - this.createNote = (parentNoteId, title, content = "", extraOptions = {}) => { - extraOptions.parentNoteId = parentNoteId; - extraOptions.title = title; - + + this.createNote = (parentNoteId, title, content = "", _extraOptions = {}) => { const parentNote = becca.getNote(parentNoteId); + if (!parentNote) { + throw new Error(`Unable to find parent note with ID ${parentNote}.`); + } + + let extraOptions: NoteParams = { + ..._extraOptions, + content: "", + type: "text", + parentNoteId, + title + }; // code note type can be inherited, otherwise "text" is the default extraOptions.type = parentNote.type === 'code' ? 'code' : 'text'; extraOptions.mime = parentNote.type === 'code' ? parentNote.mime : 'text/html'; - if (extraOptions.json) { + if (_extraOptions.json) { extraOptions.content = JSON.stringify(content || {}, null, '\t'); extraOptions.type = 'code'; extraOptions.mime = 'application/json'; @@ -342,7 +490,7 @@ function BackendScriptApi(currentNote, apiParams) { return sql.transactional(() => { const { note, branch } = noteService.createNewNote(extraOptions); - for (const attr of extraOptions.attributes || []) { + for (const attr of _extraOptions.attributes || []) { attributeService.createAttribute({ noteId: note.noteId, type: attr.type, @@ -358,14 +506,7 @@ function BackendScriptApi(currentNote, apiParams) { this.logMessages = {}; this.logSpacedUpdates = {}; - - /** - * Log given message to trilium logs and log pane in UI - * - * @method - * @param message - * @returns {void} - */ + this.log = message => { log.info(message); @@ -387,77 +528,13 @@ function BackendScriptApi(currentNote, apiParams) { this.logSpacedUpdates[noteId].scheduleUpdate(); }; - /** - * Returns root note of the calendar. - * - * @method - * @returns {BNote|null} - */ this.getRootCalendarNote = dateNoteService.getRootCalendarNote; - - /** - * Returns day note for given date. If such note doesn't exist, it is created. - * - * @method - * @param {string} date in YYYY-MM-DD format - * @param {BNote} [rootNote] - specify calendar root note, normally leave empty to use the default calendar - * @returns {BNote|null} - */ this.getDayNote = dateNoteService.getDayNote; - - /** - * Returns today's day note. If such note doesn't exist, it is created. - * - * @method - * @param {BNote} [rootNote] - specify calendar root note, normally leave empty to use the default calendar - * @returns {BNote|null} - */ this.getTodayNote = dateNoteService.getTodayNote; - - /** - * Returns note for the first date of the week of the given date. - * - * @method - * @param {string} date in YYYY-MM-DD format - * @param {object} [options] - * @param {string} [options.startOfTheWeek=monday] - either "monday" (default) or "sunday" - * @param {BNote} [rootNote] - specify calendar root note, normally leave empty to use the default calendar - * @returns {BNote|null} - */ this.getWeekNote = dateNoteService.getWeekNote; - - /** - * Returns month note for given date. If such a note doesn't exist, it is created. - * - * @method - * @param {string} date in YYYY-MM format - * @param {BNote} [rootNote] - specify calendar root note, normally leave empty to use the default calendar - * @returns {BNote|null} - */ this.getMonthNote = dateNoteService.getMonthNote; - - /** - * Returns year note for given year. If such a note doesn't exist, it is created. - * - * @method - * @param {string} year in YYYY format - * @param {BNote} [rootNote] - specify calendar root note, normally leave empty to use the default calendar - * @returns {BNote|null} - */ this.getYearNote = dateNoteService.getYearNote; - /** - * Sort child notes of a given note. - * - * @method - * @param {string} parentNoteId - this note's child notes will be sorted - * @param {object} [sortConfig] - * @param {string} [sortConfig.sortBy=title] - 'title', 'dateCreated', 'dateModified' or a label name - * See {@link https://github.com/zadam/trilium/wiki/Sorting} for details. - * @param {boolean} [sortConfig.reverse=false] - * @param {boolean} [sortConfig.foldersFirst=false] - * @returns {void} - */ this.sortNotes = (parentNoteId, sortConfig = {}) => treeService.sortNotes( parentNoteId, sortConfig.sortBy || "title", @@ -465,85 +542,15 @@ function BackendScriptApi(currentNote, apiParams) { !!sortConfig.foldersFirst ); - /** - * This method finds note by its noteId and prefix and either sets it to the given parentNoteId - * or removes the branch (if parentNoteId is not given). - * - * This method looks similar to toggleNoteInParent() but differs because we're looking up branch by prefix. - * - * @method - * @deprecated this method is pretty confusing and serves specialized purpose only - * @param {string} noteId - * @param {string} prefix - * @param {string|null} parentNoteId - * @returns {void} - */ this.setNoteToParent = treeService.setNoteToParent; - - /** - * This functions wraps code which is supposed to be running in transaction. If transaction already - * exists, then we'll use that transaction. - * - * @method - * @param {function} func - * @returns {any} result of func callback - */ this.transactional = sql.transactional; - - /** - * Return randomly generated string of given length. This random string generation is NOT cryptographically secure. - * - * @method - * @param {int} length of the string - * @returns {string} random string - */ this.randomString = utils.randomString; - - /** - * @method - * @param {string} string to escape - * @returns {string} escaped string - */ this.escapeHtml = utils.escapeHtml; - - /** - * @method - * @param {string} string to unescape - * @returns {string} unescaped string - */ this.unescapeHtml = utils.unescapeHtml; - - /** - * sql - * @type {module:sql} - */ this.sql = sql; - - /** - * @method - * @returns {{syncVersion, appVersion, buildRevision, dbVersion, dataDirectory, buildDate}|*} - object representing basic info about running Trilium version - */ this.getAppInfo = () => appInfo; - /** - * Creates a new launcher to the launchbar. If the launcher (id) already exists, it will be updated. - * - * @method - * @param {object} opts - * @param {string} opts.id - id of the launcher, only alphanumeric at least 6 characters long - * @param {"note" | "script" | "customWidget"} opts.type - one of - * * "note" - activating the launcher will navigate to the target note (specified in targetNoteId param) - * * "script" - activating the launcher will execute the script (specified in scriptNoteId param) - * * "customWidget" - the launcher will be rendered with a custom widget (specified in widgetNoteId param) - * @param {string} opts.title - * @param {boolean} [opts.isVisible=false] - if true, will be created in the "Visible launchers", otherwise in "Available launchers" - * @param {string} [opts.icon] - name of the boxicon to be used (e.g. "bx-time") - * @param {string} [opts.keyboardShortcut] - will activate the target note/script upon pressing, e.g. "ctrl+e" - * @param {string} [opts.targetNoteId] - for type "note" - * @param {string} [opts.scriptNoteId] - for type "script" - * @param {string} [opts.widgetNoteId] - for type "customWidget" - * @returns {{note: BNote}} - */ + this.createOrUpdateLauncher = opts => { if (!opts.id) { throw new Error("ID is a mandatory parameter for api.createOrUpdateLauncher(opts)"); } if (!opts.id.match(/[a-z0-9]{6,1000}/i)) { throw new Error(`ID must be an alphanumeric string at least 6 characters long.`); } @@ -603,29 +610,14 @@ function BackendScriptApi(currentNote, apiParams) { return { note: launcherNote }; }; - /** - * @method - * @param {string} noteId - * @param {string} format - either 'html' or 'markdown' - * @param {string} zipFilePath - * @returns {Promise} - */ this.exportSubtreeToZipFile = async (noteId, format, zipFilePath) => await exportService.exportToZipFile(noteId, format, zipFilePath); - /** - * Executes given anonymous function on the frontend(s). - * Internally, this serializes the anonymous function into string and sends it to frontend(s) via WebSocket. - * Note that there can be multiple connected frontend instances (e.g. in different tabs). In such case, all - * instances execute the given function. - * - * @method - * @param {string} script - script to be executed on the frontend - * @param {Array.} params - list of parameters to the anonymous function to be sent to frontend - * @returns {undefined} - no return value is provided. - */ - this.runOnFrontend = async (script, params = []) => { - if (typeof script === "function") { - script = script.toString(); + this.runOnFrontend = async (_script, params = []) => { + let script: string; + if (typeof _script === "string") { + script = _script; + } else { + script = _script.toString(); } ws.sendMessageToAllClients({ @@ -635,10 +627,10 @@ function BackendScriptApi(currentNote, apiParams) { startNoteId: this.startNote.noteId, currentNoteId: this.currentNote.noteId, originEntityName: "notes", // currently there's no other entity on the frontend which can trigger event - originEntityId: this.originEntity?.noteId || null + originEntityId: ("noteId" in this.originEntity && (this.originEntity as BNote)?.noteId) || null }); - function prepareParams(params) { + function prepareParams(params: any[]) { if (!params) { return params; } @@ -653,36 +645,13 @@ function BackendScriptApi(currentNote, apiParams) { }); } }; - - /** - * Sync process can make data intermittently inconsistent. Scripts which require strong data consistency - * can use this function to wait for a possible sync process to finish and prevent new sync process from starting - * while it is running. - * - * Because this is an async process, the inner callback doesn't have automatic transaction handling, so in case - * you need to make some DB changes, you need to surround your call with api.transactional(...) - * - * @method - * @param {function} callback - function to be executed while sync process is not running - * @returns {Promise} - resolves once the callback is finished (callback is awaited) - */ + this.runOutsideOfSync = syncMutex.doExclusively; - - /** - * @method - * @param {string} backupName - If the backupName is e.g. "now", then the backup will be written to "backup-now.db" file - * @returns {Promise} - resolves once the backup is finished - */ this.backupNow = backupService.backupNow; - - /** - * This object contains "at your risk" and "no BC guarantees" objects for advanced use cases. - * - * @property {Becca} becca - provides access to the backend in-memory object graph, see {@link https://github.com/zadam/trilium/blob/master/src/becca/becca.js} - */ + this.__private = { becca } } -module.exports = BackendScriptApi; +export = BackendScriptApi; diff --git a/src/services/date_notes.ts b/src/services/date_notes.ts index 5ae1f8366..989106939 100644 --- a/src/services/date_notes.ts +++ b/src/services/date_notes.ts @@ -191,7 +191,7 @@ function getDayNote(dateStr: string, _rootNote: BNote | null = null): BNote { return dateNote as unknown as BNote; } -function getTodayNote(rootNote = null) { +function getTodayNote(rootNote: BNote | null = null) { return getDayNote(dateUtils.localNowDate(), rootNote); } @@ -216,7 +216,7 @@ interface WeekNoteOpts { startOfTheWeek?: StartOfWeek } -function getWeekNote(dateStr: string, options: WeekNoteOpts = {}, rootNote = null) { +function getWeekNote(dateStr: string, options: WeekNoteOpts = {}, rootNote: BNote | null = null) { const startOfTheWeek = options.startOfTheWeek || "monday"; const dateObj = getStartOfTheWeek(dateUtils.parseLocalDate(dateStr), startOfTheWeek); diff --git a/src/services/import/zip.ts b/src/services/import/zip.ts index fbf7c539b..cda7347aa 100644 --- a/src/services/import/zip.ts +++ b/src/services/import/zip.ts @@ -239,7 +239,7 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo noteId: noteId, type: resolveNoteType(noteMeta?.type), mime: noteMeta ? noteMeta.mime : 'text/html', - prefix: noteMeta ? noteMeta.prefix : '', + prefix: noteMeta ? noteMeta.prefix || "" : '', isExpanded: noteMeta ? noteMeta.isExpanded : false, notePosition: (noteMeta && firstNote) ? noteMeta.notePosition : undefined, isProtected: importRootNote.isProtected && protectedSessionService.isProtectedSessionAvailable(), @@ -510,7 +510,7 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo noteId, type, mime, - prefix: noteMeta ? noteMeta.prefix : '', + prefix: noteMeta ? noteMeta.prefix || "" : '', isExpanded: noteMeta ? noteMeta.isExpanded : false, // root notePosition should be ignored since it relates to the original document // now import root should be placed after existing notes into new parent diff --git a/src/services/note-interface.ts b/src/services/note-interface.ts index f35caa906..0b99c31ef 100644 --- a/src/services/note-interface.ts +++ b/src/services/note-interface.ts @@ -3,10 +3,12 @@ import { NoteType } from "../becca/entities/rows"; export interface NoteParams { /** optionally can force specific noteId */ noteId?: string; + branchId?: string; parentNoteId: string; templateNoteId?: string; title: string; content: string; + /** text, code, file, image, search, book, relationMap, canvas, webView */ type: NoteType; /** default value is derived from default mimes for type */ mime?: string; diff --git a/src/services/notes.ts b/src/services/notes.ts index d75e90e59..6dd68dc73 100644 --- a/src/services/notes.ts +++ b/src/services/notes.ts @@ -25,6 +25,7 @@ import ws = require('./ws'); import html2plaintext = require('html2plaintext'); import { AttachmentRow, AttributeRow, BranchRow, NoteRow, NoteType } from '../becca/entities/rows'; import TaskContext = require('./task_context'); +import { NoteParams } from './note-interface'; interface FoundLink { name: "imageLink" | "internalLink" | "includeNoteLink" | "relationMapLink", @@ -152,31 +153,6 @@ function getAndValidateParent(params: GetValidateParams) { return parentNote; } -interface NoteParams { - /** optionally can force specific noteId */ - noteId?: string; - branchId?: 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 | null; - /** default is the last existing notePosition in a parent + 10 */ - notePosition?: number; - dateCreated?: string; - utcDateCreated?: string; - ignoreForbiddenParents?: boolean; - target?: "into"; -} - function createNewNote(params: NoteParams): { note: BNote; branch: BBranch; diff --git a/src/services/script_context.js b/src/services/script_context.js index d4b83bc37..7586dab24 100644 --- a/src/services/script_context.js +++ b/src/services/script_context.js @@ -1,5 +1,5 @@ const utils = require('./utils'); -const BackendScriptApi = require('./backend_script_api.js'); +const BackendScriptApi = require('./backend_script_api'); function ScriptContext(allNotes, apiParams = {}) { this.modules = {}; diff --git a/src/services/ws.ts b/src/services/ws.ts index ca6e0ff98..6ff81e1db 100644 --- a/src/services/ws.ts +++ b/src/services/ws.ts @@ -43,6 +43,15 @@ interface Message { message?: string; reason?: string; result?: string; + + script?: string; + params?: any[]; + noteId?: string; + messages?: string[]; + startNoteId?: string; + currentNoteId?: string; + originEntityName?: "notes"; + originEntityId?: string | null; } type SessionParser = (req: IncomingMessage, params: {}, cb: () => void) => void;