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> <dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li> <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> </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> <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); 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. * 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 froca from "./froca.js";
import utils from "./utils.js"; import utils from "./utils.js";
async function getAndExecuteBundle(noteId, originEntity = null) { async function getAndExecuteBundle(noteId, originEntity = null, script = null, params = null) {
const bundle = await server.get(`script/bundle/${noteId}`); const bundle = await server.post(`script/bundle/${noteId}`, {
script,
params
});
return await executeBundle(bundle, originEntity); return await executeBundle(bundle, originEntity);
} }

View File

@ -10,7 +10,7 @@ async function render(note, $el) {
$el.empty().toggle(renderNoteIds.length > 0); $el.empty().toggle(renderNoteIds.length > 0);
for (const renderNoteId of renderNoteIds) { for (const renderNoteId of renderNoteIds) {
const bundle = await server.get(`script/bundle/${renderNoteId}`); const bundle = await server.post(`script/bundle/${renderNoteId}`);
const $scriptContainer = $('<div>'); const $scriptContainer = $('<div>');
$el.append($scriptContainer); $el.append($scriptContainer);

View File

@ -125,6 +125,13 @@ async function handleMessage(event) {
else if (message.type === 'toast') { else if (message.type === 'toast') {
toastService.showMessage(message.message); 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 = []; let entityChangeIdReachedListeners = [];

View File

@ -107,8 +107,9 @@ function getRelationBundles(req) {
function getBundle(req) { function getBundle(req) {
const note = becca.getNote(req.params.noteId); const note = becca.getNote(req.params.noteId);
const {script, params} = req.body;
return scriptService.getScriptBundleForFrontend(note); return scriptService.getScriptBundleForFrontend(note, script, params);
} }
module.exports = { module.exports = {

View File

@ -302,7 +302,7 @@ function register(app) {
apiRoute(PST, '/api/script/run/:noteId', scriptRoute.run); apiRoute(PST, '/api/script/run/:noteId', scriptRoute.run);
apiRoute(GET, '/api/script/startup', scriptRoute.getStartupBundles); apiRoute(GET, '/api/script/startup', scriptRoute.getStartupBundles);
apiRoute(GET, '/api/script/widgets', scriptRoute.getWidgetBundles); 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); apiRoute(GET, '/api/script/relation/:noteId/:relationName', scriptRoute.getRelationBundles);
// no CSRF since this is called from android app // 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); 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. * 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; return;
} }
const bundle = getScriptBundle(note); const bundle = getScriptBundle(note, true, 'backend');
return executeBundle(bundle, apiParams); 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 // 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 // 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 }); return executeBundle(bundle, { startNote, originEntity });
} }
@ -96,9 +96,17 @@ function getParams(params) {
/** /**
* @param {BNote} note * @param {BNote} note
* @param {string} [script]
* @param {Array} [params]
*/ */
function getScriptBundleForFrontend(note) { function getScriptBundleForFrontend(note, script, params) {
const bundle = getScriptBundle(note); let overrideContent = null;
if (script) {
overrideContent = `return (${script}\r\n)(${getParams(params)})`;
}
const bundle = getScriptBundle(note, true, 'frontend', [], overrideContent);
if (!bundle) { if (!bundle) {
return; return;
@ -119,9 +127,9 @@ function getScriptBundleForFrontend(note) {
* @param {boolean} [root=true] * @param {boolean} [root=true]
* @param {string|null} [scriptEnv] * @param {string|null} [scriptEnv]
* @param {string[]} [includedNoteIds] * @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()) { if (!note.isContentAvailable()) {
return; return;
} }
@ -134,12 +142,6 @@ function getScriptBundle(note, root = true, scriptEnv = null, includedNoteIds =
return; return;
} }
if (root) {
scriptEnv = backendOverrideContent
? 'backend'
: note.getScriptEnv();
}
if (note.type !== 'file' && !root && scriptEnv !== note.getScriptEnv()) { if (note.type !== 'file' && !root && scriptEnv !== note.getScriptEnv()) {
return; return;
} }
@ -180,7 +182,7 @@ function getScriptBundle(note, root = true, scriptEnv = null, includedNoteIds =
apiContext.modules['${note.noteId}'] = { exports: {} }; 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(', ')}) { ${root ? 'return ' : ''}${isFrontend ? 'await' : ''} ((${isFrontend ? 'async' : ''} function(exports, module, require, api${modules.length > 0 ? ', ' : ''}${modules.map(child => sanitizeVariableName(child.title)).join(', ')}) {
try { try {
${backendOverrideContent || note.getContent()}; ${overrideContent || note.getContent()};
} catch (e) { throw new Error("Load of script note \\"${note.title}\\" (${note.noteId}) failed with: " + e.message); } } 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]; for (const exportKey in exports) module.exports[exportKey] = exports[exportKey];
return module.exports; return module.exports;