add "api.runOnFrontend()" to the backend script API

This commit is contained in:
zadam 2023-08-30 23:18:16 +02:00
parent 8da5b90aea
commit 6f7fbacca1
9 changed files with 302 additions and 20 deletions

View file

@ -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"> &rarr; {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.&lt;?></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"> &rarr; {<a href="BNote.html">BNote</a>|null}</span></h4>

View file

@ -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.&lt;?>} 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.
*

View file

@ -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);
}

View file

@ -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);

View file

@ -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 = [];

View file

@ -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 = {

View file

@ -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

View file

@ -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.
*

View file

@ -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;