diff --git a/src/public/app/services/frontend_script_api.js b/src/public/app/services/frontend_script_api.js index e341e8382..989a49879 100644 --- a/src/public/app/services/frontend_script_api.js +++ b/src/public/app/services/frontend_script_api.js @@ -178,36 +178,73 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain } /** - * Executes given anonymous function on the backend. - * Internally this serializes the anonymous function into string and sends it to backend via AJAX. - * - * @method - * @param {string|Function} script - script to be executed on the backend - * @param {Array} params - list of parameters to the anonymous function to be sent to backend - * @returns {Promise} return value of the executed function on the backend + * @private */ - this.runOnBackend = async (script, params = []) => { - if (typeof script === "function") { - script = script.toString(); + this.__runOnBackendInner = async (func, params, transactional) => { + if (typeof func === "function") { + func = func.toString(); } const ret = await server.post('script/exec', { - script: script, + script: func, params: prepareParams(params), startNoteId: startNote.noteId, currentNoteId: currentNote.noteId, originEntityName: "notes", // currently there's no other entity on the frontend which can trigger event - originEntityId: originEntity ? originEntity.noteId : null + originEntityId: originEntity ? originEntity.noteId : null, + transactional }, "script"); if (ret.success) { await ws.waitForMaxKnownEntityChangeId(); return ret.executionResult; - } - else { + } else { throw new Error(`server error: ${ret.error}`); } + } + + /** + * Executes given anonymous function on the backend. + * Internally this serializes the anonymous function into string and sends it to backend via AJAX. + * Please make sure that the supplied function is synchronous. Only sync functions will work correctly + * with transaction management. If you really know what you're doing, you can call api.runAsyncOnBackendWithManualTransactionHandling() + * + * @method + * @param {function|string} func - (synchronous) function to be executed on the backend + * @param {Array.} params - list of parameters to the anonymous function to be sent to backend + * @returns {Promise<*>} return value of the executed function on the backend + */ + this.runOnBackend = async (func, params = []) => { + if (func?.constructor.name === "AsyncFunction" || func?.startsWith?.("async ")) { + toastService.showError("You're passing an async function to api.runOnBackend() which will likely not work as you intended. " + + "Either make the function synchronous (by removing 'async' keyword), or use api.runAsyncOnBackendWithManualTransactionHandling()"); + } + + return await this.__runOnBackendInner(func, params, true); + }; + + /** + * Executes given anonymous function on the backend. + * Internally this serializes the anonymous function into string and sends it to backend via AJAX. + * This function is meant for advanced needs where an async function is necessary. + * In this case, the automatic request-scoped transaction management is not applied, + * and you need to manually define transaction via api.transactional(). + * + * If you have a synchronous function, please use api.runOnBackend(). + * + * @method + * @param {function|string} func - (synchronous) function to be executed on the backend + * @param {Array.} params - list of parameters to the anonymous function to be sent to backend + * @returns {Promise<*>} return value of the executed function on the backend + */ + this.runAsyncOnBackendWithManualTransactionHandling = async (func, params = []) => { + if (func?.constructor.name === "Function" || func?.startsWith?.("function")) { + toastService.showError("You're passing a synchronous function to api.runAsyncOnBackendWithManualTransactionHandling(), " + + "while you should likely use api.runOnBackend() instead."); + } + + return await this.__runOnBackendInner(func, params, false); }; /** @@ -500,7 +537,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain * @param {string} date - e.g. "2019-04-29" * @returns {Promise} */ - this.getWeekNote = dateNotesService.getWeekNote; + this.getWeekNote = dateNotesService.getWeekNote; /** * Returns month-note. If it doesn't exist, it is automatically created. diff --git a/src/routes/api/script.js b/src/routes/api/script.js index 4f94bfe17..43b726aff 100644 --- a/src/routes/api/script.js +++ b/src/routes/api/script.js @@ -4,12 +4,16 @@ const scriptService = require('../../services/script'); const attributeService = require('../../services/attributes'); const becca = require('../../becca/becca'); const syncService = require('../../services/sync'); +const sql = require('../../services/sql'); +// The async/await here is very confusing, because the body.script may, but may not be async. If it is async, then we +// need to await it and make the complete response including metadata available in a Promise, so that the route detects +// this and does result.then(). async function exec(req) { try { const {body} = req; - const result = await scriptService.executeScript( + const execute = body => scriptService.executeScript( body.script, body.params, body.startNoteId, @@ -18,6 +22,10 @@ async function exec(req) { body.originEntityId ); + const result = body.transactional + ? sql.transactional(() => execute(body)) + : await execute(body); + return { success: true, executionResult: result, diff --git a/src/routes/routes.js b/src/routes/routes.js index 1cbe4cd83..304761920 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -302,7 +302,8 @@ function register(app) { apiRoute(GET, '/api/database/check-integrity', databaseRoute.checkIntegrity); - apiRoute(PST, '/api/script/exec', scriptRoute.exec); + route(PST, '/api/script/exec', [auth.checkApiAuth, csrfMiddleware], scriptRoute.exec, apiResultHandler, false); + apiRoute(PST, '/api/script/run/:noteId', scriptRoute.run); apiRoute(GET, '/api/script/startup', scriptRoute.getStartupBundles); apiRoute(GET, '/api/script/widgets', scriptRoute.getWidgetBundles); @@ -449,7 +450,7 @@ function route(method, path, middleware, routeHandler, resultHandler = null, tra return; } - if (result && result.then) { // promise + if (result?.then) { // promise result .then(promiseResult => handleResponse(resultHandler, req, res, promiseResult, start)) .catch(e => handleException(e, method, path, res));