From 6f7fbacca1a1b2c9cb57624d11cfb5bdea42a6b5 Mon Sep 17 00:00:00 2001 From: zadam Date: Wed, 30 Aug 2023 23:18:16 +0200 Subject: [PATCH] add "api.runOnFrontend()" to the backend script API --- docs/backend_api/BackendScriptApi.html | 187 +++++++++++++++++- .../services_backend_script_api.js.html | 42 ++++ src/public/app/services/bundle.js | 7 +- src/public/app/services/render.js | 2 +- src/public/app/services/ws.js | 7 + src/routes/api/script.js | 3 +- src/routes/routes.js | 2 +- src/services/backend_script_api.js | 42 ++++ src/services/script.js | 30 +-- 9 files changed, 302 insertions(+), 20 deletions(-) diff --git a/docs/backend_api/BackendScriptApi.html b/docs/backend_api/BackendScriptApi.html index b5c348b05..416bf48af 100644 --- a/docs/backend_api/BackendScriptApi.html +++ b/docs/backend_api/BackendScriptApi.html @@ -240,7 +240,7 @@ available in the JS backend notes. You can use e.g. api.log(api.startNote.
Source:
@@ -6381,6 +6381,191 @@ if some action needs to happen on only one specific instance. +

runOnFrontend(script, params) → {undefined}

+ + + + + + +
+ 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. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
script + + +string + + + + script to be executed on the frontend
params + + +Array.<?> + + + + list of parameters to the anonymous function to be sent to frontend
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ - no return value is provided. +
+ + + +
+
+ Type +
+
+ +undefined + + +
+
+ + + + + + + + + + + + +

searchForNote(query, searchParamsopt) → {BNote|null}

diff --git a/docs/backend_api/services_backend_script_api.js.html b/docs/backend_api/services_backend_script_api.js.html index d33d0df11..a0a6c60f1 100644 --- a/docs/backend_api/services_backend_script_api.js.html +++ b/docs/backend_api/services_backend_script_api.js.html @@ -557,6 +557,48 @@ function BackendScriptApi(currentNote, apiParams) { */ 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(); + } + + ws.sendMessageToAllClients({ + type: 'execute-script', + script: script, + params: prepareParams(params), + 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 + }); + + function prepareParams(params) { + if (!params) { + return params; + } + + return params.map(p => { + if (typeof p === "function") { + return `!@#Function: ${p.toString()}`; + } + else { + return p; + } + }); + } + }; + /** * This object contains "at your risk" and "no BC guarantees" objects for advanced use cases. * diff --git a/src/public/app/services/bundle.js b/src/public/app/services/bundle.js index 40658f692..a55abfe4a 100644 --- a/src/public/app/services/bundle.js +++ b/src/public/app/services/bundle.js @@ -4,8 +4,11 @@ import toastService from "./toast.js"; import froca from "./froca.js"; import utils from "./utils.js"; -async function getAndExecuteBundle(noteId, originEntity = null) { - const bundle = await server.get(`script/bundle/${noteId}`); +async function getAndExecuteBundle(noteId, originEntity = null, script = null, params = null) { + const bundle = await server.post(`script/bundle/${noteId}`, { + script, + params + }); return await executeBundle(bundle, originEntity); } diff --git a/src/public/app/services/render.js b/src/public/app/services/render.js index c7b3f0b2c..e7ecf8d28 100644 --- a/src/public/app/services/render.js +++ b/src/public/app/services/render.js @@ -10,7 +10,7 @@ async function render(note, $el) { $el.empty().toggle(renderNoteIds.length > 0); for (const renderNoteId of renderNoteIds) { - const bundle = await server.get(`script/bundle/${renderNoteId}`); + const bundle = await server.post(`script/bundle/${renderNoteId}`); const $scriptContainer = $('
'); $el.append($scriptContainer); diff --git a/src/public/app/services/ws.js b/src/public/app/services/ws.js index 357a20fe3..a5fbe60a3 100644 --- a/src/public/app/services/ws.js +++ b/src/public/app/services/ws.js @@ -125,6 +125,13 @@ async function handleMessage(event) { else if (message.type === 'toast') { toastService.showMessage(message.message); } + else if (message.type === 'execute-script') { + const bundleService = (await import("../services/bundle.js")).default; + const froca = (await import("../services/froca.js")).default; + const originEntity = message.originEntityId ? await froca.getNote(message.originEntityId) : null; + + bundleService.getAndExecuteBundle(message.currentNoteId, originEntity, message.script, message.params); + } } let entityChangeIdReachedListeners = []; diff --git a/src/routes/api/script.js b/src/routes/api/script.js index b591307df..4f94bfe17 100644 --- a/src/routes/api/script.js +++ b/src/routes/api/script.js @@ -107,8 +107,9 @@ function getRelationBundles(req) { function getBundle(req) { const note = becca.getNote(req.params.noteId); + const {script, params} = req.body; - return scriptService.getScriptBundleForFrontend(note); + return scriptService.getScriptBundleForFrontend(note, script, params); } module.exports = { diff --git a/src/routes/routes.js b/src/routes/routes.js index a7f540d09..50ef93634 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -302,7 +302,7 @@ function register(app) { apiRoute(PST, '/api/script/run/:noteId', scriptRoute.run); apiRoute(GET, '/api/script/startup', scriptRoute.getStartupBundles); apiRoute(GET, '/api/script/widgets', scriptRoute.getWidgetBundles); - apiRoute(GET, '/api/script/bundle/:noteId', scriptRoute.getBundle); + apiRoute(PST, '/api/script/bundle/:noteId', scriptRoute.getBundle); apiRoute(GET, '/api/script/relation/:noteId/:relationName', scriptRoute.getRelationBundles); // no CSRF since this is called from android app diff --git a/src/services/backend_script_api.js b/src/services/backend_script_api.js index ed0e0902c..3cad85535 100644 --- a/src/services/backend_script_api.js +++ b/src/services/backend_script_api.js @@ -529,6 +529,48 @@ function BackendScriptApi(currentNote, apiParams) { */ 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(); + } + + ws.sendMessageToAllClients({ + type: 'execute-script', + script: script, + params: prepareParams(params), + 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 + }); + + function prepareParams(params) { + if (!params) { + return params; + } + + return params.map(p => { + if (typeof p === "function") { + return `!@#Function: ${p.toString()}`; + } + else { + return p; + } + }); + } + }; + /** * This object contains "at your risk" and "no BC guarantees" objects for advanced use cases. * diff --git a/src/services/script.js b/src/services/script.js index 55538ff44..5650b8a9d 100644 --- a/src/services/script.js +++ b/src/services/script.js @@ -10,7 +10,7 @@ function executeNote(note, apiParams) { return; } - const bundle = getScriptBundle(note); + const bundle = getScriptBundle(note, true, 'backend'); return executeBundle(bundle, apiParams); } @@ -68,9 +68,9 @@ function executeScript(script, params, startNoteId, currentNoteId, originEntityN // 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 backendOverrideContent = `return (${script}\r\n)(${getParams(params)})`; + const overrideContent = `return (${script}\r\n)(${getParams(params)})`; - const bundle = getScriptBundle(currentNote, true, null, [], backendOverrideContent); + const bundle = getScriptBundle(currentNote, true, 'backend', [], overrideContent); return executeBundle(bundle, { startNote, originEntity }); } @@ -96,9 +96,17 @@ function getParams(params) { /** * @param {BNote} note + * @param {string} [script] + * @param {Array} [params] */ -function getScriptBundleForFrontend(note) { - const bundle = getScriptBundle(note); +function getScriptBundleForFrontend(note, script, params) { + let overrideContent = null; + + if (script) { + overrideContent = `return (${script}\r\n)(${getParams(params)})`; + } + + const bundle = getScriptBundle(note, true, 'frontend', [], overrideContent); if (!bundle) { return; @@ -119,9 +127,9 @@ function getScriptBundleForFrontend(note) { * @param {boolean} [root=true] * @param {string|null} [scriptEnv] * @param {string[]} [includedNoteIds] - * @param {string|null} [backendOverrideContent] + * @param {string|null} [overrideContent] */ -function getScriptBundle(note, root = true, scriptEnv = null, includedNoteIds = [], backendOverrideContent = null) { +function getScriptBundle(note, root = true, scriptEnv = null, includedNoteIds = [], overrideContent = null) { if (!note.isContentAvailable()) { return; } @@ -134,12 +142,6 @@ function getScriptBundle(note, root = true, scriptEnv = null, includedNoteIds = return; } - if (root) { - scriptEnv = backendOverrideContent - ? 'backend' - : note.getScriptEnv(); - } - if (note.type !== 'file' && !root && scriptEnv !== note.getScriptEnv()) { return; } @@ -180,7 +182,7 @@ function getScriptBundle(note, root = true, scriptEnv = null, includedNoteIds = apiContext.modules['${note.noteId}'] = { exports: {} }; ${root ? 'return ' : ''}${isFrontend ? 'await' : ''} ((${isFrontend ? 'async' : ''} function(exports, module, require, api${modules.length > 0 ? ', ' : ''}${modules.map(child => sanitizeVariableName(child.title)).join(', ')}) { try { -${backendOverrideContent || note.getContent()}; +${overrideContent || note.getContent()}; } catch (e) { throw new Error("Load of script note \\"${note.title}\\" (${note.noteId}) failed with: " + e.message); } for (const exportKey in exports) module.exports[exportKey] = exports[exportKey]; return module.exports;