diff --git a/src/app.js b/src/app.js index 49373545e..601ed0ece 100644 --- a/src/app.js +++ b/src/app.js @@ -7,7 +7,7 @@ const compression = require('compression'); const sessionParser = require('./routes/session_parser.js'); const utils = require('./services/utils'); -require('./services/handlers.js'); +require('./services/handlers'); require('./becca/becca_loader'); const app = express(); @@ -51,7 +51,7 @@ require('./services/backup'); // trigger consistency checks timer require('./services/consistency_checks'); -require('./services/scheduler.js'); +require('./services/scheduler'); if (utils.isElectron()) { require('@electron/remote/main').initialize(); diff --git a/src/becca/entities/bnote.ts b/src/becca/entities/bnote.ts index c3f8339e9..54bb0c427 100644 --- a/src/becca/entities/bnote.ts +++ b/src/becca/entities/bnote.ts @@ -668,7 +668,7 @@ class BNote extends AbstractBeccaEntity { * @param name - relation name to filter * @returns all note's relations (attributes with type relation), excluding inherited ones */ - getOwnedRelations(name: string): BAttribute[] { + getOwnedRelations(name?: string | null): BAttribute[] { return this.getOwnedAttributes(RELATION, name); } @@ -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/routes/api/script.js b/src/routes/api/script.js index f7903f411..8bd7ba712 100644 --- a/src/routes/api/script.js +++ b/src/routes/api/script.js @@ -1,6 +1,6 @@ "use strict"; -const scriptService = require('../../services/script.js'); +const scriptService = require('../../services/script'); const attributeService = require('../../services/attributes'); const becca = require('../../becca/becca'); const syncService = require('../../services/sync'); @@ -11,7 +11,7 @@ const sql = require('../../services/sql'); // this and does result.then(). async function exec(req) { try { - const {body} = req; + const { body } = req; const execute = body => scriptService.executeScript( body.script, @@ -115,7 +115,7 @@ function getRelationBundles(req) { function getBundle(req) { const note = becca.getNote(req.params.noteId); - const {script, params} = req.body; + const { script, params } = req.body; return scriptService.getScriptBundleForFrontend(note, script, params); } diff --git a/src/routes/custom.js b/src/routes/custom.js index f566902f6..fbf6c42c3 100644 --- a/src/routes/custom.js +++ b/src/routes/custom.js @@ -1,6 +1,6 @@ const log = require('../services/log'); const fileService = require('./api/files.js'); -const scriptService = require('../services/script.js'); +const scriptService = require('../services/script'); const cls = require('../services/cls'); const sql = require('../services/sql'); const becca = require('../becca/becca'); 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..34767ae75 100644 --- a/src/services/backend_script_api.js +++ b/src/services/backend_script_api.ts @@ -1,27 +1,39 @@ -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'; +import { ApiParams } from './backend_script_api_interface'; /** @@ -35,139 +47,372 @@ const optionsService = require('./options'); * @var {BackendScriptApi} api */ +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; + }; +} + +// TODO: Convert to class. /** *

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 +428,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 +447,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 +454,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 +487,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,17 +503,14 @@ 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); + if (!this.startNote) { + return; + } + const { noteId } = this.startNote; this.logMessages[noteId] = this.logMessages[noteId] || []; @@ -387,77 +529,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 +543,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,42 +611,27 @@ 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({ type: 'execute-script', script: script, params: prepareParams(params), - startNoteId: this.startNote.noteId, + 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: (this.originEntity && "noteId" in this.originEntity && (this.originEntity as BNote)?.noteId) || null }); - function prepareParams(params) { + function prepareParams(params: any[]) { if (!params) { return params; } @@ -653,36 +646,15 @@ 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 as any as { + new (currentNote: BNote, apiParams: ApiParams): Api +}; diff --git a/src/services/backend_script_api_interface.ts b/src/services/backend_script_api_interface.ts new file mode 100644 index 000000000..7031d1bc3 --- /dev/null +++ b/src/services/backend_script_api_interface.ts @@ -0,0 +1,7 @@ +import AbstractBeccaEntity = require("../becca/entities/abstract_becca_entity"); +import BNote = require("../becca/entities/bnote"); + +export interface ApiParams { + startNote?: BNote; + originEntity?: AbstractBeccaEntity; +} \ No newline at end of file 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/handlers.js b/src/services/handlers.ts similarity index 82% rename from src/services/handlers.js rename to src/services/handlers.ts index 11c9ef5fe..9e10b778a 100644 --- a/src/services/handlers.js +++ b/src/services/handlers.ts @@ -1,13 +1,18 @@ -const eventService = require('./events'); -const scriptService = require('./script.js'); -const treeService = require('./tree'); -const noteService = require('./notes'); -const becca = require('../becca/becca'); -const BAttribute = require('../becca/entities/battribute'); -const hiddenSubtreeService = require('./hidden_subtree'); -const oneTimeTimer = require('./one_time_timer'); +import eventService = require('./events'); +import scriptService = require('./script'); +import treeService = require('./tree'); +import noteService = require('./notes'); +import becca = require('../becca/becca'); +import BAttribute = require('../becca/entities/battribute'); +import hiddenSubtreeService = require('./hidden_subtree'); +import oneTimeTimer = require('./one_time_timer'); +import BNote = require('../becca/entities/bnote'); +import AbstractBeccaEntity = require('../becca/entities/abstract_becca_entity'); +import { DefinitionObject } from './promoted_attribute_definition_interface'; -function runAttachedRelations(note, relationName, originEntity) { +type Handler = (definition: DefinitionObject, note: BNote, targetNote: BNote) => void; + +function runAttachedRelations(note: BNote, relationName: string, originEntity: AbstractBeccaEntity) { if (!note) { return; } @@ -16,7 +21,7 @@ function runAttachedRelations(note, relationName, originEntity) { const notesToRun = new Set( note.getRelations(relationName) .map(relation => relation.getTargetNote()) - .filter(note => !!note) + .filter(note => !!note) as BNote[] ); for (const noteToRun of notesToRun) { @@ -42,7 +47,7 @@ eventService.subscribe(eventService.NOTE_TITLE_CHANGED, note => { } }); -eventService.subscribe([ eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED ], ({ entityName, entity }) => { +eventService.subscribe([eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED], ({ entityName, entity }) => { if (entityName === 'attributes') { runAttachedRelations(entity.getNote(), 'runOnAttributeChange', entity); @@ -58,7 +63,7 @@ eventService.subscribe([ eventService.ENTITY_CHANGED, eventService.ENTITY_DELETE } }); -eventService.subscribe(eventService.ENTITY_CHANGED, ({entityName, entity}) => { +eventService.subscribe(eventService.ENTITY_CHANGED, ({ entityName, entity }) => { if (entityName === 'branches') { const parentNote = becca.getNote(entity.parentNoteId); @@ -74,7 +79,7 @@ eventService.subscribe(eventService.ENTITY_CHANGED, ({entityName, entity}) => { } }); -eventService.subscribe(eventService.NOTE_CONTENT_CHANGE, ({entity}) => { +eventService.subscribe(eventService.NOTE_CONTENT_CHANGE, ({ entity }) => { runAttachedRelations(entity, 'runOnNoteContentChange', entity); }); @@ -84,6 +89,9 @@ eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) => if (entity.type === 'relation' && entity.name === 'template') { const note = becca.getNote(entity.noteId); + if (!note) { + return; + } const templateNote = becca.getNote(entity.value); @@ -94,6 +102,7 @@ eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) => const content = note.getContent(); if (["text", "code"].includes(note.type) + && typeof content === "string" // if the note has already content we're not going to overwrite it with template's one && (!content || content.trim().length === 0) && templateNote.hasStringContent()) { @@ -138,7 +147,7 @@ eventService.subscribe(eventService.CHILD_NOTE_CREATED, ({ parentNote, childNote runAttachedRelations(parentNote, 'runOnChildNoteCreation', childNote); }); -function processInverseRelations(entityName, entity, handler) { +function processInverseRelations(entityName: string, entity: BAttribute, handler: Handler) { if (entityName === 'attributes' && entity.type === 'relation') { const note = entity.getNote(); const relDefinitions = note.getLabels(`relation:${entity.name}`); @@ -149,13 +158,15 @@ function processInverseRelations(entityName, entity, handler) { if (definition.inverseRelation && definition.inverseRelation.trim()) { const targetNote = entity.getTargetNote(); - handler(definition, note, targetNote); + if (targetNote) { + handler(definition, note, targetNote); + } } } } } -function handleSortedAttribute(entity) { +function handleSortedAttribute(entity: BAttribute) { treeService.sortNotesIfNeeded(entity.noteId); if (entity.isInheritable) { @@ -169,7 +180,7 @@ function handleSortedAttribute(entity) { } } -function handleMaybeSortingLabel(entity) { +function handleMaybeSortingLabel(entity: BAttribute) { // check if this label is used for sorting, if yes force re-sort const note = becca.notes[entity.noteId]; @@ -203,7 +214,7 @@ eventService.subscribe(eventService.ENTITY_CHANGED, ({ entityName, entity }) => new BAttribute({ noteId: targetNote.noteId, type: 'relation', - name: definition.inverseRelation, + name: definition.inverseRelation || "", value: note.noteId, isInheritable: entity.isInheritable }).save(); @@ -215,7 +226,7 @@ eventService.subscribe(eventService.ENTITY_CHANGED, ({ entityName, entity }) => }); eventService.subscribe(eventService.ENTITY_DELETED, ({ entityName, entity }) => { - processInverseRelations(entityName, entity, (definition, note, targetNote) => { + processInverseRelations(entityName, entity, (definition: DefinitionObject, note: BNote, targetNote: BNote) => { // if one inverse attribute is deleted, then the other should be deleted as well const relations = targetNote.getOwnedRelations(definition.inverseRelation); @@ -238,6 +249,6 @@ eventService.subscribe(eventService.ENTITY_DELETED, ({ entityName, entity }) => } }); -module.exports = { +export = { runAttachedRelations }; diff --git a/src/services/import/zip.ts b/src/services/import/zip.ts index 122e0260d..93ccc2da9 100644 --- a/src/services/import/zip.ts +++ b/src/services/import/zip.ts @@ -239,8 +239,8 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo noteId: noteId, type: resolveNoteType(noteMeta?.type), mime: noteMeta ? noteMeta.mime : 'text/html', - prefix: noteMeta ? noteMeta.prefix : '', - isExpanded: noteMeta ? noteMeta.isExpanded : false, + prefix: noteMeta?.prefix || '', + isExpanded: !!noteMeta?.isExpanded, notePosition: (noteMeta && firstNote) ? noteMeta.notePosition : undefined, isProtected: importRootNote.isProtected && protectedSessionService.isProtectedSessionAvailable(), }); @@ -510,8 +510,8 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo noteId, type, mime, - prefix: noteMeta ? noteMeta.prefix : '', - isExpanded: noteMeta ? noteMeta.isExpanded : false, + prefix: noteMeta?.prefix || '', + isExpanded: !!noteMeta?.isExpanded, // root notePosition should be ignored since it relates to the original document // now import root should be placed after existing notes into new parent notePosition: (noteMeta && firstNote) ? noteMeta.notePosition : undefined, 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/promoted_attribute_definition_interface.ts b/src/services/promoted_attribute_definition_interface.ts new file mode 100644 index 000000000..2f68aa7ac --- /dev/null +++ b/src/services/promoted_attribute_definition_interface.ts @@ -0,0 +1,8 @@ +export interface DefinitionObject { + isPromoted?: boolean; + labelType?: string; + multiplicity?: string; + numberPrecision?: number; + promotedAlias?: string; + inverseRelation?: string; +} diff --git a/src/services/promoted_attribute_definition_parser.ts b/src/services/promoted_attribute_definition_parser.ts index 3efe16f4d..228c96093 100644 --- a/src/services/promoted_attribute_definition_parser.ts +++ b/src/services/promoted_attribute_definition_parser.ts @@ -1,11 +1,4 @@ -interface DefinitionObject { - isPromoted?: boolean; - labelType?: string; - multiplicity?: string; - numberPrecision?: number; - promotedAlias?: string; - inverseRelation?: string; -} +import { DefinitionObject } from "./promoted_attribute_definition_interface"; function parse(value: string): DefinitionObject { const tokens = value.split(',').map(t => t.trim()); diff --git a/src/services/scheduler.js b/src/services/scheduler.ts similarity index 68% rename from src/services/scheduler.js rename to src/services/scheduler.ts index aa0a3dfcd..819de0e59 100644 --- a/src/services/scheduler.js +++ b/src/services/scheduler.ts @@ -1,28 +1,24 @@ -const scriptService = require('./script.js'); -const cls = require('./cls'); -const sqlInit = require('./sql_init'); -const config = require('./config'); -const log = require('./log'); -const attributeService = require('../services/attributes'); -const protectedSessionService = require('../services/protected_session'); -const hiddenSubtreeService = require('./hidden_subtree'); +import scriptService = require('./script'); +import cls = require('./cls'); +import sqlInit = require('./sql_init'); +import config = require('./config'); +import log = require('./log'); +import attributeService = require('../services/attributes'); +import protectedSessionService = require('../services/protected_session'); +import hiddenSubtreeService = require('./hidden_subtree'); +import BNote = require('../becca/entities/bnote'); -/** - * @param {BNote} note - * @return {int[]} - */ -function getRunAtHours(note) { +function getRunAtHours(note: BNote): number[] { try { return note.getLabelValues('runAtHour').map(hour => parseInt(hour)); - } - catch (e) { + } catch (e: any) { log.error(`Could not parse runAtHour for note ${note.noteId}: ${e.message}`); return []; } } -function runNotesWithLabel(runAttrValue) { +function runNotesWithLabel(runAttrValue: string) { const instanceName = config.General ? config.General.instanceName : null; const currentHours = new Date().getHours(); const notes = attributeService.getNotesWithLabel('run', runAttrValue); @@ -34,7 +30,7 @@ function runNotesWithLabel(runAttrValue) { if ((runOnInstances.length === 0 || runOnInstances.includes(instanceName)) && (runAtHours.length === 0 || runAtHours.includes(currentHours)) ) { - scriptService.executeNoteNoException(note, {originEntity: note}); + scriptService.executeNoteNoException(note, { originEntity: note }); } } } diff --git a/src/services/script.js b/src/services/script.ts similarity index 67% rename from src/services/script.js rename to src/services/script.ts index f82f50444..9054ff75b 100644 --- a/src/services/script.js +++ b/src/services/script.ts @@ -1,9 +1,22 @@ -const ScriptContext = require('./script_context.js'); -const cls = require('./cls'); -const log = require('./log'); -const becca = require('../becca/becca'); +import ScriptContext = require('./script_context'); +import cls = require('./cls'); +import log = require('./log'); +import becca = require('../becca/becca'); +import BNote = require('../becca/entities/bnote'); +import { ApiParams } from './backend_script_api_interface'; -function executeNote(note, apiParams) { +interface Bundle { + note?: BNote; + noteId?: string; + script: string; + html: string; + allNotes?: BNote[]; + allNoteIds?: string[]; +} + +type ScriptParams = any[]; + +function executeNote(note: BNote, apiParams: ApiParams) { if (!note.isJavaScript() || note.getScriptEnv() !== 'backend' || !note.isContentAvailable()) { log.info(`Cannot execute note ${note.noteId} "${note.title}", note must be of type "Code: JS backend"`); @@ -11,11 +24,14 @@ function executeNote(note, apiParams) { } const bundle = getScriptBundle(note, true, 'backend'); - + if (!bundle) { + throw new Error("Unable to determine bundle."); + } + return executeBundle(bundle, apiParams); } -function executeNoteNoException(note, apiParams) { +function executeNoteNoException(note: BNote, apiParams: ApiParams) { try { executeNote(note, apiParams); } @@ -24,7 +40,7 @@ function executeNoteNoException(note, apiParams) { } } -function executeBundle(bundle, apiParams = {}) { +function executeBundle(bundle: Bundle, apiParams: ApiParams = {}) { if (!apiParams.startNote) { // this is the default case, the only exception is when we want to preserve frontend startNote apiParams.startNote = bundle.note; @@ -33,19 +49,19 @@ function executeBundle(bundle, apiParams = {}) { const originalComponentId = cls.get('componentId'); cls.set('componentId', 'script'); - cls.set('bundleNoteId', bundle.note.noteId); + cls.set('bundleNoteId', bundle.note?.noteId); // last \r\n is necessary if the script contains line comment on its last line const script = `function() {\r ${bundle.script}\r }`; - const ctx = new ScriptContext(bundle.allNotes, apiParams); + const ctx = new ScriptContext(bundle.allNotes || [], apiParams); try { return execute(ctx, script); } - catch (e) { - log.error(`Execution of script "${bundle.note.title}" (${bundle.note.noteId}) failed with error: ${e.message}`); + catch (e: any) { + log.error(`Execution of script "${bundle.note?.title}" (${bundle.note?.noteId}) failed with error: ${e.message}`); throw e; } @@ -61,25 +77,36 @@ ${bundle.script}\r * This method preserves frontend startNode - that's why we start execution from currentNote and override * bundle's startNote. */ -function executeScript(script, params, startNoteId, currentNoteId, originEntityName, originEntityId) { +function executeScript(script: string, params: ScriptParams, startNoteId: string, currentNoteId: string, originEntityName: string, originEntityId: string) { const startNote = becca.getNote(startNoteId); const currentNote = becca.getNote(currentNoteId); const originEntity = becca.getEntity(originEntityName, originEntityId); + if (!currentNote) { + throw new Error("Cannot find note."); + } + // we're just executing an excerpt of the original frontend script in the backend context, so we must // override normal note's content, and it's mime type / script environment const overrideContent = `return (${script}\r\n)(${getParams(params)})`; const bundle = getScriptBundle(currentNote, true, 'backend', [], overrideContent); + if (!bundle) { + throw new Error("Unable to determine script bundle."); + } + + if (!startNote || !originEntity) { + throw new Error("Missing start note or origin entity."); + } return executeBundle(bundle, { startNote, originEntity }); } -function execute(ctx, script) { - return function() { return eval(`const apiContext = this;\r\n(${script}\r\n)()`); }.call(ctx); +function execute(ctx: ScriptContext, script: string) { + return function () { return eval(`const apiContext = this;\r\n(${script}\r\n)()`); }.call(ctx); } -function getParams(params) { +function getParams(params: ScriptParams) { if (!params) { return params; } @@ -94,12 +121,7 @@ function getParams(params) { }).join(","); } -/** - * @param {BNote} note - * @param {string} [script] - * @param {Array} [params] - */ -function getScriptBundleForFrontend(note, script, params) { +function getScriptBundleForFrontend(note: BNote, script: string, params: ScriptParams) { let overrideContent = null; if (script) { @@ -113,23 +135,16 @@ function getScriptBundleForFrontend(note, script, params) { } // for frontend, we return just noteIds because frontend needs to use its own entity instances - bundle.noteId = bundle.note.noteId; + bundle.noteId = bundle.note?.noteId; delete bundle.note; - bundle.allNoteIds = bundle.allNotes.map(note => note.noteId); + bundle.allNoteIds = bundle.allNotes?.map(note => note.noteId); delete bundle.allNotes; return bundle; } -/** - * @param {BNote} note - * @param {boolean} [root=true] - * @param {string|null} [scriptEnv] - * @param {string[]} [includedNoteIds] - * @param {string|null} [overrideContent] - */ -function getScriptBundle(note, root = true, scriptEnv = null, includedNoteIds = [], overrideContent = null) { +function getScriptBundle(note: BNote, root: boolean = true, scriptEnv: string | null = null, includedNoteIds: string[] = [], overrideContent: string | null = null): Bundle | undefined { if (!note.isContentAvailable()) { return; } @@ -146,7 +161,7 @@ function getScriptBundle(note, root = true, scriptEnv = null, includedNoteIds = return; } - const bundle = { + const bundle: Bundle = { note: note, script: '', html: '', @@ -165,10 +180,14 @@ function getScriptBundle(note, root = true, scriptEnv = null, includedNoteIds = const childBundle = getScriptBundle(child, false, scriptEnv, includedNoteIds); if (childBundle) { - modules.push(childBundle.note); + if (childBundle.note) { + modules.push(childBundle.note); + } bundle.script += childBundle.script; bundle.html += childBundle.html; - bundle.allNotes = bundle.allNotes.concat(childBundle.allNotes); + if (bundle.allNotes && childBundle.allNotes) { + bundle.allNotes = bundle.allNotes.concat(childBundle.allNotes); + } } } @@ -196,11 +215,11 @@ return module.exports; return bundle; } -function sanitizeVariableName(str) { +function sanitizeVariableName(str: string) { return str.replace(/[^a-z0-9_]/gim, ""); } -module.exports = { +export = { executeNote, executeNoteNoException, executeScript, diff --git a/src/services/script_context.js b/src/services/script_context.js deleted file mode 100644 index d4b83bc37..000000000 --- a/src/services/script_context.js +++ /dev/null @@ -1,22 +0,0 @@ -const utils = require('./utils'); -const BackendScriptApi = require('./backend_script_api.js'); - -function ScriptContext(allNotes, apiParams = {}) { - this.modules = {}; - this.notes = utils.toObject(allNotes, note => [note.noteId, note]); - this.apis = utils.toObject(allNotes, note => [note.noteId, new BackendScriptApi(note, apiParams)]); - this.require = moduleNoteIds => { - return moduleName => { - const candidates = allNotes.filter(note => moduleNoteIds.includes(note.noteId)); - const note = candidates.find(c => c.title === moduleName); - - if (!note) { - return require(moduleName); - } - - return this.modules[note.noteId].exports; - } - }; -} - -module.exports = ScriptContext; diff --git a/src/services/script_context.ts b/src/services/script_context.ts new file mode 100644 index 000000000..4de4863d0 --- /dev/null +++ b/src/services/script_context.ts @@ -0,0 +1,37 @@ +import utils = require('./utils'); +import BackendScriptApi = require('./backend_script_api'); +import BNote = require('../becca/entities/bnote'); +import { ApiParams } from './backend_script_api_interface'; + +type Module = { + exports: any[]; +}; + +class ScriptContext { + modules: Record; + notes: {}; + apis: {}; + allNotes: BNote[]; + + constructor(allNotes: BNote[], apiParams: ApiParams) { + this.allNotes = allNotes; + this.modules = {}; + this.notes = utils.toObject(allNotes, note => [note.noteId, note]); + this.apis = utils.toObject(allNotes, note => [note.noteId, new BackendScriptApi(note, apiParams)]); + } + + require(moduleNoteIds: string[]) { + return (moduleName: string) => { + const candidates = this.allNotes.filter(note => moduleNoteIds.includes(note.noteId)); + const note = candidates.find(c => c.title === moduleName); + + if (!note) { + return require(moduleName); + } + + return this.modules[note.noteId].exports; + } + }; +} + +export = ScriptContext; diff --git a/src/services/search/services/search.ts b/src/services/search/services/search.ts index c986b80bb..7461d5b4e 100644 --- a/src/services/search/services/search.ts +++ b/src/services/search/services/search.ts @@ -78,7 +78,7 @@ function searchFromRelation(note: BNote, relationName: string) { return []; } - const scriptService = require('../../script.js'); // to avoid circular dependency + const scriptService = require('../../script'); // TODO: to avoid circular dependency const result = scriptService.executeNote(scriptNote, {originEntity: note}); if (!Array.isArray(result)) { diff --git a/src/services/ws.ts b/src/services/ws.ts index 437379371..f13bc312d 100644 --- a/src/services/ws.ts +++ b/src/services/ws.ts @@ -42,6 +42,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;