mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	add "api.runOnFrontend()" to the backend script API
This commit is contained in:
		| @@ -240,7 +240,7 @@ available in the JS backend notes. You can use e.g. <code>api.log(api.startNote. | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line537">line 537</a> | ||||
|         <a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line579">line 579</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
| @@ -6381,6 +6381,191 @@ if some action needs to happen on only one specific instance. | ||||
|      | ||||
|  | ||||
|      | ||||
|     <h4 class="name" id="runOnFrontend"><span class="type-signature"></span>runOnFrontend<span class="signature">(script, params)</span><span class="type-signature"> → {undefined}</span></h4> | ||||
|      | ||||
|  | ||||
|      | ||||
|  | ||||
|  | ||||
|  | ||||
| <div class="description"> | ||||
|     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. | ||||
| </div> | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|     <h5>Parameters:</h5> | ||||
|      | ||||
|  | ||||
| <table class="params"> | ||||
|     <thead> | ||||
|     <tr> | ||||
|          | ||||
|         <th>Name</th> | ||||
|          | ||||
|  | ||||
|         <th>Type</th> | ||||
|  | ||||
|          | ||||
|  | ||||
|          | ||||
|  | ||||
|         <th class="last">Description</th> | ||||
|     </tr> | ||||
|     </thead> | ||||
|  | ||||
|     <tbody> | ||||
|      | ||||
|  | ||||
|         <tr> | ||||
|              | ||||
|                 <td class="name"><code>script</code></td> | ||||
|              | ||||
|  | ||||
|             <td class="type"> | ||||
|              | ||||
|                  | ||||
| <span class="param-type">string</span> | ||||
|  | ||||
|  | ||||
|              | ||||
|             </td> | ||||
|  | ||||
|              | ||||
|  | ||||
|              | ||||
|  | ||||
|             <td class="description last">script to be executed on the frontend</td> | ||||
|         </tr> | ||||
|  | ||||
|      | ||||
|  | ||||
|         <tr> | ||||
|              | ||||
|                 <td class="name"><code>params</code></td> | ||||
|              | ||||
|  | ||||
|             <td class="type"> | ||||
|              | ||||
|                  | ||||
| <span class="param-type">Array.<?></span> | ||||
|  | ||||
|  | ||||
|              | ||||
|             </td> | ||||
|  | ||||
|              | ||||
|  | ||||
|              | ||||
|  | ||||
|             <td class="description last">list of parameters to the anonymous function to be sent to frontend</td> | ||||
|         </tr> | ||||
|  | ||||
|      | ||||
|     </tbody> | ||||
| </table> | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| <dl class="details"> | ||||
|  | ||||
|      | ||||
|  | ||||
|      | ||||
|  | ||||
|      | ||||
|  | ||||
|      | ||||
|  | ||||
|      | ||||
|  | ||||
|      | ||||
|  | ||||
|      | ||||
|  | ||||
|      | ||||
|  | ||||
|      | ||||
|  | ||||
|      | ||||
|  | ||||
|      | ||||
|  | ||||
|      | ||||
|  | ||||
|      | ||||
|     <dt class="tag-source">Source:</dt> | ||||
|     <dd class="tag-source"><ul class="dummy"><li> | ||||
|         <a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line543">line 543</a> | ||||
|     </li></ul></dd> | ||||
|      | ||||
|  | ||||
|      | ||||
|  | ||||
|      | ||||
|  | ||||
|      | ||||
| </dl> | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| <h5>Returns:</h5> | ||||
|  | ||||
|          | ||||
| <div class="param-desc"> | ||||
|     - no return value is provided. | ||||
| </div> | ||||
|  | ||||
|  | ||||
|  | ||||
| <dl> | ||||
|     <dt> | ||||
|         Type | ||||
|     </dt> | ||||
|     <dd> | ||||
|          | ||||
| <span class="param-type">undefined</span> | ||||
|  | ||||
|  | ||||
|     </dd> | ||||
| </dl> | ||||
|  | ||||
|      | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|          | ||||
|              | ||||
|  | ||||
|      | ||||
|  | ||||
|      | ||||
|     <h4 class="name" id="searchForNote"><span class="type-signature"></span>searchForNote<span class="signature">(query, searchParams<span class="signature-attributes">opt</span>)</span><span class="type-signature"> → {<a href="BNote.html">BNote</a>|null}</span></h4> | ||||
|      | ||||
|  | ||||
|   | ||||
| @@ -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. | ||||
|      * | ||||
|   | ||||
| @@ -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); | ||||
| } | ||||
|   | ||||
| @@ -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 = $('<div>'); | ||||
|         $el.append($scriptContainer); | ||||
|   | ||||
| @@ -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 = []; | ||||
|   | ||||
| @@ -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 = { | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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. | ||||
|      * | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user