diff --git a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-permission.js b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-permission.js index fe742956612..c8f200fb09b 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-permission.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-permission.js @@ -23,7 +23,7 @@ var Command = function(params,commander,callback) { this.callback = callback; }; -Command.prototype.execute = function() { +Command.prototype.execute = async function() { var self = this; if(this.params.length < 2) { @@ -37,7 +37,7 @@ Command.prototype.execute = function() { var permission_name = this.params[0]; var description = this.params[1]; - $tw.mws.store.sqlTiddlerDatabase.createPermission(permission_name, description); + await $tw.mws.store.sqlTiddlerDatabase.createPermission(permission_name, description); self.callback(); return null; }; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-role.js b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-role.js index ec435a97ff3..07dfdf6163a 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-role.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-role.js @@ -23,7 +23,7 @@ var Command = function(params,commander,callback) { this.callback = callback; }; -Command.prototype.execute = function() { +Command.prototype.execute = async function() { var self = this; if(this.params.length < 2) { @@ -37,7 +37,7 @@ Command.prototype.execute = function() { var role_name = this.params[0]; var description = this.params[1]; - $tw.mws.store.sqlTiddlerDatabase.createRole(role_name, description); + await $tw.mws.store.sqlTiddlerDatabase.createRole(role_name, description); self.callback(null, "Role Created Successfully!"); return null; }; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-user.js b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-user.js index fc0c4e6e197..e2f952aba5e 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-user.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-user.js @@ -25,7 +25,7 @@ var Command = function(params,commander,callback) { this.callback = callback; }; -Command.prototype.execute = function() { +Command.prototype.execute = async function() { var self = this; if(this.params.length < 2) { @@ -41,10 +41,10 @@ Command.prototype.execute = function() { var email = this.params[2] || username + "@example.com"; var hashedPassword = crypto.createHash("sha256").update(password).digest("hex"); - var user = $tw.mws.store.sqlTiddlerDatabase.getUserByUsername(username); + var user = await $tw.mws.store.sqlTiddlerDatabase.getUserByUsername(username); if(!user) { - $tw.mws.store.sqlTiddlerDatabase.createUser(username, email, hashedPassword); + await $tw.mws.store.sqlTiddlerDatabase.createUser(username, email, hashedPassword); console.log("User Account Created Successfully with username: " + username + " and password: " + password); self.callback(); } diff --git a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-assign-role-permission.js b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-assign-role-permission.js index 89ed568d932..b4a44a5c3bc 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-assign-role-permission.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-assign-role-permission.js @@ -23,7 +23,7 @@ var Command = function(params,commander,callback) { this.callback = callback; }; -Command.prototype.execute = function() { +Command.prototype.execute = async function() { var self = this; if(this.params.length < 2) { @@ -36,8 +36,8 @@ Command.prototype.execute = function() { var role_name = this.params[0]; var permission_name = this.params[1]; - var role = $tw.mws.store.sqlTiddlerDatabase.getRoleByName(role_name); - var permission = $tw.mws.store.sqlTiddlerDatabase.getPermissionByName(permission_name); + var role = await $tw.mws.store.sqlTiddlerDatabase.getRoleByName(role_name); + var permission = await $tw.mws.store.sqlTiddlerDatabase.getPermissionByName(permission_name); if(!role) { return "Error: Unable to find Role: "+role_name; @@ -47,10 +47,10 @@ Command.prototype.execute = function() { return "Error: Unable to find Permission: "+permission_name; } - var permission = $tw.mws.store.sqlTiddlerDatabase.getPermissionByName(permission_name); + var permission = await $tw.mws.store.sqlTiddlerDatabase.getPermissionByName(permission_name); - $tw.mws.store.sqlTiddlerDatabase.addPermissionToRole(role.role_id, permission.permission_id); + await $tw.mws.store.sqlTiddlerDatabase.addPermissionToRole(role.role_id, permission.permission_id); self.callback(); return null; }; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-assign-user-role.js b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-assign-user-role.js index 2657dbdd332..76102d5d449 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-assign-user-role.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-assign-user-role.js @@ -23,7 +23,7 @@ var Command = function(params,commander,callback) { this.callback = callback; }; -Command.prototype.execute = function() { +Command.prototype.execute = async function() { var self = this; if(this.params.length < 2) { @@ -36,8 +36,8 @@ Command.prototype.execute = function() { var username = this.params[0]; var role_name = this.params[1]; - var role = $tw.mws.store.sqlTiddlerDatabase.getRoleByName(role_name); - var user = $tw.mws.store.sqlTiddlerDatabase.getUserByUsername(username); + var role = await $tw.mws.store.sqlTiddlerDatabase.getRoleByName(role_name); + var user = await $tw.mws.store.sqlTiddlerDatabase.getUserByUsername(username); if(!role) { return "Error: Unable to find Role: "+role_name; @@ -47,7 +47,7 @@ Command.prototype.execute = function() { return "Error: Unable to find user with the username "+username; } - $tw.mws.store.sqlTiddlerDatabase.addRoleToUser(user.user_id, role.role_id); + await $tw.mws.store.sqlTiddlerDatabase.addRoleToUser(user.user_id, role.role_id); console.log(role_name+" role has been assigned to user with username "+username) self.callback(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-create-bag.js b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-create-bag.js index c90088a8729..c33c6dfcd49 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-create-bag.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-create-bag.js @@ -25,7 +25,7 @@ var Command = function(params,commander,callback) { this.callback = callback; }; -Command.prototype.execute = function() { +Command.prototype.execute = async function() { var self = this; // Check parameters if(this.params.length < 1) { @@ -34,7 +34,7 @@ Command.prototype.execute = function() { var bagName = this.params[0], bagDescription = this.params[1] || bagName; // Create bag - var result = $tw.mws.store.createBag(bagName,bagDescription); + var result = await $tw.mws.store.createBag(bagName,bagDescription); if(result) { return result.message; } else { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-create-recipe.js b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-create-recipe.js index 6515c817a20..d1d070d37e1 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-create-recipe.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-create-recipe.js @@ -27,7 +27,7 @@ var Command = function(params,commander,callback) { this.callback = callback; }; -Command.prototype.execute = function() { +Command.prototype.execute = async function() { var self = this; // Check parameters if(this.params.length < 1) { @@ -35,9 +35,9 @@ Command.prototype.execute = function() { } var recipeName = this.params[0], bagList = (this.params[1] || "").split(" "), - recipeDescription = this.params[2] || recipeNameName; + recipeDescription = this.params[2]; // Create recipe - var result = $tw.mws.store.createRecipe(recipeName,bagList,recipeDescription); + var result = await $tw.mws.store.createRecipe(recipeName,bagList,recipeDescription); if(result) { return result.message; } else { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-load-archive.js b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-load-archive.js index 9e37cb32b08..36a6aea347c 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-load-archive.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-load-archive.js @@ -14,7 +14,7 @@ Command to load archive of recipes, bags and tiddlers from a directory exports.info = { name: "mws-load-archive", - synchronous: true + synchronous: false }; var Command = function(params,commander,callback) { @@ -23,18 +23,18 @@ var Command = function(params,commander,callback) { this.callback = callback; }; -Command.prototype.execute = function() { - var self = this; +Command.prototype.execute = async function() { // Check parameters if(this.params.length < 1) { return "Missing pathname"; } var archivePath = this.params[0]; - loadBackupArchive(archivePath); + await loadBackupArchive(archivePath); + this.callback(); return null; }; -function loadBackupArchive(archivePath) { +async function loadBackupArchive(archivePath) { const fs = require("fs"), path = require("path"); // Iterate the bags @@ -43,7 +43,7 @@ function loadBackupArchive(archivePath) { const bagName = decodeURIComponent(bagFilename); console.log(`Reading bag ${bagName}`); const bagInfo = JSON.parse(fs.readFileSync(path.resolve(archivePath,"bags",bagFilename,"meta.json"),"utf8")); - $tw.mws.store.createBag(bagName,bagInfo.description,bagInfo.accesscontrol); + await $tw.mws.store.createBag(bagName,bagInfo.description,bagInfo.accesscontrol); if(fs.existsSync(path.resolve(archivePath,"bags",bagFilename,"tiddlers"))) { const tiddlerFilenames = fs.readdirSync(path.resolve(archivePath,"bags",bagFilename,"tiddlers")); for(const tiddlerFilename of tiddlerFilenames) { @@ -52,7 +52,7 @@ function loadBackupArchive(archivePath) { jsonTiddler = fs.readFileSync(tiddlerPath,"utf8"), tiddler = sanitiseTiddler(JSON.parse(jsonTiddler)); if(tiddler && tiddler.title) { - $tw.mws.store.saveBagTiddler(tiddler,bagName); + await $tw.mws.store.saveBagTiddler(tiddler,bagName); } else { console.log(`Malformed JSON tiddler in file ${tiddlerPath}`); } @@ -66,7 +66,7 @@ function loadBackupArchive(archivePath) { if(recipeFilename.endsWith(".json")) { const recipeName = decodeURIComponent(recipeFilename.substring(0,recipeFilename.length - ".json".length)); const jsonInfo = JSON.parse(fs.readFileSync(path.resolve(archivePath,"recipes",recipeFilename),"utf8")); - $tw.mws.store.createRecipe(recipeName,jsonInfo.bag_names,jsonInfo.description,jsonInfo.accesscontrol); + await $tw.mws.store.createRecipe(recipeName,jsonInfo.bag_names,jsonInfo.description,jsonInfo.accesscontrol); } } }; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-load-plugin-bags.js b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-load-plugin-bags.js index 40bfb37493d..a7fcf220ba6 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-load-plugin-bags.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-load-plugin-bags.js @@ -23,57 +23,71 @@ var Command = function(params,commander,callback) { this.callback = callback; }; -Command.prototype.execute = function() { - var self = this; - loadPluginBags(); +Command.prototype.execute = async function() { + await loadPluginBags(); return null; }; -function loadPluginBags() { +async function loadPluginBags() { const path = require("path"), fs = require("fs"); // Copy plugins var makePluginBagName = function(type,publisher,name) { return "$:/" + type + "/" + (publisher ? publisher + "/" : "") + name; }, - savePlugin = function(pluginFields,type,publisher,name) { + savePlugin = async function(pluginFields,type,publisher,name) { const bagName = makePluginBagName(type,publisher,name); - const result = $tw.mws.store.createBag(bagName,pluginFields.description || "(no description)",{allowPrivilegedCharacters: true}); + const result = await $tw.mws.store.createBag(bagName,pluginFields.description || "(no description)",{allowPrivilegedCharacters: true}); if(result) { - console.log(`Error creating plugin bag ${bagname}: ${JSON.stringify(result)}`); + console.log(`Error creating plugin bag ${bagName}: ${JSON.stringify(result)}`); } - $tw.mws.store.saveBagTiddler(pluginFields,bagName); + await $tw.mws.store.saveBagTiddler(pluginFields,bagName); }, - collectPlugins = function(folder,type,publisher) { + collectPlugins = async function(folder,type,publisher) { var pluginFolders = $tw.utils.getSubdirectories(folder) || []; for(var p=0; p void} options.cbPartStart + * @param {(chunk: Buffer) => void} options.cbPartChunk + * @param {() => void} options.cbPartEnd + * @param {(err: string) => void} options.cbFinished + */ function streamMultipartData(request,options) { // Check that the Content-Type is multipart/form-data const contentType = request.headers['content-type']; @@ -215,6 +235,7 @@ function streamMultipartData(request,options) { } // Extract and parse headers const headersPart = Uint8Array.prototype.slice.call(buffer,boundaryIndex + boundaryBuffer.length,endOfHeaders).toString(); + /** @type {Record} */ const currentHeaders = {}; headersPart.split("\r\n").forEach(headerLine => { const [key, value] = headerLine.split(": "); @@ -292,16 +313,49 @@ Server.prototype.defaultVariables = { "system-tiddler-render-type": "text/plain", "system-tiddler-render-template": "$:/core/templates/wikified-tiddler", "debug-level": "none", + /** @type {"yes" | "no"} */ "gzip": "no", - "use-browser-cache": "no" -}; + /** @type {"yes" | "no"} */ + "use-browser-cache": "no", + "path-prefix": "", + /** @type {"yes" | "no"} */ + "csrf-disable": "no", + /** @type {string | undefined} */ + username: undefined, + /** @type {string | undefined} */ + password: undefined, + /** @type {string | undefined} */ + credentials: undefined, + /** @type {string | undefined} */ + readers: undefined, + /** @type {string | undefined} */ + writers: undefined, + /** @type {string | undefined} */ + admin: undefined, + /** @type {string | undefined} TLS Private Key file path */ + "tls-key": undefined, + /** @type {string | undefined} TLS Public Cert file path */ + "tls-cert": undefined, + /** @type {string | undefined} TLS Private Key passphrase */ + "tls-passphrase": undefined, + /** @type {string | undefined} Server name, mostly for 403 errors */ + "server-name": undefined, + /** @type {string | undefined} the expected origin header */ + "origin": undefined, +}; +/** + * @template {keyof Server["defaultVariables"]} K + * @param {K} name + * @returns {Server["defaultVariables"][K]} + */ Server.prototype.get = function(name) { return this.variables[name]; }; -Server.prototype.addRoute = function(route) { - this.routes.push(route); +Server.prototype.addRoute = function(route, title) { + if(!route.path) $tw.utils.log("Warning: Route has no path: " + title); + else this.routes.push(route); }; Server.prototype.addAuthenticator = function(AuthenticatorClass) { @@ -315,9 +369,15 @@ Server.prototype.addAuthenticator = function(AuthenticatorClass) { this.authenticators.push(authenticator); } }; - -Server.prototype.findMatchingRoute = function(request,state) { - for(var t=0; t} */(new Promise((resolve) => { + /** @type {any} */ + var data = ""; + request.on("data", function (chunk) { + data += chunk.toString(); + }); + request.on("end", function () { + if (route.bodyFormat === "www-form-urlencoded") { + data = queryString.parse(data); + } + state.data = data; + resolve(); + }); + })); + await route.handler(request,response,state); + } else if (route.bodyFormat === "buffer") { + await /** @type {Promise} */(new Promise((resolve) => { + /** @type {any} */ + var data = []; + request.on("data", function (chunk) { + data.push(chunk); + }); + request.on("end", function () { + state.data = Buffer.concat(data); + resolve(); + }) + })); + await route.handler(request, response, state); } else { response.writeHead(400,"Invalid bodyFormat " + route.bodyFormat + " in route " + route.method + " " + route.path.source); response.end(); @@ -541,6 +630,8 @@ prefix: optional prefix (falls back to value of "path-prefix" variable) callback: optional callback(err) to be invoked when the listener is up and running */ Server.prototype.listen = function(port,host,prefix,options) { + const { ok } = require("assert"); + var self = this; // Handle defaults for port and host port = port || this.get("port"); @@ -563,6 +654,7 @@ Server.prototype.listen = function(port,host,prefix,options) { $tw.utils.warning(error); } // Create the server + require("https").createServer var server = this.transport.createServer(this.listenOptions || {},function(request,response,options) { if(self.get("debug-level") !== "none") { var start = $tw.utils.timer(); @@ -570,7 +662,7 @@ Server.prototype.listen = function(port,host,prefix,options) { console.log("Response time:",request.method,request.url,$tw.utils.timer() - start); }); } - self.requestHandler(request,response,options); + void self.requestHandler(request,response,options); }); // Display the port number after we've started listening (the port number might have been specified as zero, in which case we will get an assigned port) server.on("listening",function() { @@ -579,8 +671,9 @@ Server.prototype.listen = function(port,host,prefix,options) { server.close(); }); // Log listening details - var address = server.address(), - url = self.protocol + "://" + (address.family === "IPv6" ? "[" + address.address + "]" : address.address) + ":" + address.port + prefix; + var address = server.address(); + ok(typeof address === "object", "Expected server.address() to return an object"); + var url = self.protocol + "://" + (address.family === "IPv6" ? "[" + address.address + "]" : address.address) + ":" + address.port + prefix; $tw.utils.log("Serving on " + url,"brown/orange"); $tw.utils.log("(press ctrl-C to exit)","red"); if(options.callback) { @@ -593,4 +686,22 @@ Server.prototype.listen = function(port,host,prefix,options) { exports.Server = Server; + +class ServerManager { + constructor() { + this.servers = []; + } + + /** + * @param {ServerOptions} options + */ + createServer(options) { + const server = new Server(options); + this.servers.push(server); + return server; + } +} + +exports.ServerManager = ServerManager; + })(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/change-user-password.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/change-user-password.js index 303b8e4e6e6..8bf86acbaa4 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/change-user-password.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/change-user-password.js @@ -21,7 +21,8 @@ exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; -exports.handler = function (request, response, state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function (request, response, state) { var userId = state.data.userId; // Clean up any existing error/success messages $tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/change-password/" + userId + "/error"); @@ -65,7 +66,7 @@ exports.handler = function (request, response, state) { return; } - var userData = state.server.sqlTiddlerDatabase.getUser(userId); + var userData = await state.server.sqlTiddlerDatabase.getUser(userId); if(!userData) { $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ @@ -78,7 +79,7 @@ exports.handler = function (request, response, state) { } var newHash = auth.hashPassword(newPassword); - var result = state.server.sqlTiddlerDatabase.updateUserPassword(userId, newHash); + var result = await state.server.sqlTiddlerDatabase.updateUserPassword(userId, newHash); $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/change-password/" + userId + "/success", diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-acl.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-acl.js index a4c4768e4e5..eb20d57641f 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-acl.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-acl.js @@ -12,7 +12,8 @@ POST /admin/delete-acl /*global $tw: false */ "use strict"; - var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware; + + var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/routes/helpers/acl-middleware.js").middleware; exports.method = "POST"; @@ -23,7 +24,8 @@ POST /admin/delete-acl exports.csrfDisable = true; - exports.handler = function (request, response, state) { + /** @type {ServerRouteHandler} */ + exports.handler = async function (request, response, state) { var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase; var recipe_name = state.data.recipe_name; var bag_name = state.data.bag_name; @@ -32,7 +34,7 @@ POST /admin/delete-acl aclMiddleware(request, response, state, entity_type, "WRITE"); - sqlTiddlerDatabase.deleteACL(acl_id); + await sqlTiddlerDatabase.deleteACL(acl_id); response.writeHead(302, { "Location": "/admin/acl/" + recipe_name + "/" + bag_name }); response.end(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-bag-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-bag-tiddler.js index 722ef2d8c09..a042de676c5 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-bag-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-bag-tiddler.js @@ -12,20 +12,20 @@ DELETE /bags/:bag_name/tiddler/:title /*global $tw: false */ "use strict"; -var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware; +var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/routes/helpers/acl-middleware.js").middleware; exports.method = "DELETE"; exports.path = /^\/bags\/([^\/]+)\/tiddlers\/(.+)$/; - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function(request,response,state) { aclMiddleware(request, response, state, "bag", "WRITE"); // Get the parameters var bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]), title = $tw.utils.decodeURIComponentSafe(state.params[1]); if(bag_name) { if(!response.headersSent) { - var result = $tw.mws.store.deleteTiddler(title,bag_name); + var result = await $tw.mws.store.deleteTiddler(title,bag_name); response.writeHead(204, "OK", { "X-Revision-Number": result.tiddler_id.toString(), Etag: state.makeTiddlerEtag(result), diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-role.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-role.js index 571545b1519..6e69ab67cfd 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-role.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-role.js @@ -19,8 +19,8 @@ POST /admin/delete-role exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; - - exports.handler = function (request, response, state) { + /** @type {ServerRouteHandler} */ + exports.handler = async function (request, response, state) { var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase; var role_id = state.data.role_id; @@ -31,7 +31,7 @@ POST /admin/delete-role } // Check if the role exists - var role = sqlTiddlerDatabase.getRoleById(role_id); + var role = await sqlTiddlerDatabase.getRoleById(role_id); if(!role) { response.writeHead(404, "Not Found"); response.end("Role not found"); @@ -39,13 +39,13 @@ POST /admin/delete-role } // Check if the role is in use - var isRoleInUse = sqlTiddlerDatabase.isRoleInUse(role_id); + var isRoleInUse = await sqlTiddlerDatabase.isRoleInUse(role_id); if(isRoleInUse) { - sqlTiddlerDatabase.deleteUserRolesByRoleId(role_id); + await sqlTiddlerDatabase.deleteUserRolesByRoleId(role_id); } // Delete the role - sqlTiddlerDatabase.deleteRole(role_id); + await sqlTiddlerDatabase.deleteRole(role_id); // Redirect back to the roles management page response.writeHead(302, { "Location": "/admin/roles" }); response.end(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-user-account.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-user-account.js index 5fb0f219f28..bc6e3515cd0 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-user-account.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-user-account.js @@ -19,8 +19,8 @@ exports.path = /^\/delete-user-account\/?$/; exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; - -exports.handler = function (request, response, state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function (request, response, state) { var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase; var userId = state.data.userId; @@ -30,7 +30,7 @@ exports.handler = function (request, response, state) { title: "$:/temp/mws/delete-user/error", text: "You must be an administrator to delete user accounts" })); - response.writeHead(302, { "Location": '/admin/users/'+userId }); + response.writeHead(302, { "Location": "/admin/users/"+userId }); response.end(); return; } @@ -41,49 +41,49 @@ exports.handler = function (request, response, state) { title: "$:/temp/mws/delete-user/error", text: "Cannot delete your own account" })); - response.writeHead(302, { "Location": '/admin/users/'+userId }); + response.writeHead(302, { "Location": "/admin/users/"+userId }); response.end(); return; } // Check if the user exists - var user = sqlTiddlerDatabase.getUser(userId); + var user = await sqlTiddlerDatabase.getUser(userId); if(!user) { $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/delete-user/error", text: "User not found" })); - response.writeHead(302, { "Location": '/admin/users/'+userId }); + response.writeHead(302, { "Location": "/admin/users/"+userId }); response.end(); return; } // Check if this is the last admin account - var adminRole = sqlTiddlerDatabase.getRoleByName("ADMIN"); + var adminRole = await sqlTiddlerDatabase.getRoleByName("ADMIN"); if(!adminRole) { $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/delete-user/error", text: "Admin role not found" })); - response.writeHead(302, { "Location": '/admin/users/'+userId }); + response.writeHead(302, { "Location": "/admin/users/"+userId }); response.end(); return; } - var adminUsers = sqlTiddlerDatabase.listUsersByRoleId(adminRole.role_id); + var adminUsers = await sqlTiddlerDatabase.listUsersByRoleId(adminRole.role_id); if(adminUsers.length <= 1 && adminUsers.some(admin => admin.user_id === parseInt(userId))) { $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/delete-user/error", text: "Cannot delete the last admin account" })); - response.writeHead(302, { "Location": '/admin/users/'+userId }); + response.writeHead(302, { "Location": "/admin/users/"+userId }); response.end(); return; } - sqlTiddlerDatabase.deleteUserRolesByUserId(userId); - sqlTiddlerDatabase.deleteUserSessions(userId); - sqlTiddlerDatabase.deleteUser(userId); + await sqlTiddlerDatabase.deleteUserRolesByUserId(userId); + await sqlTiddlerDatabase.deleteUserSessions(userId); + await sqlTiddlerDatabase.deleteUser(userId); // Redirect back to the users management page response.writeHead(302, { "Location": "/admin/users" }); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-acl.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-acl.js index 1c6e2f1b7e8..6351d465936 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-acl.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-acl.js @@ -14,34 +14,34 @@ GET /admin/acl exports.method = "GET"; exports.path = /^\/admin\/acl\/(.+)$/; - -exports.handler = function (request, response, state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function (request, response, state) { var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase; var params = state.params[0].split("/") var recipeName = params[0]; var bagName = params[params.length - 1]; - var recipes = sqlTiddlerDatabase.listRecipes() - var bags = sqlTiddlerDatabase.listBags() + var recipes = await sqlTiddlerDatabase.listRecipes() + var bags = await sqlTiddlerDatabase.listBags() - var recipe = recipes.find((entry) => entry.recipe_name === recipeName && entry.bag_names.includes(bagName)) - var bag = bags.find((entry) => entry.bag_name === bagName); + var recipe = recipes.find(entry => entry.recipe_name === recipeName && entry.bag_names.includes(bagName)) + var bag = bags.find(entry => entry.bag_name === bagName); - if (!recipe || !bag) { + if(!recipe || !bag) { response.writeHead(500, "Unable to handle request", { "Content-Type": "text/html" }); response.end(); return; } - var recipeAclRecords = sqlTiddlerDatabase.getEntityAclRecords(recipe.recipe_name); - var bagAclRecords = sqlTiddlerDatabase.getEntityAclRecords(bag.bag_name); - var roles = state.server.sqlTiddlerDatabase.listRoles(); - var permissions = state.server.sqlTiddlerDatabase.listPermissions(); + var recipeAclRecords = await sqlTiddlerDatabase.getEntityAclRecords(recipe.recipe_name); + var bagAclRecords = await sqlTiddlerDatabase.getEntityAclRecords(bag.bag_name); + var roles = await state.server.sqlTiddlerDatabase.listRoles(); + var permissions = await state.server.sqlTiddlerDatabase.listPermissions(); // This ensures that the user attempting to view the ACL management page has permission to do so if(!state.authenticatedUser?.isAdmin && !state.firstGuestUser && - (!state.authenticatedUser || (recipeAclRecords.length > 0 && !sqlTiddlerDatabase.hasRecipePermission(state.authenticatedUser.user_id, recipeName, 'WRITE'))) + (!state.authenticatedUser || (recipeAclRecords.length > 0 && !await sqlTiddlerDatabase.hasRecipePermission(state.authenticatedUser.user_id, recipeName, "WRITE"))) ){ response.writeHead(403, "Forbidden"); response.end(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler-blob.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler-blob.js index 28d23212c43..b08cb525fca 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler-blob.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler-blob.js @@ -12,19 +12,19 @@ GET /bags/:bag_name/tiddler/:title/blob /*global $tw: false */ "use strict"; -var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware; +var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/routes/helpers/acl-middleware.js").middleware; exports.method = "GET"; exports.path = /^\/bags\/([^\/]+)\/tiddlers\/([^\/]+)\/blob$/; - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function(request,response,state) { aclMiddleware(request, response, state, "bag", "READ"); // Get the parameters const bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]), title = $tw.utils.decodeURIComponentSafe(state.params[1]); if(bag_name) { - const result = $tw.mws.store.getBagTiddlerStream(title,bag_name); + const result = await $tw.mws.store.getBagTiddlerStream(title,bag_name); if(result && !response.headersSent) { response.writeHead(200, "OK",{ Etag: state.makeTiddlerEtag(result), @@ -34,7 +34,7 @@ exports.handler = function(request,response,state) { return; } } - if (!response.headersSent) { + if(!response.headersSent) { response.writeHead(404); response.end(); } diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler.js index 52b169652b0..1ff56ed634e 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler.js @@ -16,18 +16,18 @@ fallback= // Optional redirect if the tiddler is not found /*global $tw: false */ "use strict"; -var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware; +var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/routes/helpers/acl-middleware.js").middleware; exports.method = "GET"; exports.path = /^\/bags\/([^\/]+)\/tiddlers\/(.+)$/; - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function(request,response,state) { aclMiddleware(request, response, state, "bag", "READ"); // Get the parameters const bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]), title = $tw.utils.decodeURIComponentSafe(state.params[1]), - tiddlerInfo = $tw.mws.store.getBagTiddler(title,bag_name); + tiddlerInfo = await $tw.mws.store.getBagTiddler(title,bag_name); if(tiddlerInfo && tiddlerInfo.tiddler) { // If application/json is requested then this is an API request, and gets the response in JSON if(request.headers.accept && request.headers.accept.indexOf("application/json") !== -1) { @@ -38,7 +38,7 @@ exports.handler = function(request,response,state) { return; } else { // This is not a JSON API request, we should return the raw tiddler content - const result = $tw.mws.store.getBagTiddlerStream(title,bag_name); + const result = await $tw.mws.store.getBagTiddlerStream(title,bag_name); if(result) { if(!response.headersSent){ response.writeHead(200, "OK",{ @@ -59,7 +59,7 @@ exports.handler = function(request,response,state) { } else { // Redirect to fallback URL if tiddler not found if(state.queryParameters.fallback) { - if (!response.headersSent){ + if(!response.headersSent){ response.writeHead(302, "OK",{ "Location": state.queryParameters.fallback }); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag.js index 7d262b83fcf..22163537ec2 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag.js @@ -20,22 +20,22 @@ exports.path = /^\/bags\/([^\/]+)(\/?)$/; exports.useACL = true; exports.entityName = "bag" - -exports.handler = function (request, response, state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function (request, response, state) { // Redirect if there is no trailing slash. We do this so that the relative URL specified in the upload form works correctly - if (state.params[1] !== "/") { + if(state.params[1] !== "/") { state.redirect(301, state.urlInfo.path + "/"); return; } // Get the parameters var bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]), - bagTiddlers = bag_name && $tw.mws.store.getBagTiddlers(bag_name); - if (bag_name && bagTiddlers) { + bagTiddlers = bag_name && await $tw.mws.store.getBagTiddlers(bag_name); + if(bag_name && bagTiddlers) { // If application/json is requested then this is an API request, and gets the response in JSON - if (request.headers.accept && request.headers.accept.indexOf("application/json") !== -1) { + if(request.headers.accept && request.headers.accept.indexOf("application/json") !== -1) { state.sendResponse(200, { "Content-Type": "application/json" }, JSON.stringify(bagTiddlers), "utf8"); } else { - if (!response.headersSent) { + if(!response.headersSent) { // This is not a JSON API request, we should return the raw tiddler content response.writeHead(200, "OK", { "Content-Type": "text/html" @@ -53,7 +53,7 @@ exports.handler = function (request, response, state) { } } } else { - if (!response.headersSent) { + if(!response.headersSent) { response.writeHead(404); response.end(); } diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-index.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-index.js index 1b5dc96fe6f..c306373a39f 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-index.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-index.js @@ -15,28 +15,28 @@ GET /?show_system=true exports.method = "GET"; exports.path = /^\/$/; - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function(request,response,state) { // Get the bag and recipe information - var bagList = $tw.mws.store.listBags(), - recipeList = $tw.mws.store.listRecipes(), + var bagList = await $tw.mws.store.listBags(), + recipeList = await $tw.mws.store.listRecipes(), sqlTiddlerDatabase = state.server.sqlTiddlerDatabase; // If application/json is requested then this is an API request, and gets the response in JSON if(request.headers.accept && request.headers.accept.indexOf("application/json") !== -1) { - state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify(recipes),"utf8"); + state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify(recipeList),"utf8"); } else { // This is not a JSON API request, we should return the raw tiddler content response.writeHead(200, "OK",{ "Content-Type": "text/html" }); // filter bags and recipies by user's read access from ACL - var allowedRecipes = recipeList.filter(recipe => recipe.recipe_name.startsWith("$:/") || state.authenticatedUser?.isAdmin || sqlTiddlerDatabase.hasRecipePermission(state.authenticatedUser?.user_id, recipe.recipe_name, 'READ') || state.allowAnon && state.allowAnonReads); - var allowedBags = bagList.filter(bag => bag.bag_name.startsWith("$:/") || state.authenticatedUser?.isAdmin || sqlTiddlerDatabase.hasBagPermission(state.authenticatedUser?.user_id, bag.bag_name, 'READ') || state.allowAnon && state.allowAnonReads); + var allowedRecipes = recipeList.filter(recipe => recipe.recipe_name.startsWith("$:/") || state.authenticatedUser?.isAdmin || sqlTiddlerDatabase.hasRecipePermission(state.authenticatedUser?.user_id, recipe.recipe_name, "READ") || state.allowAnon && state.allowAnonReads); + var allowedBags = bagList.filter(bag => bag.bag_name.startsWith("$:/") || state.authenticatedUser?.isAdmin || sqlTiddlerDatabase.hasBagPermission(state.authenticatedUser?.user_id, bag.bag_name, "READ") || state.allowAnon && state.allowAnonReads); allowedRecipes = allowedRecipes.map(recipe => { return { ...recipe, - has_acl_access: state.authenticatedUser?.isAdmin || recipe.owner_id === state.authenticatedUser?.user_id || sqlTiddlerDatabase.hasRecipePermission(state.authenticatedUser?.user_id, recipe.recipe_name, 'WRITE') + has_acl_access: state.authenticatedUser?.isAdmin || recipe.owner_id === state.authenticatedUser?.user_id || sqlTiddlerDatabase.hasRecipePermission(state.authenticatedUser?.user_id, recipe.recipe_name, "WRITE") } }); // Render the html diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-login.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-login.js index dd0421a66ea..8b524067395 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-login.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-login.js @@ -15,10 +15,10 @@ GET /login exports.method = "GET"; exports.path = /^\/login$/; - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function(request,response,state) { // Check if the user already has a valid session - var authenticatedUser = state.server.authenticateUser(request, response); + var authenticatedUser = await state.server.authenticateUser(request, response); if(authenticatedUser) { // User is already logged in, redirect to home page response.writeHead(302, { "Location": "/" }); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-events.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-events.js index bfdc40c187d..3716239b3b6 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-events.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-events.js @@ -21,8 +21,8 @@ const SSE_HEARTBEAT_INTERVAL_MS = 10 * 1000; exports.method = "GET"; exports.path = /^\/recipes\/([^\/]+)\/events$/; - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function(request,response,state) { // Get the parameters const recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]); let last_known_tiddler_id = 0; @@ -40,12 +40,12 @@ exports.handler = function(request,response,state) { }); // Setup the heartbeat timer var heartbeatTimer = setInterval(function() { - response.write(':keep-alive\n\n'); + response.write(":keep-alive\n\n"); },SSE_HEARTBEAT_INTERVAL_MS); // Method to get changed tiddler events and send to the client - function sendUpdates() { + async function sendUpdates() { // Get the tiddlers in the recipe since the last known tiddler_id - var recipeTiddlers = $tw.mws.store.getRecipeTiddlers(recipe_name,{ + var recipeTiddlers = await $tw.mws.store.getRecipeTiddlers(recipe_name,{ include_deleted: true, last_known_tiddler_id: last_known_tiddler_id }); @@ -56,22 +56,22 @@ exports.handler = function(request,response,state) { if(tiddlerInfo.tiddler_id > last_known_tiddler_id) { last_known_tiddler_id = tiddlerInfo.tiddler_id; } - response.write(`event: change\n`) + response.write("event: change\n") let data = tiddlerInfo; if(!tiddlerInfo.is_deleted) { - const tiddler = $tw.mws.store.getRecipeTiddler(tiddlerInfo.title,recipe_name); + const tiddler = await $tw.mws.store.getRecipeTiddler(tiddlerInfo.title,recipe_name); if(tiddler) { data = $tw.utils.extend({},data,{tiddler: tiddler.tiddler}) } } response.write(`data: ${JSON.stringify(data)}\n`); response.write(`id: ${tiddlerInfo.tiddler_id}\n`) - response.write(`\n`); + response.write("\n"); } } } // Send current and future changes - sendUpdates(); + await sendUpdates(); $tw.mws.store.addEventListener("change",sendUpdates); // Clean up when the connection closes response.on("close",function () { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddler.js index a50657ce58a..8453146e743 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddler.js @@ -23,12 +23,12 @@ exports.path = /^\/recipes\/([^\/]+)\/tiddlers\/(.+)$/; // exports.useACL = true; exports.entityName = "recipe" - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function(request,response,state) { // Get the parameters var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]), title = $tw.utils.decodeURIComponentSafe(state.params[1]), - tiddlerInfo = $tw.mws.store.getRecipeTiddler(title,recipe_name); + tiddlerInfo = await $tw.mws.store.getRecipeTiddler(title,recipe_name); if(tiddlerInfo && tiddlerInfo.tiddler) { // If application/json is requested then this is an API request, and gets the response in JSON if(request.headers.accept && request.headers.accept.indexOf("application/json") !== -1) { @@ -41,7 +41,7 @@ exports.handler = function(request,response,state) { return; } else { // This is not a JSON API request, we should return the raw tiddler content - const result = $tw.mws.store.getBagTiddlerStream(title,tiddlerInfo.bag_name); + const result = await $tw.mws.store.getBagTiddlerStream(title,tiddlerInfo.bag_name); if(result) { if(!response.headersSent){ response.writeHead(200, "OK",{ diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddlers-json.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddlers-json.js index e16e3d10a55..27f9e22cc8d 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddlers-json.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddlers-json.js @@ -15,14 +15,14 @@ GET /recipes/:recipe_name/tiddlers.json?last_known_tiddler_id=:last_known_tiddle exports.method = "GET"; exports.path = /^\/recipes\/([^\/]+)\/tiddlers.json$/; - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function(request,response,state) { if(!response.headersSent) { // Get the parameters var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]); if(recipe_name) { // Get the tiddlers in the recipe, optionally since the specified last known tiddler_id - var recipeTiddlers = $tw.mws.store.getRecipeTiddlers(recipe_name,{ + var recipeTiddlers = await $tw.mws.store.getRecipeTiddlers(recipe_name,{ include_deleted: state.queryParameters.include_deleted === "true", last_known_tiddler_id: state.queryParameters.last_known_tiddler_id }); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-system.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-system.js index a5346c1a658..a4124e4dde2 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-system.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-system.js @@ -23,7 +23,9 @@ exports.method = "GET"; exports.path = /^\/\.system\/(.+)$/; const SYSTEM_FILE_TITLE_PREFIX = "$:/plugins/tiddlywiki/multiwikiserver/system-files/"; - +/** @type {ServerRouteHandler} */ +// @ts-ignore +// eslint-disable-next-line @typescript-eslint/promise-function-async exports.handler = function(request,response,state) { // Get the parameters const filename = $tw.utils.decodeURIComponentSafe(state.params[0]), diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-users.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-users.js index 142258aa0d5..9ceab412244 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-users.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-users.js @@ -15,21 +15,21 @@ GET /admin/users exports.method = "GET"; exports.path = /^\/admin\/users$/; - -exports.handler = function(request,response,state) { - var userList = state.server.sqlTiddlerDatabase.listUsers(); - if (request.url.includes("*")) { +/** @type {ServerRouteHandler} */ +exports.handler = async function(request,response,state) { + var userList = await state.server.sqlTiddlerDatabase.listUsers(); + if(request.url.includes("*")) { $tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/post-user/error"); $tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/post-user/success"); } // Ensure userList is an array - if (!Array.isArray(userList)) { + if(!Array.isArray(userList)) { userList = []; console.error("userList is not an array"); } - if(!state.authenticatedUser.isAdmin && !state.firstGuestUser) { + if(!state.authenticatedUser?.isAdmin && !state.firstGuestUser) { response.writeHead(403, "Forbidden", { "Content-Type": "text/plain" }); response.end("Forbidden"); return; @@ -37,11 +37,11 @@ exports.handler = function(request,response,state) { // Convert dates to strings and ensure all necessary fields are present userList = userList.map(user => ({ - user_id: user.user_id || '', - username: user.username || '', - email: user.email || '', - created_at: user.created_at ? new Date(user.created_at).toISOString() : '', - last_login: user.last_login ? new Date(user.last_login).toISOString() : '' + user_id: user.user_id || "", + username: user.username || "", + email: user.email || "", + created_at: user.created_at ? new Date(user.created_at).toISOString() : "", + last_login: user.last_login ? new Date(user.last_login).toISOString() : "" })); response.writeHead(200, "OK", { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-wiki.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-wiki.js index 1765f5e208f..81128e416a5 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-wiki.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-wiki.js @@ -19,11 +19,11 @@ exports.path = /^\/wiki\/([^\/]+)$/; exports.useACL = true; exports.entityName = "recipe" - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function(request,response,state) { // Get the recipe name from the parameters var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]), - recipeTiddlers = recipe_name && $tw.mws.store.getRecipeTiddlers(recipe_name); + recipeTiddlers = recipe_name && await $tw.mws.store.getRecipeTiddlers(recipe_name); // Check request is valid if(recipe_name && recipeTiddlers) { // Start the response @@ -47,7 +47,7 @@ exports.handler = function(request,response,state) { } }); // Splice in our tiddlers - var marker = `<` + `script class="tiddlywiki-tiddler-store" type="application/json">[`, + var marker = "<" + "script class=\"tiddlywiki-tiddler-store\" type=\"application/json\">[", markerPos = template.indexOf(marker); if(markerPos === -1) { throw new Error("Cannot find tiddler store in template"); @@ -58,15 +58,21 @@ exports.handler = function(request,response,state) { } response.write(template.substring(0,markerPos + marker.length)); const bagInfo = {}, - revisionInfo = {}; - $tw.utils.each(recipeTiddlers,function(recipeTiddlerInfo) { - var result = $tw.mws.store.getRecipeTiddler(recipeTiddlerInfo.title,recipe_name); + revisionInfo = {}, + recipeTiddlerInfos = []; + + $tw.utils.each(recipeTiddlers, function(recipeTiddlerInfo) { + recipeTiddlerInfos.push(recipeTiddlerInfo); + }); + for(const recipeTiddlerInfo of recipeTiddlerInfos){ + var result = await $tw.mws.store.getRecipeTiddler(recipeTiddlerInfo.title,recipe_name); if(result) { bagInfo[result.tiddler.title] = result.bag_name; revisionInfo[result.tiddler.title] = result.tiddler_id.toString(); writeTiddler(result.tiddler); } - }); + } + writeTiddler({ title: "$:/state/multiwikiclient/tiddlers/bag", text: JSON.stringify(bagInfo), @@ -83,7 +89,7 @@ exports.handler = function(request,response,state) { }); writeTiddler({ title: "$:/state/multiwikiclient/recipe/last_tiddler_id", - text: ($tw.mws.store.getRecipeLastTiddlerId(recipe_name) || 0).toString() + text: (await $tw.mws.store.getRecipeLastTiddlerId(recipe_name) || 0).toString() }); response.write(template.substring(markerPos + marker.length)) // Finish response diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-roles.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-roles.js index e6400dbb92c..45ca5cb7b5f 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-roles.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-roles.js @@ -15,13 +15,13 @@ GET /admin/manage-roles exports.method = "GET"; exports.path = /^\/admin\/roles\/?$/; - -exports.handler = function(request, response, state) { - if (request.url.includes("*")) { +/** @type {ServerRouteHandler} */ +exports.handler = async function(request, response, state) { + if(request.url.includes("*")) { $tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/post-role/error"); $tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/post-role/success"); } - var roles = state.server.sqlTiddlerDatabase.listRoles(); + var roles = await state.server.sqlTiddlerDatabase.listRoles(); var editRoleId = request.url.includes("?") ? request.url.split("?")[1]?.split("=")[1] : null; var editRole = editRoleId ? roles.find(role => role.role_id === $tw.utils.parseInt(editRoleId, 10)) : null; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-user.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-user.js index cada04b832f..076448585ae 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-user.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-user.js @@ -15,10 +15,10 @@ GET /admin/users/:user_id exports.method = "GET"; exports.path = /^\/admin\/users\/([^\/]+)\/?$/; - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function(request,response,state) { var user_id = $tw.utils.decodeURIComponentSafe(state.params[0]); - var userData = state.server.sqlTiddlerDatabase.getUser(user_id); + var userData = await state.server.sqlTiddlerDatabase.getUser(user_id); // Clean up any existing error/success messages if the user_id is different from the "$:/temp/mws/user-info/preview-user-id" var lastPreviewedUser = $tw.wiki.getTiddlerText("$:/temp/mws/user-info/" + user_id + "/preview-user-id"); @@ -46,7 +46,8 @@ exports.handler = function(request,response,state) { } // Check if the user is trying to access their own profile or is an admin - var hasPermission = ($tw.utils.parseInt(user_id) === state.authenticatedUser.user_id) || state.authenticatedUser.isAdmin; + var hasPermission = (state.authenticatedUser && $tw.utils.parseInt(user_id) === state.authenticatedUser.user_id) + || state.authenticatedUser?.isAdmin; if(!hasPermission) { response.writeHead(403, "Forbidden", { "Content-Type": "text/plain" }); response.end("Forbidden"); @@ -63,8 +64,8 @@ exports.handler = function(request,response,state) { }; // Get all roles which the user has been assigned - var userRole = state.server.sqlTiddlerDatabase.getUserRoles(user_id); - var allRoles = state.server.sqlTiddlerDatabase.listRoles(); + var userRole = await state.server.sqlTiddlerDatabase.getUserRoles(user_id); + var allRoles = await state.server.sqlTiddlerDatabase.listRoles(); // sort allRoles by placing the user's role at the top of the list allRoles.sort(function(a, b){ return (a.role_id === userRole?.role_id ? -1 : 1) }); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-acl.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-acl.js index 63a9f414f1b..952d44a3c7f 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-acl.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-acl.js @@ -19,8 +19,8 @@ exports.path = /^\/admin\/post-acl\/?$/; exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; - -exports.handler = function (request, response, state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function (request, response, state) { var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase; var entity_type = state.data.entity_type; var recipe_name = state.data.recipe_name; @@ -30,9 +30,9 @@ exports.handler = function (request, response, state) { var isRecipe = entity_type === "recipe" try { - var entityAclRecords = sqlTiddlerDatabase.getACLByName(entity_type, isRecipe ? recipe_name : bag_name, true); + var entityAclRecords = await sqlTiddlerDatabase.getACLByName(entity_type, isRecipe ? recipe_name : bag_name, true); - var aclExists = entityAclRecords.some((record) => ( + var aclExists = entityAclRecords.some(record => ( record.role_id == role_id && record.permission_id == permission_id )) @@ -43,14 +43,14 @@ exports.handler = function (request, response, state) { // return // } - if (aclExists) { + if(aclExists) { // do nothing, return the user back to the form response.writeHead(302, { "Location": "/admin/acl/" + recipe_name + "/" + bag_name }); response.end(); return } - sqlTiddlerDatabase.createACL( + await sqlTiddlerDatabase.createACL( isRecipe ? recipe_name : bag_name, entity_type, role_id, @@ -59,7 +59,7 @@ exports.handler = function (request, response, state) { response.writeHead(302, { "Location": "/admin/acl/" + recipe_name + "/" + bag_name }); response.end(); - } catch (error) { + } catch(error) { response.writeHead(302, { "Location": "/admin/acl/" + recipe_name + "/" + bag_name }); response.end(); } diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-anon-config.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-anon-config.js index e1e841516c4..918bd320814 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-anon-config.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-anon-config.js @@ -19,8 +19,9 @@ exports.path = /^\/admin\/post-anon-config\/?$/; exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; - -exports.handler = function(request, response, state) { +/** @type {ServerRouteHandler} */ +// eslint-disable-next-line require-await +exports.handler = async function(request, response, state) { // Check if user is authenticated and is admin if(!state.authenticatedUser || !state.authenticatedUser.isAdmin) { response.writeHead(401, "Unauthorized", { "Content-Type": "text/plain" }); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-anon.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-anon.js index 911b6ef971c..003db6851c1 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-anon.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-anon.js @@ -19,8 +19,9 @@ exports.path = /^\/admin\/anon\/?$/; exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; - -exports.handler = function(request, response, state) { +/** @type {ServerRouteHandler} */ +// eslint-disable-next-line require-await +exports.handler = async function(request, response, state) { // Check if user is authenticated and is admin if(!state.authenticatedUser || !state.authenticatedUser.isAdmin) { response.writeHead(401, "Unauthorized", { "Content-Type": "text/plain" }); @@ -30,8 +31,7 @@ exports.handler = function(request, response, state) { // Update the configuration tiddlers - var wiki = $tw.wiki; - wiki.addTiddler({ + $tw.wiki.addTiddler({ title: "$:/config/MultiWikiServer/ShowAnonymousAccessModal", text: "yes" }); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag-tiddlers.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag-tiddlers.js index 0f520b1ba59..e10065a6315 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag-tiddlers.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag-tiddlers.js @@ -23,8 +23,9 @@ exports.csrfDisable = true; exports.useACL = true; exports.entityName = "bag" - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler} */ +// eslint-disable-next-line require-await +exports.handler = async function(request,response,state) { const path = require("path"), fs = require("fs"), processIncomingStream = require("$:/plugins/tiddlywiki/multiwikiserver/routes/helpers/multipart-forms.js").processIncomingStream; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag.js index bd59b06427f..aacd9a835e1 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag.js @@ -28,10 +28,10 @@ exports.csrfDisable = true; exports.useACL = true; exports.entityName = "bag" - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function(request,response,state) { if(state.data.bag_name) { - const result = $tw.mws.store.createBag(state.data.bag_name,state.data.description); + const result = await $tw.mws.store.createBag(state.data.bag_name,state.data.description); if(!result) { state.sendResponse(302,{ "Content-Type": "text/plain", diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-login.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-login.js index b2bc0ff4df4..bf5ddb79476 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-login.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-login.js @@ -16,7 +16,7 @@ password /*jslint node: true, browser: true */ /*global $tw: false */ "use strict"; -var authenticator = require('$:/plugins/tiddlywiki/multiwikiserver/auth/authentication.js').Authenticator; +var authenticator = require("$:/plugins/tiddlywiki/multiwikiserver/auth/authentication.js").Authenticator; exports.method = "POST"; @@ -25,25 +25,25 @@ exports.path = /^\/login$/; exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function(request,response,state) { var auth = authenticator(state.server.sqlTiddlerDatabase); var username = state.data.username; var password = state.data.password; - var user = state.server.sqlTiddlerDatabase.getUserByUsername(username); + var user = await state.server.sqlTiddlerDatabase.getUserByUsername(username); var isPasswordValid = auth.verifyPassword(password, user ? user.password : null) if(user && isPasswordValid) { var sessionId = auth.createSession(user.user_id); var returnUrl = state.server.parseCookieString(request.headers.cookie).returnUrl - response.setHeader('Set-Cookie', `session=${sessionId}; HttpOnly; Path=/`); + response.setHeader("Set-Cookie", `session=${sessionId}; HttpOnly; Path=/`); if(request.headers.accept && request.headers.accept.indexOf("application/json") !== -1) { state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify({ "sessionId": sessionId })); } else { response.writeHead(302, { - 'Location': returnUrl || '/' + "Location": returnUrl || "/" }); } } else { @@ -57,7 +57,7 @@ exports.handler = function(request,response,state) { })); } else { response.writeHead(302, { - 'Location': '/login' + "Location": "/login" }); } } diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-logout.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-logout.js index 36d901b4467..2372f898154 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-logout.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-logout.js @@ -17,11 +17,11 @@ exports.method = "POST"; exports.path = /^\/logout$/; exports.csrfDisable = true; - -exports.handler = function(request,response,state) { - // if(state.authenticatedUser) { - state.server.sqlTiddlerDatabase.deleteSession(state.authenticatedUser.sessionId); - // } +/** @type {ServerRouteHandler} */ +exports.handler = async function(request,response,state) { + if(state.authenticatedUser) { + await state.server.sqlTiddlerDatabase.deleteSession(state.authenticatedUser.sessionId); + } var cookies = request.headers.cookie ? request.headers.cookie.split(";") : []; for(var i = 0; i < cookies.length; i++) { var cookie = cookies[i].trim().split("=")[0]; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-recipe.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-recipe.js index aa38986a002..a19ec007d23 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-recipe.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-recipe.js @@ -29,15 +29,15 @@ exports.csrfDisable = true; exports.useACL = true; exports.entityName = "recipe" - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function(request,response,state) { var server = state.server, sqlTiddlerDatabase = server.sqlTiddlerDatabase if(state.data.recipe_name && state.data.bag_names) { - const result = $tw.mws.store.createRecipe(state.data.recipe_name,$tw.utils.parseStringArray(state.data.bag_names),state.data.description); + const result = await $tw.mws.store.createRecipe(state.data.recipe_name,$tw.utils.parseStringArray(state.data.bag_names),state.data.description); if(!result) { if(state.authenticatedUser) { - sqlTiddlerDatabase.assignRecipeToUser(state.data.recipe_name,state.authenticatedUser.user_id); + await sqlTiddlerDatabase.assignRecipeToUser(state.data.recipe_name,state.authenticatedUser.user_id); } state.sendResponse(302,{ "Content-Type": "text/plain", diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-role.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-role.js index 9692c7d689d..f65ffe0dfdf 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-role.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-role.js @@ -19,8 +19,8 @@ exports.path = /^\/admin\/post-role\/?$/; exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; - -exports.handler = function (request, response, state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function (request, response, state) { var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase; var role_name = state.data.role_name; var role_description = state.data.role_description; @@ -47,7 +47,7 @@ exports.handler = function (request, response, state) { try { // Check if role already exists - var existingRole = sqlTiddlerDatabase.getRole(role_name); + var existingRole = await sqlTiddlerDatabase.getRole(role_name); if(existingRole) { $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/post-role/error", @@ -58,7 +58,7 @@ exports.handler = function (request, response, state) { return; } - sqlTiddlerDatabase.createRole(role_name, role_description); + await sqlTiddlerDatabase.createRole(role_name, role_description); $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/post-role/success", diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-user.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-user.js index ff3acbfc916..d0a32c647f6 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-user.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-user.js @@ -29,9 +29,9 @@ function deleteTempTiddlers() { $tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/post-user/success"); }, 1000); } +/** @type {ServerRouteHandler} */ +exports.handler = async function(request, response, state) { -exports.handler = function(request, response, state) { - var current_user_id = state.authenticatedUser.user_id; var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase; var username = state.data.username; var email = state.data.email; @@ -84,8 +84,8 @@ exports.handler = function(request, response, state) { try { // Check if username or email already exists - var existingUser = sqlTiddlerDatabase.getUserByUsername(username); - var existingUserByEmail = sqlTiddlerDatabase.getUserByEmail(email); + var existingUser = await sqlTiddlerDatabase.getUserByUsername(username); + var existingUserByEmail = await sqlTiddlerDatabase.getUserByEmail(email); if(existingUser || existingUserByEmail) { $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ @@ -109,7 +109,7 @@ exports.handler = function(request, response, state) { return; } - var hasUsers = sqlTiddlerDatabase.listUsers().length > 0; + var hasUsers = (await sqlTiddlerDatabase.listUsers()).length > 0; var hashedPassword = crypto.createHash("sha256").update(password).digest("hex"); // Create new user @@ -118,7 +118,7 @@ exports.handler = function(request, response, state) { if(!hasUsers) { try { // If this is the first guest user, assign admin privileges - sqlTiddlerDatabase.setUserAdmin(userId, true); + await sqlTiddlerDatabase.setUserAdmin(userId); // Create a session for the new admin user var auth = require("$:/plugins/tiddlywiki/multiwikiserver/auth/authentication.js").Authenticator; @@ -160,12 +160,12 @@ exports.handler = function(request, response, state) { email: email, })); // assign role to user - var roles = sqlTiddlerDatabase.listRoles(); + var roles = await sqlTiddlerDatabase.listRoles(); var role = roles.find(function(role) { return role.role_name.toUpperCase() !== "ADMIN"; }); if(role) { - sqlTiddlerDatabase.addRoleToUser(userId, role.role_id); + await sqlTiddlerDatabase.addRoleToUser(userId, role.role_id); } response.writeHead(302, {"Location": "/admin/users/"+userId}); response.end(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-bag.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-bag.js index d174ee8cea2..ed97705346b 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-bag.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-bag.js @@ -19,13 +19,13 @@ exports.path = /^\/bags\/(.+)$/; exports.useACL = true; exports.entityName = "bag" - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function(request,response,state) { // Get the parameters var bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]), data = $tw.utils.parseJSONSafe(state.data); if(bag_name && data) { - var result = $tw.mws.store.createBag(bag_name,data.description); + var result = await $tw.mws.store.createBag(bag_name,data.description); if(!result) { state.sendResponse(204,{ "Content-Type": "text/plain" diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe-tiddler.js index 25279cdd0d8..4eab83e308c 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe-tiddler.js @@ -19,14 +19,14 @@ exports.path = /^\/recipes\/([^\/]+)\/tiddlers\/(.+)$/; exports.useACL = true; exports.entityName = "recipe" - -exports.handler = function (request, response, state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function (request, response, state) { // Get the parameters var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]), title = $tw.utils.decodeURIComponentSafe(state.params[1]), fields = $tw.utils.parseJSONSafe(state.data); if(recipe_name && title === fields.title) { - var result = $tw.mws.store.saveRecipeTiddler(fields, recipe_name); + var result = await $tw.mws.store.saveRecipeTiddler(fields, recipe_name); if(!response.headersSent) { if(result) { response.writeHead(204, "OK", { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe.js index 002c5e4dbd9..86fd28a9627 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe.js @@ -19,13 +19,13 @@ exports.path = /^\/recipes\/(.+)$/; exports.useACL = true; exports.entityName = "recipe" - -exports.handler = function (request, response, state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function (request, response, state) { // Get the parameters var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]), data = $tw.utils.parseJSONSafe(state.data); if(recipe_name && data) { - var result = $tw.mws.store.createRecipe(recipe_name, data.bag_names, data.description); + var result = await $tw.mws.store.createRecipe(recipe_name, data.bag_names, data.description); if(!result) { state.sendResponse(204, { "Content-Type": "text/plain" diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-role.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-role.js index 081ba9b7374..401211051f0 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-role.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-role.js @@ -19,21 +19,21 @@ exports.path = /^\/admin\/roles\/([^\/]+)\/?$/; exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; - -exports.handler = function(request, response, state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function(request, response, state) { var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase; var role_id = state.params[0]; var role_name = state.data.role_name; var role_description = state.data.role_description; - if(!state.authenticatedUser.isAdmin) { + if(!state.authenticatedUser?.isAdmin) { response.writeHead(403, "Forbidden"); response.end(); return; } // get the role - var role = sqlTiddlerDatabase.getRoleById(role_id); + var role = await sqlTiddlerDatabase.getRoleById(role_id); if(!role) { response.writeHead(404, "Role not found"); @@ -48,7 +48,7 @@ exports.handler = function(request, response, state) { } try { - sqlTiddlerDatabase.updateRole( + await sqlTiddlerDatabase.updateRole( role_id, role_name, role_description diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-user-profile.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-user-profile.js index 3cbc0669000..9049dc517f1 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-user-profile.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-user-profile.js @@ -19,8 +19,8 @@ exports.path = /^\/update-user-profile\/?$/; exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; - -exports.handler = function (request,response,state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function (request,response,state) { if(!state.authenticatedUser) { $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/login/error", @@ -50,11 +50,11 @@ exports.handler = function (request,response,state) { } if(!state.authenticatedUser.isAdmin) { - var userRole = state.server.sqlTiddlerDatabase.getUserRoles(userId); + var userRole = await state.server.sqlTiddlerDatabase.getUserRoles(userId); roleId = userRole.role_id; } - var result = state.server.sqlTiddlerDatabase.updateUser(userId, username, email, roleId); + var result = await state.server.sqlTiddlerDatabase.updateUser(userId, username, email, roleId); if(result.success) { $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js index 7210ed82c68..38188793950 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js @@ -1,5 +1,5 @@ /*\ -title: $:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js +title: $:/plugins/tiddlywiki/multiwikiserver/routes/helpers/acl-middleware.js type: application/javascript module-type: library @@ -19,17 +19,17 @@ ACL Middleware factory function function redirectToLogin(response, returnUrl) { if(!response.headersSent) { var validReturnUrlRegex = /^\/(?!.*\.(ico|png|jpg|jpeg|gif|svg|css|js|woff|woff2|ttf|eot|json)$).*$/; - var sanitizedReturnUrl = '/'; // Default to home page + var sanitizedReturnUrl = "/"; // Default to home page if(validReturnUrlRegex.test(returnUrl)) { sanitizedReturnUrl = returnUrl; - response.setHeader('Set-Cookie', `returnUrl=${encodeURIComponent(sanitizedReturnUrl)}; HttpOnly; Secure; SameSite=Strict; Path=/`); - } else{ + response.setHeader("Set-Cookie", `returnUrl=${encodeURIComponent(sanitizedReturnUrl)}; HttpOnly; Secure; SameSite=Strict; Path=/`); + } else { console.log(`Invalid return URL detected: ${returnUrl}. Redirecting to home page.`); } - const loginUrl = '/login'; + const loginUrl = "/login"; response.writeHead(302, { - 'Location': loginUrl + "Location": loginUrl }); response.end(); } @@ -60,8 +60,8 @@ exports.middleware = function (request, response, state, entityType, permissionN if(entity?.owner_id) { if(state.authenticatedUser?.user_id && (state.authenticatedUser?.user_id !== entity.owner_id) || !state.authenticatedUser?.user_id && !hasAnonymousAccess) { const hasPermission = state.authenticatedUser?.user_id ? - entityType === 'recipe' ? sqlTiddlerDatabase.hasRecipePermission(state.authenticatedUser?.user_id, decodedEntityName, isGetRequest ? 'READ' : 'WRITE') - : sqlTiddlerDatabase.hasBagPermission(state.authenticatedUser?.user_id, decodedEntityName, isGetRequest ? 'READ' : 'WRITE') + entityType === "recipe" ? sqlTiddlerDatabase.hasRecipePermission(state.authenticatedUser?.user_id, decodedEntityName, isGetRequest ? "READ" : "WRITE") + : sqlTiddlerDatabase.hasBagPermission(state.authenticatedUser?.user_id, decodedEntityName, isGetRequest ? "READ" : "WRITE") : false if(!response.headersSent && !hasPermission) { response.writeHead(403, "Forbidden"); @@ -84,7 +84,7 @@ exports.middleware = function (request, response, state, entityType, permissionN if(aclRecord && aclRecord?.permission_id === permission?.permission_id) { // If not authenticated and anonymous access is not allowed, request authentication if(!state.authenticatedUsername && !state.allowAnon) { - if(state.urlInfo.pathname !== '/login') { + if(state.urlInfo.pathname !== "/login") { redirectToLogin(response, request.url); return; } diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/multipart-forms.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/multipart-forms.js index 6a46699fabb..7b066c3e1e8 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/multipart-forms.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/multipart-forms.js @@ -19,6 +19,15 @@ response - provided by server.js bag_name - name of bag to write to callback - invoked as callback(err,results). Results is an array of titles of imported tiddlers */ +/** + * + * @param {Object} options + * @param {SqlTiddlerStore} options.store + * @param {ServerState} options.state + * @param {ServerResponse} options.response + * @param {string} options.bag_name + * @param {function} options.callback + */ exports.processIncomingStream = function(options) { const self = this; const path = require("path"), @@ -72,7 +81,7 @@ exports.processIncomingStream = function(options) { } else { const partFile = parts.find(part => part.name === "file-to-upload" && !!part.filename); if(!partFile) { - return state.sendResponse(400, {"Content-Type": "text/plain"},"Missing file to upload"); + return options.state.sendResponse(400, {"Content-Type": "text/plain"},"Missing file to upload"); } const type = partFile.headers["content-type"]; const tiddlerFields = { @@ -89,9 +98,12 @@ exports.processIncomingStream = function(options) { filepath: partFile.inboxFilename, type: type, hash: partFile.hash + }).then(() => { + $tw.utils.deleteDirectory(inboxPath); + options.callback(null,[tiddlerFields.title]); + }, err => { + options.callback(err); }); - $tw.utils.deleteDirectory(inboxPath); - options.callback(null,[tiddlerFields.title]); } } }); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/startup.js b/plugins/tiddlywiki/multiwikiserver/modules/startup.js index 42686e889be..01dfef6242e 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/startup.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/startup.js @@ -16,43 +16,29 @@ Multi wiki server initialisation exports.name = "multiwikiserver"; exports.platforms = ["node"]; exports.before = ["story"]; -exports.synchronous = true; +exports.synchronous = false; -exports.startup = function() { - const store = setupStore(); - $tw.mws = { - store: store, - serverManager: new ServerManager({ - store: store - }) - }; -} - -function setupStore() { +exports.startup = async function() { const path = require("path"); // Create and initialise the attachment store and the tiddler store - const AttachmentStore = require("$:/plugins/tiddlywiki/multiwikiserver/store/attachments.js").AttachmentStore, - attachmentStore = new AttachmentStore({ - storePath: path.resolve($tw.boot.wikiPath,"store/") - }), - SqlTiddlerStore = require("$:/plugins/tiddlywiki/multiwikiserver/store/sql-tiddler-store.js").SqlTiddlerStore, - store = new SqlTiddlerStore({ - databasePath: path.resolve($tw.boot.wikiPath,"store/database.sqlite"), - engine: $tw.wiki.getTiddlerText("$:/config/MultiWikiServer/Engine","better"), // better || wasm - attachmentStore: attachmentStore - }); - return store; -} - -function ServerManager(store) { - this.servers = []; -} - -ServerManager.prototype.createServer = function(options) { - const MWSServer = require("$:/plugins/tiddlywiki/multiwikiserver/mws-server.js").Server, - server = new MWSServer(options); - this.servers.push(server); - return server; + const { AttachmentStore } = require("$:/plugins/tiddlywiki/multiwikiserver/store/attachments.js") + const attachmentStore = new AttachmentStore({ + storePath: path.resolve($tw.boot.wikiPath, "store/") + }); + + const { SqlTiddlerStore } = require("$:/plugins/tiddlywiki/multiwikiserver/store/sql-tiddler-store.js"); + const store = new SqlTiddlerStore({ + databasePath: path.resolve($tw.boot.wikiPath, "store/database.sqlite"), + engine: $tw.wiki.getTiddlerText("$:/config/MultiWikiServer/Engine", "better"), // better || wasm + attachmentStore: attachmentStore + }); + await store.initCheck(); + + const { ServerManager } = require("$:/plugins/tiddlywiki/multiwikiserver/mws-server.js"); + const serverManager = new ServerManager(); + + $tw.mws = { store, serverManager }; + } })(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/attachments.js b/plugins/tiddlywiki/multiwikiserver/modules/store/attachments.js index 352f96a8386..7000afb3ddc 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/attachments.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/attachments.js @@ -34,148 +34,150 @@ Class to handle an attachment store. Options include: storePath - path to the store */ -function AttachmentStore(options) { - options = options || {}; - this.storePath = options.storePath; -} + class AttachmentStore { + constructor(options) { + options = options || {}; + this.storePath = options.storePath; + } -/* -Check if an attachment name is valid -*/ -AttachmentStore.prototype.isValidAttachmentName = function(attachment_name) { - const re = new RegExp('^[a-f0-9]{64}$'); - return re.test(attachment_name); -}; + /* + Check if an attachment name is valid + */ + isValidAttachmentName(attachment_name) { + const re = new RegExp("^[a-f0-9]{64}$"); + return re.test(attachment_name); + } -/* -Saves an attachment to a file. Options include: + /* + Saves an attachment to a file. Options include: + + text: text content (may be binary) + type: MIME type of content + reference: reference to use for debugging + _canonical_uri: canonical uri of the content + */ + saveAttachment(options) { + const path = require("path"), fs = require("fs"); + // Compute the content hash for naming the attachment + const contentHash = $tw.sjcl.codec.hex.fromBits($tw.sjcl.hash.sha256.hash(options.text)).slice(0, 64).toString(); + // Choose the best file extension for the attachment given its type + const contentTypeInfo = $tw.config.contentTypeInfo[options.type] || $tw.config.contentTypeInfo["application/octet-stream"]; + // Creat the attachment directory + const attachmentPath = path.resolve(this.storePath, "files", contentHash); + $tw.utils.createDirectory(attachmentPath); + // Save the data file + const dataFilename = "data" + contentTypeInfo.extension; + fs.writeFileSync(path.resolve(attachmentPath, dataFilename), options.text, contentTypeInfo.encoding); + // Save the meta.json file + fs.writeFileSync(path.resolve(attachmentPath, "meta.json"), JSON.stringify({ + _canonical_uri: options._canonical_uri, + created: $tw.utils.stringifyDate(new Date()), + modified: $tw.utils.stringifyDate(new Date()), + contentHash: contentHash, + filename: dataFilename, + type: options.type + }, null, 4)); + return contentHash; + } -text: text content (may be binary) -type: MIME type of content -reference: reference to use for debugging -_canonical_uri: canonical uri of the content -*/ -AttachmentStore.prototype.saveAttachment = function(options) { - const path = require("path"), - fs = require("fs"); - // Compute the content hash for naming the attachment - const contentHash = $tw.sjcl.codec.hex.fromBits($tw.sjcl.hash.sha256.hash(options.text)).slice(0,64).toString(); - // Choose the best file extension for the attachment given its type - const contentTypeInfo = $tw.config.contentTypeInfo[options.type] || $tw.config.contentTypeInfo["application/octet-stream"]; - // Creat the attachment directory - const attachmentPath = path.resolve(this.storePath,"files",contentHash); - $tw.utils.createDirectory(attachmentPath); - // Save the data file - const dataFilename = "data" + contentTypeInfo.extension; - fs.writeFileSync(path.resolve(attachmentPath,dataFilename),options.text,contentTypeInfo.encoding); - // Save the meta.json file - fs.writeFileSync(path.resolve(attachmentPath,"meta.json"),JSON.stringify({ - _canonical_uri: options._canonical_uri, - created: $tw.utils.stringifyDate(new Date()), - modified: $tw.utils.stringifyDate(new Date()), - contentHash: contentHash, - filename: dataFilename, - type: options.type - },null,4)); - return contentHash; -}; + /* + Adopts an attachment file into the store + */ + adoptAttachment(incomingFilepath, type, hash, _canonical_uri) { + const path = require("path"), fs = require("fs"); + // Choose the best file extension for the attachment given its type + const contentTypeInfo = $tw.config.contentTypeInfo[type] || $tw.config.contentTypeInfo["application/octet-stream"]; + // Creat the attachment directory + const attachmentPath = path.resolve(this.storePath, "files", hash); + $tw.utils.createDirectory(attachmentPath); + // Rename the data file + const dataFilename = "data" + contentTypeInfo.extension, dataFilepath = path.resolve(attachmentPath, dataFilename); + fs.renameSync(incomingFilepath, dataFilepath); + // Save the meta.json file + fs.writeFileSync(path.resolve(attachmentPath, "meta.json"), JSON.stringify({ + _canonical_uri: _canonical_uri, + created: $tw.utils.stringifyDate(new Date()), + modified: $tw.utils.stringifyDate(new Date()), + contentHash: hash, + filename: dataFilename, + type: type + }, null, 4)); + return hash; + } -/* -Adopts an attachment file into the store -*/ -AttachmentStore.prototype.adoptAttachment = function(incomingFilepath,type,hash,_canonical_uri) { - const path = require("path"), - fs = require("fs"); - // Choose the best file extension for the attachment given its type - const contentTypeInfo = $tw.config.contentTypeInfo[type] || $tw.config.contentTypeInfo["application/octet-stream"]; - // Creat the attachment directory - const attachmentPath = path.resolve(this.storePath,"files",hash); - $tw.utils.createDirectory(attachmentPath); - // Rename the data file - const dataFilename = "data" + contentTypeInfo.extension, - dataFilepath = path.resolve(attachmentPath,dataFilename); - fs.renameSync(incomingFilepath,dataFilepath); - // Save the meta.json file - fs.writeFileSync(path.resolve(attachmentPath,"meta.json"),JSON.stringify({ - _canonical_uri: _canonical_uri, - created: $tw.utils.stringifyDate(new Date()), - modified: $tw.utils.stringifyDate(new Date()), - contentHash: hash, - filename: dataFilename, - type: type - },null,4)); - return hash; -}; + /* + Get an attachment ready to stream. Returns null if there is an error or: + stream: filestream of file + type: type of file + */ + getAttachmentStream(attachment_name) { + const path = require("path"), fs = require("fs"); + // Check the attachment name + if(this.isValidAttachmentName(attachment_name)) { + // Construct the path to the attachment directory + const attachmentPath = path.resolve(this.storePath, "files", attachment_name); + // Read the meta.json file + const metaJsonPath = path.resolve(attachmentPath, "meta.json"); + if(fs.existsSync(metaJsonPath) && fs.statSync(metaJsonPath).isFile()) { + const meta = $tw.utils.parseJSONSafe(fs.readFileSync(metaJsonPath, "utf8"), function () { return null; }); + if(meta) { + const dataFilepath = path.resolve(attachmentPath, meta.filename); + // Check if the data file exists + if(fs.existsSync(dataFilepath) && fs.statSync(dataFilepath).isFile()) { + // Stream the file + return { + stream: fs.createReadStream(dataFilepath), + type: meta.type + }; + } + } + } + } + // An error occured + return null; + } -/* -Get an attachment ready to stream. Returns null if there is an error or: -stream: filestream of file -type: type of file -*/ -AttachmentStore.prototype.getAttachmentStream = function(attachment_name) { - const path = require("path"), - fs = require("fs"); - // Check the attachment name - if(this.isValidAttachmentName(attachment_name)) { - // Construct the path to the attachment directory - const attachmentPath = path.resolve(this.storePath,"files",attachment_name); - // Read the meta.json file - const metaJsonPath = path.resolve(attachmentPath,"meta.json"); - if(fs.existsSync(metaJsonPath) && fs.statSync(metaJsonPath).isFile()) { - const meta = $tw.utils.parseJSONSafe(fs.readFileSync(metaJsonPath,"utf8"),function() {return null;}); - if(meta) { - const dataFilepath = path.resolve(attachmentPath,meta.filename); - // Check if the data file exists - if(fs.existsSync(dataFilepath) && fs.statSync(dataFilepath).isFile()) { - // Stream the file - return { - stream: fs.createReadStream(dataFilepath), - type: meta.type - }; + /* + Get the size of an attachment file given the contentHash. + Returns the size in bytes, or null if the file doesn't exist. + */ + getAttachmentFileSize(contentHash) { + const path = require("path"), fs = require("fs"); + // Construct the path to the attachment directory + const attachmentPath = path.resolve(this.storePath, "files", contentHash); + // Read the meta.json file + const metaJsonPath = path.resolve(attachmentPath, "meta.json"); + if(fs.existsSync(metaJsonPath) && fs.statSync(metaJsonPath).isFile()) { + const meta = $tw.utils.parseJSONSafe(fs.readFileSync(metaJsonPath, "utf8"), function () { return null; }); + if(meta) { + const dataFilepath = path.resolve(attachmentPath, meta.filename); + // Check if the data file exists and return its size + if(fs.existsSync(dataFilepath) && fs.statSync(dataFilepath).isFile()) { + return fs.statSync(dataFilepath).size; + } } } + // Return null if the file doesn't exist or there was an error + return null; } - } - // An error occured - return null; -}; -/* -Get the size of an attachment file given the contentHash. -Returns the size in bytes, or null if the file doesn't exist. -*/ -AttachmentStore.prototype.getAttachmentFileSize = function(contentHash) { - const path = require("path"), - fs = require("fs"); - // Construct the path to the attachment directory - const attachmentPath = path.resolve(this.storePath, "files", contentHash); - // Read the meta.json file - const metaJsonPath = path.resolve(attachmentPath, "meta.json"); - if(fs.existsSync(metaJsonPath) && fs.statSync(metaJsonPath).isFile()) { - const meta = $tw.utils.parseJSONSafe(fs.readFileSync(metaJsonPath, "utf8"), function() { return null; }); - if(meta) { - const dataFilepath = path.resolve(attachmentPath, meta.filename); - // Check if the data file exists and return its size - if(fs.existsSync(dataFilepath) && fs.statSync(dataFilepath).isFile()) { - return fs.statSync(dataFilepath).size; + getAttachmentMetadata(attachmentBlob) { + const path = require("path"), fs = require("fs"); + const attachmentPath = path.resolve(this.storePath, "files", attachmentBlob); + const metaJsonPath = path.resolve(attachmentPath, "meta.json"); + if(fs.existsSync(metaJsonPath)) { + const metadata = JSON.parse(fs.readFileSync(metaJsonPath, "utf8")); + return metadata; } + return null; } } - // Return null if the file doesn't exist or there was an error - return null; -}; - -AttachmentStore.prototype.getAttachmentMetadata = function(attachmentBlob) { - const path = require("path"), - fs = require("fs"); - const attachmentPath = path.resolve(this.storePath, "files", attachmentBlob); - const metaJsonPath = path.resolve(attachmentPath, "meta.json"); - if(fs.existsSync(metaJsonPath)) { - const metadata = JSON.parse(fs.readFileSync(metaJsonPath, "utf8")); - return metadata; - } - return null; -}; + + + + + + exports.AttachmentStore = AttachmentStore; })(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-engine.js b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-engine.js index 00d15edf370..c65a1693cc5 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-engine.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-engine.js @@ -9,134 +9,159 @@ This class is intended to encapsulate all engine-specific logic. \*/ -(function() { - -/* -Create a database engine. Options include: - -databasePath - path to the database file (can be ":memory:" or missing to get a temporary database) -engine - wasm | better -*/ -function SqlEngine(options) { - options = options || {}; - // Initialise transaction mechanism - this.transactionDepth = 0; - // Initialise the statement cache - this.statements = Object.create(null); // Hashmap by SQL text of statement objects - // Choose engine - this.engine = options.engine || "node"; // node | wasm | better - // Create the database file directories if needed - if(options.databasePath) { - $tw.utils.createFileDirectories(options.databasePath); - } - // Create the database - const databasePath = options.databasePath || ":memory:"; - let Database; - switch(this.engine) { - case "node": - ({ DatabaseSync: Database } = require("node:sqlite")); - break; - case "wasm": - ({ Database } = require("node-sqlite3-wasm")); - break; - case "better": - Database = require("better-sqlite3"); - break; - } - this.db = new Database(databasePath,{ - verbose: undefined && console.log - }); - // Turn on WAL mode for better-sqlite3 - if(this.engine === "better") { - // See https://github.com/WiseLibs/better-sqlite3/blob/master/docs/performance.md - this.db.pragma("journal_mode = WAL"); - } -} +(function () { + + + /* + Create a database engine. Options include: + + databasePath - path to the database file (can be ":memory:" or missing to get a temporary database) + engine - wasm | better + */ + class SqlEngine { + constructor(options) { + options = options || {}; + // Initialise transaction mechanism + this.transactionDepth = 0; + // Initialise the statement cache + this.statements = Object.create(null); // Hashmap by SQL text of statement objects + + // Choose engine + this.engine = options.engine || "node"; // node | wasm | better + + // Create the database file directories if needed + if(options.databasePath) { + $tw.utils.createFileDirectories(options.databasePath); + } + // Create the database + const databasePath = options.databasePath || ":memory:"; + let Database; + switch(this.engine) { + case "node": + ({ DatabaseSync: Database } = require("node:sqlite")); + break; + case "wasm": + ({ Database } = require("node-sqlite3-wasm")); + break; + case "better": + Database = require("better-sqlite3"); + break; + } + this.db = new Database(databasePath, { + verbose: undefined && console.log + }); + // Turn on WAL mode for better-sqlite3 + if(this.engine === "better") { + // See https://github.com/WiseLibs/better-sqlite3/blob/master/docs/performance.md + this.db.pragma("journal_mode = WAL"); + } + } -SqlEngine.prototype.close = function() { - for(const sql in this.statements) { - if(this.statements[sql].finalize) { - this.statements[sql].finalize(); + async close() { + for(const sql in this.statements) { + if(this.statements[sql].finalize) { + await this.statements[sql].finalize(); + } + } + this.statements = Object.create(null); + this.db.close(); + this.db = undefined; } - } - this.statements = Object.create(null); - this.db.close(); - this.db = undefined; -}; - -SqlEngine.prototype.normaliseParams = function(params) { - params = params || {}; - const result = Object.create(null); - for(const paramName in params) { - if(this.engine !== "wasm" && paramName.startsWith("$")) { - result[paramName.slice(1)] = params[paramName]; - } else { - result[paramName] = params[paramName]; + + normaliseParams(params) { + params = params || {}; + const result = Object.create(null); + for(const paramName in params) { + if(this.engine !== "wasm" && paramName.startsWith("$")) { + result[paramName.slice(1)] = params[paramName]; + } else { + result[paramName] = params[paramName]; + } + } + return result; } - } - return result; -}; -SqlEngine.prototype.prepareStatement = function(sql) { - if(!(sql in this.statements)) { - this.statements[sql] = this.db.prepare(sql); - } - return this.statements[sql]; -}; - -SqlEngine.prototype.runStatement = function(sql,params) { - params = this.normaliseParams(params); - const statement = this.prepareStatement(sql); - return statement.run(params); -}; - -SqlEngine.prototype.runStatementGet = function(sql,params) { - params = this.normaliseParams(params); - const statement = this.prepareStatement(sql); - return statement.get(params); -}; - -SqlEngine.prototype.runStatementGetAll = function(sql,params) { - params = this.normaliseParams(params); - const statement = this.prepareStatement(sql); - return statement.all(params); -}; - -SqlEngine.prototype.runStatements = function(sqlArray) { - for(const sql of sqlArray) { - this.runStatement(sql); - } -}; - -/* -Execute the given function in a transaction, committing if successful but rolling back if an error occurs. Returns whatever the given function returns. - -Calls to this function can be safely nested, but only the topmost call will actually take place in a transaction. - -TODO: better-sqlite3 provides its own transaction method which we should be using if available -*/ -SqlEngine.prototype.transaction = function(fn) { - const alreadyInTransaction = this.transactionDepth > 0; - this.transactionDepth++; - try { - if(alreadyInTransaction) { - return fn(); - } else { - this.runStatement(`BEGIN TRANSACTION`); + async prepareStatement(sql) { + if(!(sql in this.statements)) { + // node:sqlite supports bigint, causing an error here + this.statements[sql] = await this.db.prepare(sql); + } + return this.statements[sql]; + } + + /** + * @returns {Promise} + */ + async runStatement(sql, params) { + params = this.normaliseParams(params); + const statement = await this.prepareStatement(sql); + return await statement.run(params); + } + + /** + * @param {string} sql + * @returns {Promise} + */ + async runStatementGet(sql, params) { + params = this.normaliseParams(params); + const statement = await this.prepareStatement(sql); + return await statement.get(params); + } + + /** + * @returns {Promise} + */ + async runStatementGetAll(sql, params) { + params = this.normaliseParams(params); + const statement = await this.prepareStatement(sql); + return await statement.all(params); + } + + /** + * @returns {Promise} + */ + async runStatements(sqlArray) { + const res = []; + for(const sql of sqlArray) { + res.push(await this.runStatement(sql)); + } + return res; + } + + /** + Execute the given function in a transaction, committing if successful but rolling back if an error occurs. Returns whatever the given function returns. + + Calls to this function can be safely nested, but only the topmost call will actually take place in a transaction. + + TODO: better-sqlite3 provides its own transaction method which we should be using if available + + @param {() => Promise} fn - function to execute in the transaction + @returns {Promise} - the result + @template T + */ + async transaction(fn) { + const alreadyInTransaction = this.transactionDepth > 0; + this.transactionDepth++; try { - var result = fn(); - this.runStatement(`COMMIT TRANSACTION`); - } catch(e) { - this.runStatement(`ROLLBACK TRANSACTION`); - throw(e); + if(alreadyInTransaction) { + return await fn(); + } else { + await this.runStatement("BEGIN TRANSACTION"); + try { + var result = await fn(); + await this.runStatement("COMMIT TRANSACTION"); + } catch(e) { + await this.runStatement("ROLLBACK TRANSACTION"); + throw (e); + } + return result; + } + } finally{ + this.transactionDepth--; } - return result; } - } finally { - this.transactionDepth--; } -}; -exports.SqlEngine = SqlEngine; + exports.SqlEngine = SqlEngine; })(); \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js index 14f8641f4ce..10b5c10532c 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js @@ -10,44 +10,45 @@ Validation is for the most part left to the caller \*/ -(function() { - -/* -Create a tiddler store. Options include: - -databasePath - path to the database file (can be ":memory:" to get a temporary database) -engine - wasm | better -*/ -function SqlTiddlerDatabase(options) { - options = options || {}; - const SqlEngine = require("$:/plugins/tiddlywiki/multiwikiserver/store/sql-engine.js").SqlEngine; - this.engine = new SqlEngine({ - databasePath: options.databasePath, - engine: options.engine - }); - this.entityTypeToTableMap = { - bag: { - table: "bags", - column: "bag_name" - }, - recipe: { - table: "recipes", - column: "recipe_name" - } - }; -} - -SqlTiddlerDatabase.prototype.close = function() { - this.engine.close(); -}; - - -SqlTiddlerDatabase.prototype.transaction = function(fn) { - return this.engine.transaction(fn); -}; - -SqlTiddlerDatabase.prototype.createTables = function() { - this.engine.runStatements([` +(function () { + + /* + Create a tiddler store. Options include: + + databasePath - path to the database file (can be ":memory:" to get a temporary database) + engine - wasm | better + */ + class SqlTiddlerDatabase { + constructor(options) { + options = options || {}; + /** @type {typeof import("./sql-engine").SqlEngine} */ + const SqlEngine = require("$:/plugins/tiddlywiki/multiwikiserver/store/sql-engine.js").SqlEngine; + this.engine = new SqlEngine({ + databasePath: options.databasePath, + engine: options.engine + }); + this.entityTypeToTableMap = { + bag: { + table: "bags", + column: "bag_name" + }, + recipe: { + table: "recipes", + column: "recipe_name" + } + }; + } + + async close() { + await this.engine.close(); + } + + async transaction(fn) { + return await this.engine.transaction(fn); + } + + async createTables() { + await this.engine.runStatements([` -- Users table CREATE TABLE IF NOT EXISTS users ( user_id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -57,7 +58,7 @@ SqlTiddlerDatabase.prototype.createTables = function() { created_at TEXT DEFAULT (datetime('now')), last_login TEXT ) - `,` + `, ` -- User Session table CREATE TABLE IF NOT EXISTS sessions ( user_id INTEGER NOT NULL, @@ -67,28 +68,28 @@ SqlTiddlerDatabase.prototype.createTables = function() { PRIMARY KEY (session_id), FOREIGN KEY (user_id) REFERENCES users(user_id) ) - `,` + `, ` -- Groups table CREATE TABLE IF NOT EXISTS groups ( group_id INTEGER PRIMARY KEY AUTOINCREMENT, group_name TEXT UNIQUE NOT NULL, description TEXT ) - `,` + `, ` -- Roles table CREATE TABLE IF NOT EXISTS roles ( role_id INTEGER PRIMARY KEY AUTOINCREMENT, role_name TEXT UNIQUE NOT NULL, description TEXT ) - `,` + `, ` -- Permissions table CREATE TABLE IF NOT EXISTS permissions ( permission_id INTEGER PRIMARY KEY AUTOINCREMENT, permission_name TEXT UNIQUE NOT NULL, description TEXT ) - `,` + `, ` -- User-Group association table CREATE TABLE IF NOT EXISTS user_groups ( user_id INTEGER, @@ -97,7 +98,7 @@ SqlTiddlerDatabase.prototype.createTables = function() { FOREIGN KEY (user_id) REFERENCES users(user_id), FOREIGN KEY (group_id) REFERENCES groups(group_id) ) - `,` + `, ` -- User-Role association table CREATE TABLE IF NOT EXISTS user_roles ( user_id INTEGER, @@ -106,7 +107,7 @@ SqlTiddlerDatabase.prototype.createTables = function() { FOREIGN KEY (user_id) REFERENCES users(user_id), FOREIGN KEY (role_id) REFERENCES roles(role_id) ) - `,` + `, ` -- Group-Role association table CREATE TABLE IF NOT EXISTS group_roles ( group_id INTEGER, @@ -115,7 +116,7 @@ SqlTiddlerDatabase.prototype.createTables = function() { FOREIGN KEY (group_id) REFERENCES groups(group_id), FOREIGN KEY (role_id) REFERENCES roles(role_id) ) - `,` + `, ` -- Role-Permission association table CREATE TABLE IF NOT EXISTS role_permissions ( role_id INTEGER, @@ -124,7 +125,7 @@ SqlTiddlerDatabase.prototype.createTables = function() { FOREIGN KEY (role_id) REFERENCES roles(role_id), FOREIGN KEY (permission_id) REFERENCES permissions(permission_id) ) - `,` + `, ` -- Bags have names and access control settings CREATE TABLE IF NOT EXISTS bags ( bag_id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -132,7 +133,7 @@ SqlTiddlerDatabase.prototype.createTables = function() { accesscontrol TEXT NOT NULL, description TEXT NOT NULL ) - `,` + `, ` -- Recipes have names... CREATE TABLE IF NOT EXISTS recipes ( recipe_id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -141,7 +142,7 @@ SqlTiddlerDatabase.prototype.createTables = function() { owner_id INTEGER, FOREIGN KEY (owner_id) REFERENCES users(user_id) ) - `,` + `, ` -- ...and recipes also have an ordered list of bags CREATE TABLE IF NOT EXISTS recipe_bags ( recipe_id INTEGER NOT NULL, @@ -151,7 +152,7 @@ SqlTiddlerDatabase.prototype.createTables = function() { FOREIGN KEY (bag_id) REFERENCES bags(bag_id) ON UPDATE CASCADE ON DELETE CASCADE, UNIQUE (recipe_id, bag_id) ) - `,` + `, ` -- Tiddlers are contained in bags and have titles CREATE TABLE IF NOT EXISTS tiddlers ( tiddler_id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -162,7 +163,7 @@ SqlTiddlerDatabase.prototype.createTables = function() { FOREIGN KEY (bag_id) REFERENCES bags(bag_id) ON UPDATE CASCADE ON DELETE CASCADE, UNIQUE (bag_id, title) ) - `,` + `, ` -- Tiddlers also have unordered lists of fields, each of which has a name and associated value CREATE TABLE IF NOT EXISTS fields ( tiddler_id INTEGER, @@ -171,7 +172,7 @@ SqlTiddlerDatabase.prototype.createTables = function() { FOREIGN KEY (tiddler_id) REFERENCES tiddlers(tiddler_id) ON UPDATE CASCADE ON DELETE CASCADE, UNIQUE (tiddler_id, field_name) ) - `,` + `, ` -- ACL table (using bag/recipe ids directly) CREATE TABLE IF NOT EXISTS acl ( acl_id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -182,137 +183,137 @@ SqlTiddlerDatabase.prototype.createTables = function() { FOREIGN KEY (role_id) REFERENCES roles(role_id), FOREIGN KEY (permission_id) REFERENCES permissions(permission_id) ) - `,` + `, ` -- Indexes for performance (we can add more as needed based on query patterns) CREATE INDEX IF NOT EXISTS idx_tiddlers_bag_id ON tiddlers(bag_id) - `,` + `, ` CREATE INDEX IF NOT EXISTS idx_fields_tiddler_id ON fields(tiddler_id) - `,` + `, ` CREATE INDEX IF NOT EXISTS idx_recipe_bags_recipe_id ON recipe_bags(recipe_id) - `,` + `, ` CREATE INDEX IF NOT EXISTS idx_acl_entity_id ON acl(entity_name) `]); -}; + } -SqlTiddlerDatabase.prototype.listBags = function() { - const rows = this.engine.runStatementGetAll(` + async listBags() { + const rows = await this.engine.runStatementGetAll(` SELECT bag_name, bag_id, accesscontrol, description FROM bags ORDER BY bag_name `); - return rows; -}; - -/* -Create or update a bag -Returns the bag_id of the bag -*/ -SqlTiddlerDatabase.prototype.createBag = function(bag_name,description,accesscontrol) { - accesscontrol = accesscontrol || ""; - // Run the queries - var bag = this.engine.runStatement(` + return rows; + } + + /* + Create or update a bag + Returns the bag_id of the bag + */ + async createBag(bag_name, description, accesscontrol) { + accesscontrol = accesscontrol || ""; + // Run the queries + var bag = await this.engine.runStatement(` INSERT OR IGNORE INTO bags (bag_name, accesscontrol, description) VALUES ($bag_name, '', '') - `,{ - $bag_name: bag_name - }); - const updateBags = this.engine.runStatement(` + `, { + $bag_name: bag_name + }); + const updateBags = await this.engine.runStatement(` UPDATE bags SET accesscontrol = $accesscontrol, description = $description WHERE bag_name = $bag_name - `,{ - $bag_name: bag_name, - $accesscontrol: accesscontrol, - $description: description - }); - return updateBags.lastInsertRowid; -}; - -/* -Returns array of {recipe_name:,recipe_id:,description:,bag_names: []} -*/ -SqlTiddlerDatabase.prototype.listRecipes = function() { - const rows = this.engine.runStatementGetAll(` + `, { + $bag_name: bag_name, + $accesscontrol: accesscontrol, + $description: description + }); + return updateBags.lastInsertRowid; + } + + /* + Returns array of {recipe_name:,recipe_id:,description:,bag_names: []} + */ + async listRecipes() { + const rows = await this.engine.runStatementGetAll(` SELECT r.recipe_name, r.recipe_id, r.description, r.owner_id, b.bag_name, rb.position FROM recipes AS r JOIN recipe_bags AS rb ON rb.recipe_id = r.recipe_id JOIN bags AS b ON rb.bag_id = b.bag_id ORDER BY r.recipe_name, rb.position `); - const results = []; - let currentRecipeName = null, currentRecipeIndex = -1; - for(const row of rows) { - if(row.recipe_name !== currentRecipeName) { - currentRecipeName = row.recipe_name; - currentRecipeIndex += 1; - results.push({ - recipe_name: row.recipe_name, - recipe_id: row.recipe_id, - description: row.description, - owner_id: row.owner_id, - bag_names: [] - }); - } - results[currentRecipeIndex].bag_names.push(row.bag_name); - } - return results; -}; - -/* -Create or update a recipe -Returns the recipe_id of the recipe -*/ -SqlTiddlerDatabase.prototype.createRecipe = function(recipe_name,bag_names,description) { - // Run the queries - this.engine.runStatement(` + const results = []; + let currentRecipeName = null, currentRecipeIndex = -1; + for (const row of rows) { + if (row.recipe_name !== currentRecipeName) { + currentRecipeName = row.recipe_name; + currentRecipeIndex += 1; + results.push({ + recipe_name: row.recipe_name, + recipe_id: row.recipe_id, + description: row.description, + owner_id: row.owner_id, + bag_names: [] + }); + } + results[currentRecipeIndex].bag_names.push(row.bag_name); + } + return results; + } + + /* + Create or update a recipe + Returns the recipe_id of the recipe + */ + async createRecipe(recipe_name, bag_names, description) { + // Run the queries + await this.engine.runStatement(` -- Delete existing recipe_bags entries for this recipe DELETE FROM recipe_bags WHERE recipe_id = (SELECT recipe_id FROM recipes WHERE recipe_name = $recipe_name) - `,{ - $recipe_name: recipe_name - }); - const updateRecipes = this.engine.runStatement(` + `, { + $recipe_name: recipe_name + }); + const updateRecipes = await this.engine.runStatement(` -- Create the entry in the recipes table if required INSERT OR REPLACE INTO recipes (recipe_name, description) VALUES ($recipe_name, $description) - `,{ - $recipe_name: recipe_name, - $description: description - }); - this.engine.runStatement(` + `, { + $recipe_name: recipe_name, + $description: description + }); + await this.engine.runStatement(` INSERT INTO recipe_bags (recipe_id, bag_id, position) SELECT r.recipe_id, b.bag_id, j.key as position FROM recipes r JOIN bags b INNER JOIN json_each($bag_names) AS j ON j.value = b.bag_name WHERE r.recipe_name = $recipe_name - `,{ - $recipe_name: recipe_name, - $bag_names: JSON.stringify(bag_names) - }); - - return updateRecipes.lastInsertRowid; -}; - -/* -Assign a recipe to a user -*/ -SqlTiddlerDatabase.prototype.assignRecipeToUser = function(recipe_name,user_id) { - this.engine.runStatement(` + `, { + $recipe_name: recipe_name, + $bag_names: JSON.stringify(bag_names) + }); + + return updateRecipes.lastInsertRowid; + } + + /* + Assign a recipe to a user + */ + async assignRecipeToUser(recipe_name, user_id) { + await this.engine.runStatement(` UPDATE recipes SET owner_id = $user_id WHERE recipe_name = $recipe_name - `,{ - $recipe_name: recipe_name, - $user_id: user_id - }); -}; - -/* -Returns {tiddler_id:} -*/ -SqlTiddlerDatabase.prototype.saveBagTiddler = function(tiddlerFields,bag_name,attachment_blob) { - attachment_blob = attachment_blob || null; - // Update the tiddlers table - var info = this.engine.runStatement(` + `, { + $recipe_name: recipe_name, + $user_id: user_id + }); + } + + /* + Returns {tiddler_id:} + */ + async saveBagTiddler(tiddlerFields, bag_name, attachment_blob) { + attachment_blob = attachment_blob || null; + // Update the tiddlers table + var info = await this.engine.runStatement(` INSERT OR REPLACE INTO tiddlers (bag_id, title, is_deleted, attachment_blob) VALUES ( (SELECT bag_id FROM bags WHERE bag_name = $bag_name), @@ -320,13 +321,13 @@ SqlTiddlerDatabase.prototype.saveBagTiddler = function(tiddlerFields,bag_name,at FALSE, $attachment_blob ) - `,{ - $title: tiddlerFields.title, - $attachment_blob: attachment_blob, - $bag_name: bag_name - }); - // Update the fields table - this.engine.runStatement(` + `, { + $title: tiddlerFields.title, + $attachment_blob: attachment_blob, + $bag_name: bag_name + }); + // Update the fields table + await this.engine.runStatement(` INSERT OR REPLACE INTO fields (tiddler_id, field_name, field_value) SELECT t.tiddler_id, @@ -342,22 +343,22 @@ SqlTiddlerDatabase.prototype.saveBagTiddler = function(tiddlerFields,bag_name,at ) AND title = $title ) AS t JOIN json_each($field_values) AS json_each - `,{ - $title: tiddlerFields.title, - $bag_name: bag_name, - $field_values: JSON.stringify(Object.assign({},tiddlerFields,{title: undefined})) - }); - return { - tiddler_id: info.lastInsertRowid - } -}; - -/* -Returns {tiddler_id:,bag_name:} or null if the recipe is empty -*/ -SqlTiddlerDatabase.prototype.saveRecipeTiddler = function(tiddlerFields,recipe_name,attachment_blob) { - // Find the topmost bag in the recipe - var row = this.engine.runStatementGet(` + `, { + $title: tiddlerFields.title, + $bag_name: bag_name, + $field_values: JSON.stringify(Object.assign({}, tiddlerFields, { title: undefined })) + }); + return { + tiddler_id: info.lastInsertRowid + }; + } + + /* + Returns {tiddler_id:,bag_name:} or null if the recipe is empty + */ + async saveRecipeTiddler(tiddlerFields, recipe_name, attachment_blob) { + // Find the topmost bag in the recipe + var row = await this.engine.runStatementGet(` SELECT b.bag_name FROM bags AS b JOIN ( @@ -372,26 +373,26 @@ SqlTiddlerDatabase.prototype.saveRecipeTiddler = function(tiddlerFields,recipe_n LIMIT 1 ) AS selected_bag ON b.bag_id = selected_bag.bag_id - `,{ - $recipe_name: recipe_name - }); - if(!row) { - return null; - } - // Save the tiddler to the topmost bag - var info = this.saveBagTiddler(tiddlerFields,row.bag_name,attachment_blob); - return { - tiddler_id: info.tiddler_id, - bag_name: row.bag_name - }; -}; - -/* -Returns {tiddler_id:} of the delete marker -*/ -SqlTiddlerDatabase.prototype.deleteTiddler = function(title,bag_name) { - // Delete the fields of this tiddler - this.engine.runStatement(` + `, { + $recipe_name: recipe_name + }); + if (!row) { + return null; + } + // Save the tiddler to the topmost bag + var info = await this.saveBagTiddler(tiddlerFields, row.bag_name, attachment_blob); + return { + tiddler_id: info.tiddler_id, + bag_name: row.bag_name + }; + } + + /* + Returns {tiddler_id:} of the delete marker + */ + async deleteTiddler(title, bag_name) { + // Delete the fields of this tiddler + await this.engine.runStatement(` DELETE FROM fields WHERE tiddler_id IN ( SELECT t.tiddler_id @@ -399,12 +400,12 @@ SqlTiddlerDatabase.prototype.deleteTiddler = function(title,bag_name) { INNER JOIN bags AS b ON t.bag_id = b.bag_id WHERE b.bag_name = $bag_name AND t.title = $title ) - `,{ - $title: title, - $bag_name: bag_name - }); - // Mark the tiddler itself as deleted - const rowDeleteMarker = this.engine.runStatement(` + `, { + $title: title, + $bag_name: bag_name + }); + // Mark the tiddler itself as deleted + const rowDeleteMarker = await this.engine.runStatement(` INSERT OR REPLACE INTO tiddlers (bag_id, title, is_deleted, attachment_blob) VALUES ( (SELECT bag_id FROM bags WHERE bag_name = $bag_name), @@ -412,55 +413,55 @@ SqlTiddlerDatabase.prototype.deleteTiddler = function(title,bag_name) { TRUE, NULL ) - `,{ - $title: title, - $bag_name: bag_name - }); - return {tiddler_id: rowDeleteMarker.lastInsertRowid}; -}; - -/* -returns {tiddler_id:,tiddler:,attachment_blob:} -*/ -SqlTiddlerDatabase.prototype.getBagTiddler = function(title,bag_name) { - const rowTiddler = this.engine.runStatementGet(` + `, { + $title: title, + $bag_name: bag_name + }); + return { tiddler_id: rowDeleteMarker.lastInsertRowid }; + } + + /* + returns {tiddler_id:,tiddler:,attachment_blob:} + */ + async getBagTiddler(title, bag_name) { + const rowTiddler = await this.engine.runStatementGet(` SELECT t.tiddler_id, t.attachment_blob FROM bags AS b INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id WHERE t.title = $title AND b.bag_name = $bag_name AND t.is_deleted = FALSE - `,{ - $title: title, - $bag_name: bag_name - }); - if(!rowTiddler) { - return null; - } - const rows = this.engine.runStatementGetAll(` + `, { + $title: title, + $bag_name: bag_name + }); + if (!rowTiddler) { + return null; + } + const rows = await this.engine.runStatementGetAll(` SELECT field_name, field_value, tiddler_id FROM fields WHERE tiddler_id = $tiddler_id - `,{ - $tiddler_id: rowTiddler.tiddler_id - }); - if(rows.length === 0) { - return null; - } else { - return { - tiddler_id: rows[0].tiddler_id, - attachment_blob: rowTiddler.attachment_blob, - tiddler: rows.reduce((accumulator,value) => { - accumulator[value["field_name"]] = value.field_value; - return accumulator; - },{title: title}) - }; - } -}; + `, { + $tiddler_id: rowTiddler.tiddler_id + }); + if (rows.length === 0) { + return null; + } else { + return { + tiddler_id: rows[0].tiddler_id, + attachment_blob: rowTiddler.attachment_blob, + tiddler: rows.reduce((accumulator, value) => { + accumulator[value["field_name"]] = value.field_value; + return accumulator; + }, { title: title }) + }; + } + } -/* -Returns {bag_name:, tiddler: {fields}, tiddler_id:, attachment_blob:} -*/ -SqlTiddlerDatabase.prototype.getRecipeTiddler = function(title,recipe_name) { - const rowTiddlerId = this.engine.runStatementGet(` + /* + Returns {bag_name:, tiddler: {fields}, tiddler_id:, attachment_blob:} + */ + async getRecipeTiddler(title, recipe_name) { + const rowTiddlerId = await this.engine.runStatementGet(` SELECT t.tiddler_id, t.attachment_blob, b.bag_name FROM bags AS b INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id @@ -471,74 +472,74 @@ SqlTiddlerDatabase.prototype.getRecipeTiddler = function(title,recipe_name) { AND t.is_deleted = FALSE ORDER BY rb.position DESC LIMIT 1 - `,{ - $title: title, - $recipe_name: recipe_name - }); - if(!rowTiddlerId) { - return null; - } - // Get the fields - const rows = this.engine.runStatementGetAll(` + `, { + $title: title, + $recipe_name: recipe_name + }); + if (!rowTiddlerId) { + return null; + } + // Get the fields + const rows = await this.engine.runStatementGetAll(` SELECT field_name, field_value FROM fields WHERE tiddler_id = $tiddler_id - `,{ - $tiddler_id: rowTiddlerId.tiddler_id - }); - return { - bag_name: rowTiddlerId.bag_name, - tiddler_id: rowTiddlerId.tiddler_id, - attachment_blob: rowTiddlerId.attachment_blob, - tiddler: rows.reduce((accumulator,value) => { - accumulator[value["field_name"]] = value.field_value; - return accumulator; - },{title: title}) - }; -}; - -/* -Checks if a user has permission to access a recipe -*/ -SqlTiddlerDatabase.prototype.hasRecipePermission = function(userId, recipeName, permissionName) { - try { - // check if the user is the owner of the entity - const recipe = this.engine.runStatementGet(` + `, { + $tiddler_id: rowTiddlerId.tiddler_id + }); + return { + bag_name: rowTiddlerId.bag_name, + tiddler_id: rowTiddlerId.tiddler_id, + attachment_blob: rowTiddlerId.attachment_blob, + tiddler: rows.reduce((accumulator, value) => { + accumulator[value["field_name"]] = value.field_value; + return accumulator; + }, { title: title }) + }; + } + + /* + Checks if a user has permission to access a recipe + */ + async hasRecipePermission(userId, recipeName, permissionName) { + try { + // check if the user is the owner of the entity + const recipe = await this.engine.runStatementGet(` SELECT owner_id FROM recipes WHERE recipe_name = $recipe_name `, { - $recipe_name: recipeName - }); + $recipe_name: recipeName + }); - if(!!recipe?.owner_id && recipe?.owner_id === userId) { - return true; - } else { - var permission = this.checkACLPermission(userId, "recipe", recipeName, permissionName, recipe?.owner_id) - return permission; + if (recipe && !!recipe.owner_id && recipe.owner_id === userId) { + return true; + } else { + var permission = this.checkACLPermission(userId, "recipe", recipeName, permissionName, recipe && recipe.owner_id); + return permission; + } + + } catch (error) { + console.error(error); + return false; + } + } + + /* + Checks if a user has permission to access a bag + */ + async hasBagPermission(userId, bagName, permissionName) { + return await this.checkACLPermission(userId, "bag", bagName, permissionName); } - - } catch (error) { - console.error(error) - return false - } -}; - -/* -Checks if a user has permission to access a bag -*/ -SqlTiddlerDatabase.prototype.hasBagPermission = function(userId, bagName, permissionName) { - return this.checkACLPermission(userId, "bag", bagName, permissionName) -}; - -SqlTiddlerDatabase.prototype.getACLByName = function(entityType, entityName, fetchAll) { - const entityInfo = this.entityTypeToTableMap[entityType]; - if (!entityInfo) { - throw new Error("Invalid entity type: " + entityType); - } - // First, check if there's an ACL record for the entity and get the permission_id - var checkACLExistsQuery = ` + async getACLByName(entityType, entityName, fetchAll) { + const entityInfo = this.entityTypeToTableMap[entityType]; + if (!entityInfo) { + throw new Error("Invalid entity type: " + entityType); + } + + // First, check if there's an ACL record for the entity and get the permission_id + var checkACLExistsQuery = ` SELECT acl.*, permissions.permission_name FROM acl LEFT JOIN permissions ON acl.permission_id = permissions.permission_id @@ -546,35 +547,35 @@ SqlTiddlerDatabase.prototype.getACLByName = function(entityType, entityName, fet AND acl.entity_name = $entity_name `; - if (!fetchAll) { - checkACLExistsQuery += ' LIMIT 1' - } - - const aclRecord = this.engine[fetchAll ? 'runStatementGetAll' : 'runStatementGet'](checkACLExistsQuery, { - $entity_type: entityType, - $entity_name: entityName - }); + if (!fetchAll) { + checkACLExistsQuery += " LIMIT 1"; + } - return aclRecord; -} + const aclRecord = await this.engine[fetchAll ? "runStatementGetAll" : "runStatementGet"](checkACLExistsQuery, { + $entity_type: entityType, + $entity_name: entityName + }); -SqlTiddlerDatabase.prototype.checkACLPermission = function(userId, entityType, entityName, permissionName, ownerId) { - try { - // if the entityName starts with "$:/", we'll assume its a system bag/recipe, then grant the user permission - if(entityName.startsWith("$:/")) { - return true; + return aclRecord; } - const aclRecords = this.getACLByName(entityType, entityName, true); - const aclRecord = aclRecords.find(record => record.permission_name === permissionName); + async checkACLPermission(userId, entityType, entityName, permissionName, ownerId) { + try { + // if the entityName starts with "$:/", we'll assume its a system bag/recipe, then grant the user permission + if (entityName.startsWith("$:/")) { + return true; + } - // If no ACL record exists, return true for hasPermission - if ((!aclRecord && !ownerId && aclRecords.length === 0) || ((!!aclRecord && !!ownerId) && ownerId === userId)) { - return true; - } + const aclRecords = await this.getACLByName(entityType, entityName, true); + const aclRecord = aclRecords.find(record => record.permission_name === permissionName); + + // If no ACL record exists, return true for hasPermission + if ((!aclRecord && !ownerId && aclRecords.length === 0) || ((!!aclRecord && !!ownerId) && ownerId === userId)) { + return true; + } - // If ACL record exists, check for user permission using the retrieved permission_id - const checkPermissionQuery = ` + // If ACL record exists, check for user permission using the retrieved permission_id + const checkPermissionQuery = ` SELECT * FROM users u JOIN user_roles ur ON u.user_id = ur.user_id @@ -587,58 +588,58 @@ SqlTiddlerDatabase.prototype.checkACLPermission = function(userId, entityType, e LIMIT 1 `; - const result = this.engine.runStatementGet(checkPermissionQuery, { - $user_id: userId, - $entity_type: entityType, - $entity_name: entityName, - $permission_id: aclRecord?.permission_id - }); - - let hasPermission = result !== undefined; - - return hasPermission; - - } catch (error) { - console.error(error); - return false - } -}; + const result = await this.engine.runStatementGet(checkPermissionQuery, { + $user_id: userId, + $entity_type: entityType, + $entity_name: entityName, + $permission_id: aclRecord && aclRecord.permission_id + }); + + let hasPermission = result !== undefined; -/** - * Returns the ACL records for an entity (bag or recipe) - */ -SqlTiddlerDatabase.prototype.getEntityAclRecords = function(entityName) { - const checkACLExistsQuery = ` + return hasPermission; + + } catch (error) { + console.error(error); + return false; + } + } + + /** + * Returns the ACL records for an entity (bag or recipe) + */ + async getEntityAclRecords(entityName) { + const checkACLExistsQuery = ` SELECT * FROM acl WHERE entity_name = $entity_name `; - const aclRecords = this.engine.runStatementGetAll(checkACLExistsQuery, { - $entity_name: entityName - }); - - return aclRecords -} - -/* -Get the entity by name -*/ -SqlTiddlerDatabase.prototype.getEntityByName = function(entityType, entityName) { - const entityInfo = this.entityTypeToTableMap[entityType]; - if (entityInfo) { - return this.engine.runStatementGet(`SELECT * FROM ${entityInfo.table} WHERE ${entityInfo.column} = $entity_name`, { - $entity_name: entityName - }); - } - return null; -} - -/* -Get the titles of the tiddlers in a bag. Returns an empty array for bags that do not exist -*/ -SqlTiddlerDatabase.prototype.getBagTiddlers = function(bag_name) { - const rows = this.engine.runStatementGetAll(` + const aclRecords = await this.engine.runStatementGetAll(checkACLExistsQuery, { + $entity_name: entityName + }); + + return aclRecords; + } + + /* + Get the entity by name + */ + async getEntityByName(entityType, entityName) { + const entityInfo = this.entityTypeToTableMap[entityType]; + if (entityInfo) { + return await this.engine.runStatementGet(`SELECT * FROM ${entityInfo.table} WHERE ${entityInfo.column} = $entity_name`, { + $entity_name: entityName + }); + } + return null; + } + + /* + Get the titles of the tiddlers in a bag. Returns an empty array for bags that do not exist + */ + async getBagTiddlers(bag_name) { + const rows = await this.engine.runStatementGetAll(` SELECT DISTINCT title, tiddler_id FROM tiddlers WHERE bag_id IN ( @@ -648,17 +649,17 @@ SqlTiddlerDatabase.prototype.getBagTiddlers = function(bag_name) { ) AND tiddlers.is_deleted = FALSE ORDER BY title ASC - `,{ - $bag_name: bag_name - }); - return rows; -}; - -/* -Get the tiddler_id of the newest tiddler in a bag. Returns null for bags that do not exist -*/ -SqlTiddlerDatabase.prototype.getBagLastTiddlerId = function(bag_name) { - const row = this.engine.runStatementGet(` + `, { + $bag_name: bag_name + }); + return rows; + } + + /* + Get the tiddler_id of the newest tiddler in a bag. Returns null for bags that do not exist + */ + async getBagLastTiddlerId(bag_name) { + const row = await this.engine.runStatementGet(` SELECT tiddler_id FROM tiddlers WHERE bag_id IN ( @@ -668,51 +669,51 @@ SqlTiddlerDatabase.prototype.getBagLastTiddlerId = function(bag_name) { ) ORDER BY tiddler_id DESC LIMIT 1 - `,{ - $bag_name: bag_name - }); - if(row) { - return row.tiddler_id; - } else { - return null; - } -}; - -/* -Get the metadata of the tiddlers in a recipe as an array [{title:,tiddler_id:,bag_name:,is_deleted:}], -sorted in ascending order of tiddler_id. - -Options include: - -limit: optional maximum number of results to return -last_known_tiddler_id: tiddler_id of the last known update. Only returns tiddlers that have been created, modified or deleted since -include_deleted: boolean, defaults to false + `, { + $bag_name: bag_name + }); + if (row) { + return row.tiddler_id; + } else { + return null; + } + } -Returns null for recipes that do not exist -*/ -SqlTiddlerDatabase.prototype.getRecipeTiddlers = function(recipe_name,options) { - options = options || {}; - // Get the recipe ID - const rowsCheckRecipe = this.engine.runStatementGet(` + /* + Get the metadata of the tiddlers in a recipe as an array [{title:,tiddler_id:,bag_name:,is_deleted:}], + sorted in ascending order of tiddler_id. + + Options include: + + limit: optional maximum number of results to return + last_known_tiddler_id: tiddler_id of the last known update. Only returns tiddlers that have been created, modified or deleted since + include_deleted: boolean, defaults to false + + Returns null for recipes that do not exist + */ + async getRecipeTiddlers(recipe_name, options) { + options = options || {}; + // Get the recipe ID + const rowsCheckRecipe = await this.engine.runStatementGet(` SELECT recipe_id FROM recipes WHERE recipes.recipe_name = $recipe_name - `,{ - $recipe_name: recipe_name - }); - if(!rowsCheckRecipe) { - return null; - } - const recipe_id = rowsCheckRecipe.recipe_id; - // Compose the query to get the tiddlers - const params = { - $recipe_id: recipe_id - } - if(options.limit) { - params.$limit = options.limit.toString(); - } - if(options.last_known_tiddler_id) { - params.$last_known_tiddler_id = options.last_known_tiddler_id; - } - const rows = this.engine.runStatementGetAll(` + `, { + $recipe_name: recipe_name + }); + if (!rowsCheckRecipe) { + return null; + } + const recipe_id = rowsCheckRecipe.recipe_id; + // Compose the query to get the tiddlers + const params = { + $recipe_id: recipe_id + }; + if (options.limit) { + params.$limit = options.limit.toString(); + } + if (options.last_known_tiddler_id) { + params.$last_known_tiddler_id = options.last_known_tiddler_id; + } + const rows = await this.engine.runStatementGetAll(` SELECT title, tiddler_id, is_deleted, bag_name FROM ( SELECT t.title, t.tiddler_id, t.is_deleted, b.bag_name, MAX(rb.position) AS position @@ -726,15 +727,15 @@ SqlTiddlerDatabase.prototype.getRecipeTiddlers = function(recipe_name,options) { ORDER BY t.title, tiddler_id DESC ${options.limit ? "LIMIT $limit" : ""} ) - `,params); - return rows; -}; - -/* -Get the tiddler_id of the newest tiddler in a recipe. Returns null for recipes that do not exist -*/ -SqlTiddlerDatabase.prototype.getRecipeLastTiddlerId = function(recipe_name) { - const row = this.engine.runStatementGet(` + `, params); + return rows; + } + + /* + Get the tiddler_id of the newest tiddler in a recipe. Returns null for recipes that do not exist + */ + async getRecipeLastTiddlerId(recipe_name) { + const row = await this.engine.runStatementGet(` SELECT t.title, t.tiddler_id, b.bag_name, MAX(rb.position) AS position FROM bags AS b INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id @@ -744,19 +745,19 @@ SqlTiddlerDatabase.prototype.getRecipeLastTiddlerId = function(recipe_name) { GROUP BY t.title ORDER BY t.tiddler_id DESC LIMIT 1 - `,{ - $recipe_name: recipe_name - }); - if(row) { - return row.tiddler_id; - } else { - return null; - } -}; + `, { + $recipe_name: recipe_name + }); + if (row) { + return row.tiddler_id; + } else { + return null; + } + } -SqlTiddlerDatabase.prototype.deleteAllTiddlersInBag = function(bag_name) { - // Delete the fields - this.engine.runStatement(` + async deleteAllTiddlersInBag(bag_name) { + // Delete the fields + await this.engine.runStatement(` DELETE FROM fields WHERE tiddler_id IN ( SELECT tiddler_id @@ -764,25 +765,25 @@ SqlTiddlerDatabase.prototype.deleteAllTiddlersInBag = function(bag_name) { WHERE bag_id = (SELECT bag_id FROM bags WHERE bag_name = $bag_name) AND is_deleted = FALSE ) - `,{ - $bag_name: bag_name - }); - // Mark the tiddlers as deleted - this.engine.runStatement(` + `, { + $bag_name: bag_name + }); + // Mark the tiddlers as deleted + await this.engine.runStatement(` UPDATE tiddlers SET is_deleted = TRUE WHERE bag_id = (SELECT bag_id FROM bags WHERE bag_name = $bag_name) AND is_deleted = FALSE - `,{ - $bag_name: bag_name - }); -}; - -/* -Get the names of the bags in a recipe. Returns an empty array for recipes that do not exist -*/ -SqlTiddlerDatabase.prototype.getRecipeBags = function(recipe_name) { - const rows = this.engine.runStatementGetAll(` + `, { + $bag_name: bag_name + }); + } + + /* + Get the names of the bags in a recipe. Returns an empty array for recipes that do not exist + */ + async getRecipeBags(recipe_name) { + const rows = await this.engine.runStatementGetAll(` SELECT bags.bag_name FROM bags JOIN ( @@ -793,33 +794,33 @@ SqlTiddlerDatabase.prototype.getRecipeBags = function(recipe_name) { ORDER BY rb.position ) AS bag_priority ON bags.bag_id = bag_priority.bag_id ORDER BY position - `,{ - $recipe_name: recipe_name - }); - return rows.map(value => value.bag_name); -}; - -/* -Get the attachment value of a bag, if any exist -*/ -SqlTiddlerDatabase.prototype.getBagTiddlerAttachmentBlob = function(title,bag_name) { - const row = this.engine.runStatementGet(` + `, { + $recipe_name: recipe_name + }); + return rows.map(value => value.bag_name); + } + + /* + Get the attachment value of a bag, if any exist + */ + async getBagTiddlerAttachmentBlob(title, bag_name) { + const row = await this.engine.runStatementGet(` SELECT t.attachment_blob FROM bags AS b INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id WHERE t.title = $title AND b.bag_name = $bag_name AND t.is_deleted = FALSE `, { - $title: title, - $bag_name: bag_name - }); - return row ? row.attachment_blob : null; -}; - -/* -Get the attachment value of a recipe, if any exist -*/ -SqlTiddlerDatabase.prototype.getRecipeTiddlerAttachmentBlob = function(title,recipe_name) { - const row = this.engine.runStatementGet(` + $title: title, + $bag_name: bag_name + }); + return row ? row.attachment_blob : null; + } + + /* + Get the attachment value of a recipe, if any exist + */ + async getRecipeTiddlerAttachmentBlob(title, recipe_name) { + const row = await this.engine.runStatementGet(` SELECT t.attachment_blob FROM bags AS b INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id @@ -829,651 +830,747 @@ SqlTiddlerDatabase.prototype.getRecipeTiddlerAttachmentBlob = function(title,rec ORDER BY rb.position DESC LIMIT 1 `, { - $title: title, - $recipe_name: recipe_name - }); - return row ? row.attachment_blob : null; -}; + $title: title, + $recipe_name: recipe_name + }); + return row ? row.attachment_blob : null; + } -// User CRUD operations -SqlTiddlerDatabase.prototype.createUser = function(username, email, password) { - const result = this.engine.runStatement(` + // User CRUD operations + async createUser(username, email, password) { + const result = await this.engine.runStatement(` INSERT INTO users (username, email, password) VALUES ($username, $email, $password) `, { - $username: username, - $email: email, - $password: password - }); - return result.lastInsertRowid; -}; + $username: username, + $email: email, + $password: password + }); + return result.lastInsertRowid; + } -SqlTiddlerDatabase.prototype.getUser = function(userId) { - return this.engine.runStatementGet(` + async getUser(userId) { + return await this.engine.runStatementGet(` SELECT * FROM users WHERE user_id = $userId `, { - $userId: userId - }); -}; + $userId: userId + }); + } -SqlTiddlerDatabase.prototype.getUserByUsername = function(username) { - return this.engine.runStatementGet(` + async getUserByUsername(username) { + return await this.engine.runStatementGet(` SELECT * FROM users WHERE username = $username `, { - $username: username - }); -}; + $username: username + }); + } -SqlTiddlerDatabase.prototype.getUserByEmail = function(email) { - return this.engine.runStatementGet(` + async getUserByEmail(email) { + return await this.engine.runStatementGet(` SELECT * FROM users WHERE email = $email `, { - $email: email - }); -}; + $email: email + }); + } -SqlTiddlerDatabase.prototype.listUsersByRoleId = function(roleId) { - return this.engine.runStatementGetAll(` + async listUsersByRoleId(roleId) { + return await this.engine.runStatementGetAll(` SELECT u.* FROM users u JOIN user_roles ur ON u.user_id = ur.user_id WHERE ur.role_id = $roleId ORDER BY u.username `, { - $roleId: roleId - }); -}; + $roleId: roleId + }); + } -SqlTiddlerDatabase.prototype.updateUser = function (userId, username, email, roleId) { - const existingUser = this.engine.runStatement(` + async updateUser(userId, username, email, roleId) { + const existingUser = await this.engine.runStatementGet(` SELECT user_id FROM users WHERE email = $email AND user_id != $userId `, { - $email: email, - $userId: userId - }); - - if (existingUser.length > 0) { - return { - success: false, - message: "Email address already in use by another user." - }; - } + $email: email, + $userId: userId + }); - try { - this.engine.transaction(() => { - // Update user information - this.engine.runStatement(` + if (existingUser.length > 0) { + return { + success: false, + message: "Email address already in use by another user." + }; + } + + try { + await this.engine.transaction(async () => { + // Update user information + await this.engine.runStatement(` UPDATE users SET username = $username, email = $email WHERE user_id = $userId `, { - $userId: userId, - $username: username, - $email: email - }); - - if (roleId) { - // Remove all existing roles for the user - this.engine.runStatement(` + $userId: userId, + $username: username, + $email: email + }); + + if (roleId) { + // Remove all existing roles for the user + await this.engine.runStatement(` DELETE FROM user_roles WHERE user_id = $userId `, { - $userId: userId - }); + $userId: userId + }); - // Add the new role - this.engine.runStatement(` + // Add the new role + await this.engine.runStatement(` INSERT INTO user_roles (user_id, role_id) VALUES ($userId, $roleId) `, { - $userId: userId, - $roleId: roleId + $userId: userId, + $roleId: roleId + }); + } }); + + return { + success: true, + message: "User profile and role updated successfully." + }; + } catch (error) { + return { + success: false, + message: "Failed to update user profile: " + error.message + }; } - }); - - return { - success: true, - message: "User profile and role updated successfully." - }; - } catch (error) { - return { - success: false, - message: "Failed to update user profile: " + error.message - }; - } -}; + } -SqlTiddlerDatabase.prototype.updateUserPassword = function (userId, newHash) { - try { - this.engine.runStatement(` + async updateUserPassword(userId, newHash) { + try { + await this.engine.runStatement(` UPDATE users SET password = $newHash WHERE user_id = $userId `, { - $userId: userId, - $newHash: newHash, - }); - - return { - success: true, - message: "Password updated successfully." - }; - } catch (error) { - return { - success: false, - message: "Failed to update password: " + error.message - }; - } -}; + $userId: userId, + $newHash: newHash, + }); + + return { + success: true, + message: "Password updated successfully." + }; + } catch (error) { + return { + success: false, + message: "Failed to update password: " + error.message + }; + } + } -SqlTiddlerDatabase.prototype.deleteUser = function(userId) { - this.engine.runStatement(` + async deleteUser(userId) { + await this.engine.runStatement(` DELETE FROM users WHERE user_id = $userId `, { - $userId: userId - }); -}; + $userId: userId + }); + } -SqlTiddlerDatabase.prototype.listUsers = function() { - return this.engine.runStatementGetAll(` + async listUsers() { + return await this.engine.runStatementGetAll(` SELECT * FROM users ORDER BY username `); -}; + } -SqlTiddlerDatabase.prototype.createOrUpdateUserSession = function(userId, sessionId) { - const currentTimestamp = new Date().toISOString(); + async createOrUpdateUserSession(userId, sessionId) { + const currentTimestamp = new Date().toISOString(); - // First, try to update an existing session - const updateResult = this.engine.runStatement(` + // First, try to update an existing session + const updateResult = await this.engine.runStatement(` UPDATE sessions SET session_id = $sessionId, last_accessed = $timestamp WHERE user_id = $userId `, { - $userId: userId, - $sessionId: sessionId, - $timestamp: currentTimestamp - }); + $userId: userId, + $sessionId: sessionId, + $timestamp: currentTimestamp + }); - // If no existing session was updated, create a new one - if (updateResult.changes === 0) { - this.engine.runStatement(` + // If no existing session was updated, create a new one + if (updateResult.changes === 0) { + await this.engine.runStatement(` INSERT INTO sessions (user_id, session_id, created_at, last_accessed) VALUES ($userId, $sessionId, $timestamp, $timestamp) `, { $userId: userId, $sessionId: sessionId, $timestamp: currentTimestamp - }); - } + }); + } - return sessionId; -}; + return sessionId; + } -SqlTiddlerDatabase.prototype.createUserSession = function(userId, sessionId) { - const currentTimestamp = new Date().toISOString(); - this.engine.runStatement(` + async createUserSession(userId, sessionId) { + const currentTimestamp = new Date().toISOString(); + await this.engine.runStatement(` INSERT INTO sessions (user_id, session_id, created_at, last_accessed) VALUES ($userId, $sessionId, $timestamp, $timestamp) `, { - $userId: userId, - $sessionId: sessionId, - $timestamp: currentTimestamp - }); + $userId: userId, + $sessionId: sessionId, + $timestamp: currentTimestamp + }); - return sessionId; -}; + return sessionId; + } -SqlTiddlerDatabase.prototype.findUserBySessionId = function(sessionId) { - // First, get the user_id from the sessions table - const sessionResult = this.engine.runStatementGet(` + /** + * @typedef {Object} User + * @property {number} user_id + * @property {string} username + * @property {string} email + * @property {string?} password + * @property {string} created_at + * @property {string} last_login + */ + /** + * + * @param {any} sessionId + * @returns {Promise} + */ + async findUserBySessionId(sessionId) { + // First, get the user_id from the sessions table + const sessionResult = await this.engine.runStatementGet(` SELECT user_id, last_accessed FROM sessions WHERE session_id = $sessionId `, { - $sessionId: sessionId - }); + $sessionId: sessionId + }); - if (!sessionResult) { - return null; // Session not found - } + if (!sessionResult) { + return null; // Session not found + } - const lastAccessed = new Date(sessionResult.last_accessed); - const expirationTime = 24 * 60 * 60 * 1000; // 24 hours in milliseconds - if (new Date() - lastAccessed > expirationTime) { - // Session has expired - this.deleteSession(sessionId); - return null; - } + const lastAccessed = new Date(sessionResult.last_accessed); + const expirationTime = 24 * 60 * 60 * 1000; // 24 hours in milliseconds + if (+new Date() - +lastAccessed > expirationTime) { + // Session has expired + await this.deleteSession(sessionId); + return null; + } - // Update the last_accessed timestamp - const currentTimestamp = new Date().toISOString(); - this.engine.runStatement(` + // Update the last_accessed timestamp + const currentTimestamp = new Date().toISOString(); + await this.engine.runStatement(` UPDATE sessions SET last_accessed = $timestamp WHERE session_id = $sessionId `, { - $sessionId: sessionId, - $timestamp: currentTimestamp - }); + $sessionId: sessionId, + $timestamp: currentTimestamp + }); - const userResult = this.engine.runStatementGet(` + /** @type {any} */ + const userResult = await this.engine.runStatementGet(` SELECT * FROM users WHERE user_id = $userId `, { - $userId: sessionResult.user_id - }); + $userId: sessionResult.user_id + }); - if (!userResult) { - return null; - } + if (!userResult) { + return null; + } - return userResult; -}; + /** @type {User} */ + return userResult; + } -SqlTiddlerDatabase.prototype.deleteSession = function(sessionId) { - this.engine.runStatement(` + async deleteSession(sessionId) { + await this.engine.runStatement(` DELETE FROM sessions WHERE session_id = $sessionId `, { - $sessionId: sessionId - }); -}; + $sessionId: sessionId + }); + } -SqlTiddlerDatabase.prototype.deleteUserSessions = function(userId) { - this.engine.runStatement(` + async deleteUserSessions(userId) { + await this.engine.runStatement(` DELETE FROM sessions WHERE user_id = $userId `, { - $userId: userId - }); -}; + $userId: userId + }); + } -// Set the user as an admin -SqlTiddlerDatabase.prototype.setUserAdmin = function(userId) { - var admin = this.getRoleByName("ADMIN"); - if(admin) { - this.addRoleToUser(userId, admin.role_id); - } -}; + // Set the user as an admin + async setUserAdmin(userId) { + var admin = await this.getRoleByName("ADMIN"); + if (admin) { + await this.addRoleToUser(userId, admin.role_id); + } + } -// Group CRUD operations -SqlTiddlerDatabase.prototype.createGroup = function(groupName, description) { - const result = this.engine.runStatement(` + // Group CRUD operations + async createGroup(groupName, description) { + const result = await this.engine.runStatement(` INSERT INTO groups (group_name, description) VALUES ($groupName, $description) `, { - $groupName: groupName, - $description: description - }); - return result.lastInsertRowid; -}; + $groupName: groupName, + $description: description + }); + return result.lastInsertRowid; + } -SqlTiddlerDatabase.prototype.getGroup = function(groupId) { - return this.engine.runStatementGet(` + async getGroup(groupId) { + return await this.engine.runStatementGet(` SELECT * FROM groups WHERE group_id = $groupId `, { - $groupId: groupId - }); -}; + $groupId: groupId + }); + } -SqlTiddlerDatabase.prototype.updateGroup = function(groupId, groupName, description) { - this.engine.runStatement(` + async updateGroup(groupId, groupName, description) { + await this.engine.runStatement(` UPDATE groups SET group_name = $groupName, description = $description WHERE group_id = $groupId `, { - $groupId: groupId, - $groupName: groupName, - $description: description - }); -}; + $groupId: groupId, + $groupName: groupName, + $description: description + }); + } -SqlTiddlerDatabase.prototype.deleteGroup = function(groupId) { - this.engine.runStatement(` + async deleteGroup(groupId) { + await this.engine.runStatement(` DELETE FROM groups WHERE group_id = $groupId `, { - $groupId: groupId - }); -}; + $groupId: groupId + }); + } -SqlTiddlerDatabase.prototype.listGroups = function() { - return this.engine.runStatementGetAll(` + async listGroups() { + return await this.engine.runStatementGetAll(` SELECT * FROM groups ORDER BY group_name `); -}; + } -// Role CRUD operations -SqlTiddlerDatabase.prototype.createRole = function(roleName, description) { - const result = this.engine.runStatement(` + // Role CRUD operations + async createRole(roleName, description) { + const result = await this.engine.runStatement(` INSERT OR IGNORE INTO roles (role_name, description) VALUES ($roleName, $description) `, { - $roleName: roleName, - $description: description - }); - return result.lastInsertRowid; -}; + $roleName: roleName, + $description: description + }); + return result.lastInsertRowid; + } -SqlTiddlerDatabase.prototype.getRole = function(roleId) { - return this.engine.runStatementGet(` + async getRole(roleId) { + return await this.engine.runStatementGet(` SELECT * FROM roles WHERE role_id = $roleId `, { - $roleId: roleId - }); -}; + $roleId: roleId + }); + } -SqlTiddlerDatabase.prototype.getRoleByName = function(roleName) { - return this.engine.runStatementGet(` + async getRoleByName(roleName) { + return await this.engine.runStatementGet(` SELECT * FROM roles WHERE role_name = $roleName `, { - $roleName: roleName - }); -} + $roleName: roleName + }); + } -SqlTiddlerDatabase.prototype.updateRole = function(roleId, roleName, description) { - this.engine.runStatement(` + async updateRole(roleId, roleName, description) { + await this.engine.runStatement(` UPDATE roles SET role_name = $roleName, description = $description WHERE role_id = $roleId `, { - $roleId: roleId, - $roleName: roleName, - $description: description - }); -}; + $roleId: roleId, + $roleName: roleName, + $description: description + }); + } -SqlTiddlerDatabase.prototype.deleteRole = function(roleId) { - this.engine.runStatement(` + async deleteRole(roleId) { + await this.engine.runStatement(` DELETE FROM roles WHERE role_id = $roleId `, { - $roleId: roleId - }); -}; + $roleId: roleId + }); + } -SqlTiddlerDatabase.prototype.listRoles = function() { - return this.engine.runStatementGetAll(` + async listRoles() { + return await this.engine.runStatementGetAll(` SELECT * FROM roles ORDER BY role_name DESC `); -}; + } -// Permission CRUD operations -SqlTiddlerDatabase.prototype.createPermission = function(permissionName, description) { - const result = this.engine.runStatement(` + // Permission CRUD operations + async createPermission(permissionName, description) { + const result = await this.engine.runStatement(` INSERT OR IGNORE INTO permissions (permission_name, description) VALUES ($permissionName, $description) `, { - $permissionName: permissionName, - $description: description - }); - return result.lastInsertRowid; -}; + $permissionName: permissionName, + $description: description + }); + return result.lastInsertRowid; + } -SqlTiddlerDatabase.prototype.getPermission = function(permissionId) { - return this.engine.runStatementGet(` + async getPermission(permissionId) { + return await this.engine.runStatementGet(` SELECT * FROM permissions WHERE permission_id = $permissionId `, { - $permissionId: permissionId - }); -}; + $permissionId: permissionId + }); + } -SqlTiddlerDatabase.prototype.getPermissionByName = function(permissionName) { - return this.engine.runStatementGet(` + async getPermissionByName(permissionName) { + return await this.engine.runStatementGet(` SELECT * FROM permissions WHERE permission_name = $permissionName `, { - $permissionName: permissionName - }); -}; + $permissionName: permissionName + }); + } -SqlTiddlerDatabase.prototype.updatePermission = function(permissionId, permissionName, description) { - this.engine.runStatement(` + async updatePermission(permissionId, permissionName, description) { + await this.engine.runStatement(` UPDATE permissions SET permission_name = $permissionName, description = $description WHERE permission_id = $permissionId `, { - $permissionId: permissionId, - $permissionName: permissionName, - $description: description - }); -}; + $permissionId: permissionId, + $permissionName: permissionName, + $description: description + }); + } -SqlTiddlerDatabase.prototype.deletePermission = function(permissionId) { - this.engine.runStatement(` + async deletePermission(permissionId) { + await this.engine.runStatement(` DELETE FROM permissions WHERE permission_id = $permissionId `, { - $permissionId: permissionId - }); -}; + $permissionId: permissionId + }); + } -SqlTiddlerDatabase.prototype.listPermissions = function() { - return this.engine.runStatementGetAll(` + async listPermissions() { + return await this.engine.runStatementGetAll(` SELECT * FROM permissions ORDER BY permission_name `); -}; + } -// ACL CRUD operations -SqlTiddlerDatabase.prototype.createACL = function(entityName, entityType, roleId, permissionId) { - if(!entityName.startsWith("$:/")) { - const result = this.engine.runStatement(` + // ACL CRUD operations + async createACL(entityName, entityType, roleId, permissionId) { + if (!entityName.startsWith("$:/")) { + const result = await this.engine.runStatement(` INSERT OR IGNORE INTO acl (entity_name, entity_type, role_id, permission_id) VALUES ($entityName, $entityType, $roleId, $permissionId) - `, - { - $entityName: entityName, - $entityType: entityType, - $roleId: roleId, - $permissionId: permissionId - }); - return result.lastInsertRowid; - } -}; + `, + { + $entityName: entityName, + $entityType: entityType, + $roleId: roleId, + $permissionId: permissionId + }); + return result.lastInsertRowid; + } + } -SqlTiddlerDatabase.prototype.getACL = function(aclId) { - return this.engine.runStatementGet(` + async getACL(aclId) { + return await this.engine.runStatementGet(` SELECT * FROM acl WHERE acl_id = $aclId `, { - $aclId: aclId - }); -}; + $aclId: aclId + }); + } -SqlTiddlerDatabase.prototype.updateACL = function(aclId, entityId, entityType, roleId, permissionId) { - this.engine.runStatement(` + async updateACL(aclId, entityId, entityType, roleId, permissionId) { + await this.engine.runStatement(` UPDATE acl SET entity_name = $entityId, entity_type = $entityType, role_id = $roleId, permission_id = $permissionId WHERE acl_id = $aclId `, { - $aclId: aclId, - $entityId: entityId, - $entityType: entityType, - $roleId: roleId, - $permissionId: permissionId - }); -}; + $aclId: aclId, + $entityId: entityId, + $entityType: entityType, + $roleId: roleId, + $permissionId: permissionId + }); + } -SqlTiddlerDatabase.prototype.deleteACL = function(aclId) { - this.engine.runStatement(` + async deleteACL(aclId) { + await this.engine.runStatement(` DELETE FROM acl WHERE acl_id = $aclId `, { - $aclId: aclId - }); -}; + $aclId: aclId + }); + } -SqlTiddlerDatabase.prototype.listACLs = function() { - return this.engine.runStatementGetAll(` + async listACLs() { + return await this.engine.runStatementGetAll(` SELECT * FROM acl ORDER BY entity_type, entity_name `); -}; + } -// Association management functions -SqlTiddlerDatabase.prototype.addUserToGroup = function(userId, groupId) { - this.engine.runStatement(` + // Association management functions + async addUserToGroup(userId, groupId) { + await this.engine.runStatement(` INSERT OR IGNORE INTO user_groups (user_id, group_id) VALUES ($userId, $groupId) `, { - $userId: userId, - $groupId: groupId - }); -}; + $userId: userId, + $groupId: groupId + }); + } -SqlTiddlerDatabase.prototype.isUserInGroup = function(userId, groupId) { - const result = this.engine.runStatementGet(` + async isUserInGroup(userId, groupId) { + const result = await this.engine.runStatementGet(` SELECT 1 FROM user_groups WHERE user_id = $userId AND group_id = $groupId `, { - $userId: userId, - $groupId: groupId - }); - return result !== undefined; -}; + $userId: userId, + $groupId: groupId + }); + return result !== undefined; + } -SqlTiddlerDatabase.prototype.removeUserFromGroup = function(userId, groupId) { - this.engine.runStatement(` + async removeUserFromGroup(userId, groupId) { + await this.engine.runStatement(` DELETE FROM user_groups WHERE user_id = $userId AND group_id = $groupId `, { - $userId: userId, - $groupId: groupId - }); -}; + $userId: userId, + $groupId: groupId + }); + } -SqlTiddlerDatabase.prototype.addRoleToUser = function(userId, roleId) { - this.engine.runStatement(` + async addRoleToUser(userId, roleId) { + await this.engine.runStatement(` INSERT OR IGNORE INTO user_roles (user_id, role_id) VALUES ($userId, $roleId) `, { - $userId: userId, - $roleId: roleId - }); -}; + $userId: userId, + $roleId: roleId + }); + } -SqlTiddlerDatabase.prototype.removeRoleFromUser = function(userId, roleId) { - this.engine.runStatement(` + async removeRoleFromUser(userId, roleId) { + await this.engine.runStatement(` DELETE FROM user_roles WHERE user_id = $userId AND role_id = $roleId `, { - $userId: userId, - $roleId: roleId - }); -}; + $userId: userId, + $roleId: roleId + }); + } -SqlTiddlerDatabase.prototype.addRoleToGroup = function(groupId, roleId) { - this.engine.runStatement(` + async addRoleToGroup(groupId, roleId) { + await this.engine.runStatement(` INSERT OR IGNORE INTO group_roles (group_id, role_id) VALUES ($groupId, $roleId) `, { - $groupId: groupId, - $roleId: roleId - }); -}; + $groupId: groupId, + $roleId: roleId + }); + } -SqlTiddlerDatabase.prototype.removeRoleFromGroup = function(groupId, roleId) { - this.engine.runStatement(` + async removeRoleFromGroup(groupId, roleId) { + await this.engine.runStatement(` DELETE FROM group_roles WHERE group_id = $groupId AND role_id = $roleId `, { - $groupId: groupId, - $roleId: roleId - }); -}; + $groupId: groupId, + $roleId: roleId + }); + } -SqlTiddlerDatabase.prototype.addPermissionToRole = function(roleId, permissionId) { - this.engine.runStatement(` + async addPermissionToRole(roleId, permissionId) { + await this.engine.runStatement(` INSERT OR IGNORE INTO role_permissions (role_id, permission_id) VALUES ($roleId, $permissionId) `, { - $roleId: roleId, - $permissionId: permissionId - }); -}; + $roleId: roleId, + $permissionId: permissionId + }); + } -SqlTiddlerDatabase.prototype.removePermissionFromRole = function(roleId, permissionId) { - this.engine.runStatement(` + async removePermissionFromRole(roleId, permissionId) { + await this.engine.runStatement(` DELETE FROM role_permissions WHERE role_id = $roleId AND permission_id = $permissionId `, { - $roleId: roleId, - $permissionId: permissionId - }); -}; + $roleId: roleId, + $permissionId: permissionId + }); + } -SqlTiddlerDatabase.prototype.getUserRoles = function(userId) { - const query = ` + async getUserRoles(userId) { + const query = ` SELECT r.role_id, r.role_name FROM user_roles ur JOIN roles r ON ur.role_id = r.role_id WHERE ur.user_id = $userId LIMIT 1 `; - - return this.engine.runStatementGet(query, { $userId: userId }); -}; -SqlTiddlerDatabase.prototype.deleteUserRolesByRoleId = function(roleId) { - this.engine.runStatement(` + return await this.engine.runStatementGet(query, { $userId: userId }); + } + + async deleteUserRolesByRoleId(roleId) { + await this.engine.runStatement(` DELETE FROM user_roles WHERE role_id = $roleId `, { - $roleId: roleId - }); -}; + $roleId: roleId + }); + } -SqlTiddlerDatabase.prototype.deleteUserRolesByUserId = function(userId) { - this.engine.runStatement(` + async deleteUserRolesByUserId(userId) { + await this.engine.runStatement(` DELETE FROM user_roles WHERE user_id = $userId `, { - $userId: userId - }); -}; + $userId: userId + }); + } -SqlTiddlerDatabase.prototype.isRoleInUse = function(roleId) { - // Check if the role is assigned to any users - const userRoleCheck = this.engine.runStatementGet(` + async isRoleInUse(roleId) { + // Check if the role is assigned to any users + const userRoleCheck = await this.engine.runStatementGet(` SELECT 1 FROM user_roles WHERE role_id = $roleId LIMIT 1 `, { - $roleId: roleId - }); + $roleId: roleId + }); - if(userRoleCheck) { - return true; - } + if (userRoleCheck) { + return true; + } - // Check if the role is used in any ACLs - const aclRoleCheck = this.engine.runStatementGet(` + // Check if the role is used in any ACLs + const aclRoleCheck = await this.engine.runStatementGet(` SELECT 1 FROM acl WHERE role_id = $roleId LIMIT 1 `, { - $roleId: roleId - }); + $roleId: roleId + }); - if(aclRoleCheck) { - return true; - } + if (aclRoleCheck) { + return true; + } - // If we've reached this point, the role is not in use - return false; -}; + // If we've reached this point, the role is not in use + return false; + } -SqlTiddlerDatabase.prototype.getRoleById = function(roleId) { - const role = this.engine.runStatementGet(` + async getRoleById(roleId) { + const role = await this.engine.runStatementGet(` SELECT role_id, role_name, description FROM roles WHERE role_id = $roleId `, { - $roleId: roleId - }); + $roleId: roleId + }); + + return role; + } + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - return role; -}; -exports.SqlTiddlerDatabase = SqlTiddlerDatabase; + exports.SqlTiddlerDatabase = SqlTiddlerDatabase; })(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js index bb32eba18d3..452857c3094 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js @@ -13,426 +13,448 @@ This class is largely a wrapper for the sql-tiddler-database.js class, adding th \*/ -(function() { - -/* -Create a tiddler store. Options include: - -databasePath - path to the database file (can be ":memory:" to get a temporary database) -adminWiki - reference to $tw.Wiki object used for configuration -attachmentStore - reference to associated attachment store -engine - wasm | better -*/ -function SqlTiddlerStore(options) { - options = options || {}; - this.attachmentStore = options.attachmentStore; - this.adminWiki = options.adminWiki || $tw.wiki; - this.eventListeners = {}; // Hashmap by type of array of event listener functions - this.eventOutstanding = {}; // Hashmap by type of boolean true of outstanding events - // Create the database - this.databasePath = options.databasePath || ":memory:"; - var SqlTiddlerDatabase = require("$:/plugins/tiddlywiki/multiwikiserver/store/sql-tiddler-database.js").SqlTiddlerDatabase; - this.sqlTiddlerDatabase = new SqlTiddlerDatabase({ - databasePath: this.databasePath, - engine: options.engine - }); - this.sqlTiddlerDatabase.createTables(); -} - -SqlTiddlerStore.prototype.addEventListener = function(type,listener) { - this.eventListeners[type] = this.eventListeners[type] || []; - this.eventListeners[type].push(listener); -}; - -SqlTiddlerStore.prototype.removeEventListener = function(type,listener) { - const listeners = this.eventListeners[type]; - if(listeners) { - var p = listeners.indexOf(listener); - if(p !== -1) { - listeners.splice(p,1); +(function () { + + // /* + // Create a tiddler store. Options include: + + // databasePath - path to the database file (can be ":memory:" to get a temporary database) + // adminWiki - reference to $tw.Wiki object used for configuration + // attachmentStore - reference to associated attachment store + // engine - wasm | better + // */ + + class SqlTiddlerStore { + /** + * @class SqlTiddlerStore + * @param {{ + * databasePath?: String, + * adminWiki?: $TW.Wiki, + * attachmentStore?: import("./attachments").AttachmentStore, + * engine?: String + * }} options + */ + constructor(options) { + options = options || {}; + this.attachmentStore = options.attachmentStore; + this.adminWiki = options.adminWiki || $tw.wiki; + this.eventListeners = {}; // Hashmap by type of array of event listener functions + this.eventOutstanding = {}; // Hashmap by type of boolean true of outstanding events + + // Create the database + this.databasePath = options.databasePath || ":memory:"; + var SqlTiddlerDatabase = require("$:/plugins/tiddlywiki/multiwikiserver/store/sql-tiddler-database.js").SqlTiddlerDatabase; + this.sqlTiddlerDatabase = new SqlTiddlerDatabase({ + databasePath: this.databasePath, + engine: options.engine + }); + const error = new Error("syncCheck"); + this.syncCheck = setTimeout(() => { + console.error(error); + }); } - } -}; - -SqlTiddlerStore.prototype.dispatchEvent = function(type /*, args */) { - const self = this; - if(!this.eventOutstanding[type]) { - $tw.utils.nextTick(function() { - self.eventOutstanding[type] = false; - const args = Array.prototype.slice.call(arguments,1), - listeners = self.eventListeners[type]; + + + async initCheck() { + clearTimeout(this.syncCheck); + this.syncCheck = undefined; + await this.sqlTiddlerDatabase.createTables(); + } + + addEventListener(type, listener) { + this.eventListeners[type] = this.eventListeners[type] || []; + this.eventListeners[type].push(listener); + } + + removeEventListener(type, listener) { + const listeners = this.eventListeners[type]; if(listeners) { - for(var p=0; p 256) { + return "Too long"; + } + // Removed ~ from this list temporarily + if(allowPrivilegedCharacters) { + if(!(/^[^\s\u00A0\x00-\x1F\x7F`!@#%^&*()+={}\[\];\'\"<>,\\\?]+$/g.test(name))) { + return "Invalid character(s)"; + } + } else { + if(!(/^[^\s\u00A0\x00-\x1F\x7F`!@#$%^&*()+={}\[\];:\'\"<>.,\/\\\?]+$/g.test(name))) { + return "Invalid character(s)"; + } + } + return null; + } + + /* + Returns null if the argument is an array of valid bag/recipe names, or a string error message if not + */ + validateItemNames(names, allowPrivilegedCharacters) { + if(!$tw.utils.isArray(names)) { + return "Not a valid array"; + } + var errors = []; + for(const name of names) { + const result = this.validateItemName(name, allowPrivilegedCharacters); + if(result && errors.indexOf(result) === -1) { + errors.push(result); } } + if(errors.length === 0) { + return null; + } else { + return errors.join("\n"); + } + } + + async close() { + await this.sqlTiddlerDatabase.close(); + this.sqlTiddlerDatabase = undefined; + } + + /* + Given tiddler fields, tiddler_id and a bag_name, return the tiddler fields after the following process: + - Apply the tiddler_id as the revision field + - Apply the bag_name as the bag field + */ + processOutgoingTiddler(tiddlerFields, tiddler_id, bag_name, attachment_blob) { + if(attachment_blob !== null) { + return $tw.utils.extend( + {}, + tiddlerFields, + { + text: undefined, + _canonical_uri: `/bags/${$tw.utils.encodeURIComponentExtended(bag_name)}/tiddlers/${$tw.utils.encodeURIComponentExtended(tiddlerFields.title)}/blob` + } + ); + } else { + return tiddlerFields; + } + } + + /* + */ + processIncomingTiddler(tiddlerFields, existing_attachment_blob, existing_canonical_uri) { + let attachmentSizeLimit = $tw.utils.parseNumber(this.adminWiki.getTiddlerText("$:/config/MultiWikiServer/AttachmentSizeLimit")); + if(attachmentSizeLimit < 100 * 1024) { + attachmentSizeLimit = 100 * 1024; + } + const attachmentsEnabled = this.adminWiki.getTiddlerText("$:/config/MultiWikiServer/EnableAttachments", "yes") === "yes"; + const contentTypeInfo = $tw.config.contentTypeInfo[tiddlerFields.type || "text/vnd.tiddlywiki"]; + const isBinary = !!contentTypeInfo && contentTypeInfo.encoding === "base64"; + + let shouldProcessAttachment = tiddlerFields.text && tiddlerFields.text.length > attachmentSizeLimit; + + if(existing_attachment_blob) { + const fileSize = this.attachmentStore.getAttachmentFileSize(existing_attachment_blob); + if(fileSize <= attachmentSizeLimit) { + const existingAttachmentMeta = this.attachmentStore.getAttachmentMetadata(existing_attachment_blob); + const hasCanonicalField = !!tiddlerFields._canonical_uri; + const skipAttachment = hasCanonicalField && (tiddlerFields._canonical_uri === (existingAttachmentMeta ? existingAttachmentMeta._canonical_uri : existing_canonical_uri)); + shouldProcessAttachment = !skipAttachment; + } else { + shouldProcessAttachment = false; + } + } + + if(attachmentsEnabled && isBinary && shouldProcessAttachment) { + const attachment_blob = existing_attachment_blob || this.attachmentStore.saveAttachment({ + text: tiddlerFields.text, + type: tiddlerFields.type, + reference: tiddlerFields.title, + _canonical_uri: tiddlerFields._canonical_uri + }); + + if(tiddlerFields && tiddlerFields._canonical_uri) { + delete tiddlerFields._canonical_uri; + } + + return { + tiddlerFields: Object.assign({}, tiddlerFields, { text: undefined }), + attachment_blob: attachment_blob + }; + } else { + return { + tiddlerFields: tiddlerFields, + attachment_blob: existing_attachment_blob + }; + } + } + + async saveTiddlersFromPath(tiddler_files_path, bag_name) { + var self = this; + await this.sqlTiddlerDatabase.transaction(async function () { + // Clear out the bag + await self.deleteAllTiddlersInBag(bag_name); + // Get the tiddlers + var path = require("path"); + var tiddlersFromPath = $tw.loadTiddlersFromPath(path.resolve($tw.boot.corePath, $tw.config.editionsPath, tiddler_files_path)); + // Save the tiddlers + for(const tiddlersFromFile of tiddlersFromPath) { + for(const tiddler of tiddlersFromFile.tiddlers) { + await self.saveBagTiddler(tiddler, bag_name); + } + } }); - this.eventOutstanding[type] = true; - } -}; - -/* -Returns null if a bag/recipe name is valid, or a string error message if not -*/ -SqlTiddlerStore.prototype.validateItemName = function(name,allowPrivilegedCharacters) { - if(typeof name !== "string") { - return "Not a valid string"; - } - if(name.length > 256) { - return "Too long"; - } - // Removed ~ from this list temporarily - if(allowPrivilegedCharacters) { - if(!(/^[^\s\u00A0\x00-\x1F\x7F`!@#%^&*()+={}\[\];\'\"<>,\\\?]+$/g.test(name))) { - return "Invalid character(s)"; + self.dispatchEvent("change"); } - } else { - if(!(/^[^\s\u00A0\x00-\x1F\x7F`!@#$%^&*()+={}\[\];:\'\"<>.,\/\\\?]+$/g.test(name))) { - return "Invalid character(s)"; + + async listBags() { + return await this.sqlTiddlerDatabase.listBags(); } - } - return null; -}; - -/* -Returns null if the argument is an array of valid bag/recipe names, or a string error message if not -*/ -SqlTiddlerStore.prototype.validateItemNames = function(names,allowPrivilegedCharacters) { - if(!$tw.utils.isArray(names)) { - return "Not a valid array"; - } - var errors = []; - for(const name of names) { - const result = this.validateItemName(name,allowPrivilegedCharacters); - if(result && errors.indexOf(result) === -1) { - errors.push(result); + + /* + Options include: + + allowPrivilegedCharacters - allows "$", ":" and "/" to appear in recipe name + */ + async createBag(bag_name, description, options) { + options = options || {}; + var self = this; + return await this.sqlTiddlerDatabase.transaction(async function () { + const validationBagName = self.validateItemName(bag_name, options.allowPrivilegedCharacters); + if(validationBagName) { + return { message: validationBagName }; + } + await self.sqlTiddlerDatabase.createBag(bag_name, description); + self.dispatchEvent("change"); + return null; + }); } - } - if(errors.length === 0) { - return null; - } else { - return errors.join("\n"); - } -}; - -SqlTiddlerStore.prototype.close = function() { - this.sqlTiddlerDatabase.close(); - this.sqlTiddlerDatabase = undefined; -}; - -/* -Given tiddler fields, tiddler_id and a bag_name, return the tiddler fields after the following process: -- Apply the tiddler_id as the revision field -- Apply the bag_name as the bag field -*/ -SqlTiddlerStore.prototype.processOutgoingTiddler = function(tiddlerFields,tiddler_id,bag_name,attachment_blob) { - if(attachment_blob !== null) { - return $tw.utils.extend( - {}, - tiddlerFields, - { - text: undefined, - _canonical_uri: `/bags/${$tw.utils.encodeURIComponentExtended(bag_name)}/tiddlers/${$tw.utils.encodeURIComponentExtended(tiddlerFields.title)}/blob` + + async listRecipes() { + return await this.sqlTiddlerDatabase.listRecipes(); + } + + /* + Returns null on success, or {message:} on error + + Options include: + + allowPrivilegedCharacters - allows "$", ":" and "/" to appear in recipe name + */ + async createRecipe(recipe_name, bag_names, description, options) { + bag_names = bag_names || []; + description = description || ""; + options = options || {}; + const validationRecipeName = this.validateItemName(recipe_name, options.allowPrivilegedCharacters); + if(validationRecipeName) { + return { message: validationRecipeName }; } - ); - } else { - return tiddlerFields; - } -}; - -/* -*/ -SqlTiddlerStore.prototype.processIncomingTiddler = function(tiddlerFields, existing_attachment_blob, existing_canonical_uri) { - let attachmentSizeLimit = $tw.utils.parseNumber(this.adminWiki.getTiddlerText("$:/config/MultiWikiServer/AttachmentSizeLimit")); - if(attachmentSizeLimit < 100 * 1024) { - attachmentSizeLimit = 100 * 1024; - } - const attachmentsEnabled = this.adminWiki.getTiddlerText("$:/config/MultiWikiServer/EnableAttachments", "yes") === "yes"; - const contentTypeInfo = $tw.config.contentTypeInfo[tiddlerFields.type || "text/vnd.tiddlywiki"]; - const isBinary = !!contentTypeInfo && contentTypeInfo.encoding === "base64"; - - let shouldProcessAttachment = tiddlerFields.text && tiddlerFields.text.length > attachmentSizeLimit; - - if(existing_attachment_blob) { - const fileSize = this.attachmentStore.getAttachmentFileSize(existing_attachment_blob); - if(fileSize <= attachmentSizeLimit) { - const existingAttachmentMeta = this.attachmentStore.getAttachmentMetadata(existing_attachment_blob); - const hasCanonicalField = !!tiddlerFields._canonical_uri; - const skipAttachment = hasCanonicalField && (tiddlerFields._canonical_uri === (existingAttachmentMeta ? existingAttachmentMeta._canonical_uri : existing_canonical_uri)); - shouldProcessAttachment = !skipAttachment; - } else { - shouldProcessAttachment = false; - } - } - - if(attachmentsEnabled && isBinary && shouldProcessAttachment) { - const attachment_blob = existing_attachment_blob || this.attachmentStore.saveAttachment({ - text: tiddlerFields.text, - type: tiddlerFields.type, - reference: tiddlerFields.title, - _canonical_uri: tiddlerFields._canonical_uri - }); - - if(tiddlerFields && tiddlerFields._canonical_uri) { - delete tiddlerFields._canonical_uri; - } - - return { - tiddlerFields: Object.assign({}, tiddlerFields, { text: undefined }), - attachment_blob: attachment_blob - }; - } else { - return { - tiddlerFields: tiddlerFields, - attachment_blob: existing_attachment_blob - }; - } -}; - -SqlTiddlerStore.prototype.saveTiddlersFromPath = function(tiddler_files_path,bag_name) { - var self = this; - this.sqlTiddlerDatabase.transaction(function() { - // Clear out the bag - self.deleteAllTiddlersInBag(bag_name); - // Get the tiddlers - var path = require("path"); - var tiddlersFromPath = $tw.loadTiddlersFromPath(path.resolve($tw.boot.corePath,$tw.config.editionsPath,tiddler_files_path)); - // Save the tiddlers - for(const tiddlersFromFile of tiddlersFromPath) { - for(const tiddler of tiddlersFromFile.tiddlers) { - self.saveBagTiddler(tiddler,bag_name,null); + if(bag_names.length === 0) { + return { message: "Recipes must contain at least one bag" }; + } + var self = this; + return await this.sqlTiddlerDatabase.transaction(async function () { + await self.sqlTiddlerDatabase.createRecipe(recipe_name, bag_names, description); + self.dispatchEvent("change"); + return null; + }); + } + + /* + Returns {tiddler_id:} + */ + async saveBagTiddler(incomingTiddlerFields, bag_name) { + let _canonical_uri; + const existing_attachment_blob = await this.sqlTiddlerDatabase.getBagTiddlerAttachmentBlob(incomingTiddlerFields.title, bag_name); + if(existing_attachment_blob) { + _canonical_uri = `/bags/${$tw.utils.encodeURIComponentExtended(bag_name)}/tiddlers/${$tw.utils.encodeURIComponentExtended(incomingTiddlerFields.title)}/blob`; } + const{ tiddlerFields, attachment_blob } = this.processIncomingTiddler(incomingTiddlerFields, existing_attachment_blob, _canonical_uri); + const result = await this.sqlTiddlerDatabase.saveBagTiddler(tiddlerFields, bag_name, attachment_blob); + this.dispatchEvent("change"); + return result; } - }); - self.dispatchEvent("change"); -}; - -SqlTiddlerStore.prototype.listBags = function() { - return this.sqlTiddlerDatabase.listBags(); -}; - -/* -Options include: - -allowPrivilegedCharacters - allows "$", ":" and "/" to appear in recipe name -*/ -SqlTiddlerStore.prototype.createBag = function(bag_name,description,options) { - options = options || {}; - var self = this; - return this.sqlTiddlerDatabase.transaction(function() { - const validationBagName = self.validateItemName(bag_name,options.allowPrivilegedCharacters); - if(validationBagName) { - return {message: validationBagName}; + + /* + Create a tiddler in a bag adopting the specified file as the attachment. The attachment file must be on the same disk as the attachment store + Options include: + + filepath - filepath to the attachment file + hash - string hash of the attachment file + type - content type of file as uploaded + + Returns {tiddler_id:} + */ + async saveBagTiddlerWithAttachment(incomingTiddlerFields, bag_name, options) { + const attachment_blob = this.attachmentStore.adoptAttachment(options.filepath, options.type, options.hash, options._canonical_uri); + if(attachment_blob) { + const result = await this.sqlTiddlerDatabase.saveBagTiddler(incomingTiddlerFields, bag_name, attachment_blob); + this.dispatchEvent("change"); + return result; + } else { + return null; + } } - self.sqlTiddlerDatabase.createBag(bag_name,description); - self.dispatchEvent("change"); - return null; - }); -}; - -SqlTiddlerStore.prototype.listRecipes = function() { - return this.sqlTiddlerDatabase.listRecipes(); -}; - -/* -Returns null on success, or {message:} on error - -Options include: - -allowPrivilegedCharacters - allows "$", ":" and "/" to appear in recipe name -*/ -SqlTiddlerStore.prototype.createRecipe = function(recipe_name,bag_names,description,options) { - bag_names = bag_names || []; - description = description || ""; - options = options || {}; - const validationRecipeName = this.validateItemName(recipe_name,options.allowPrivilegedCharacters); - if(validationRecipeName) { - return {message: validationRecipeName}; - } - if(bag_names.length === 0) { - return {message: "Recipes must contain at least one bag"}; - } - var self = this; - return this.sqlTiddlerDatabase.transaction(function() { - self.sqlTiddlerDatabase.createRecipe(recipe_name,bag_names,description); - self.dispatchEvent("change"); - return null; - }); -}; - -/* -Returns {tiddler_id:} -*/ -SqlTiddlerStore.prototype.saveBagTiddler = function(incomingTiddlerFields,bag_name) { - let _canonical_uri; - const existing_attachment_blob = this.sqlTiddlerDatabase.getBagTiddlerAttachmentBlob(incomingTiddlerFields.title,bag_name) - if(existing_attachment_blob) { - _canonical_uri = `/bags/${$tw.utils.encodeURIComponentExtended(bag_name)}/tiddlers/${$tw.utils.encodeURIComponentExtended(incomingTiddlerFields.title)}/blob` - } - const {tiddlerFields, attachment_blob} = this.processIncomingTiddler(incomingTiddlerFields,existing_attachment_blob,_canonical_uri); - const result = this.sqlTiddlerDatabase.saveBagTiddler(tiddlerFields,bag_name,attachment_blob); - this.dispatchEvent("change"); - return result; -}; - -/* -Create a tiddler in a bag adopting the specified file as the attachment. The attachment file must be on the same disk as the attachment store -Options include: - -filepath - filepath to the attachment file -hash - string hash of the attachment file -type - content type of file as uploaded - -Returns {tiddler_id:} -*/ -SqlTiddlerStore.prototype.saveBagTiddlerWithAttachment = function(incomingTiddlerFields,bag_name,options) { - const attachment_blob = this.attachmentStore.adoptAttachment(options.filepath,options.type,options.hash,options._canonical_uri); - if(attachment_blob) { - const result = this.sqlTiddlerDatabase.saveBagTiddler(incomingTiddlerFields,bag_name,attachment_blob); - this.dispatchEvent("change"); - return result; - } else { - return null; - } -}; - -/* -Returns {tiddler_id:,bag_name:} -*/ -SqlTiddlerStore.prototype.saveRecipeTiddler = function(incomingTiddlerFields,recipe_name) { - const existing_attachment_blob = this.sqlTiddlerDatabase.getRecipeTiddlerAttachmentBlob(incomingTiddlerFields.title,recipe_name) - const {tiddlerFields, attachment_blob} = this.processIncomingTiddler(incomingTiddlerFields,existing_attachment_blob,incomingTiddlerFields._canonical_uri); - const result = this.sqlTiddlerDatabase.saveRecipeTiddler(tiddlerFields,recipe_name,attachment_blob); - this.dispatchEvent("change"); - return result; -}; - -SqlTiddlerStore.prototype.deleteTiddler = function(title,bag_name) { - const result = this.sqlTiddlerDatabase.deleteTiddler(title,bag_name); - this.dispatchEvent("change"); - return result; -}; - -/* -returns {tiddler_id:,tiddler:} -*/ -SqlTiddlerStore.prototype.getBagTiddler = function(title,bag_name) { - var tiddlerInfo = this.sqlTiddlerDatabase.getBagTiddler(title,bag_name); - if(tiddlerInfo) { - return Object.assign( - {}, - tiddlerInfo, - { - tiddler: this.processOutgoingTiddler(tiddlerInfo.tiddler,tiddlerInfo.tiddler_id,bag_name,tiddlerInfo.attachment_blob) - }); - } else { - return null; - } -}; - -/* -Get an attachment ready to stream. Returns null if there is an error or: -tiddler_id: revision of tiddler -stream: stream of file -type: type of file -Returns {tiddler_id:,bag_name:} -*/ -SqlTiddlerStore.prototype.getBagTiddlerStream = function(title,bag_name) { - const tiddlerInfo = this.sqlTiddlerDatabase.getBagTiddler(title,bag_name); - if(tiddlerInfo) { - if(tiddlerInfo.attachment_blob) { - return $tw.utils.extend( - {}, - this.attachmentStore.getAttachmentStream(tiddlerInfo.attachment_blob), - { - tiddler_id: tiddlerInfo.tiddler_id, - bag_name: bag_name + + /* + Returns {tiddler_id:,bag_name:} + */ + async saveRecipeTiddler(incomingTiddlerFields, recipe_name) { + const existing_attachment_blob = await this.sqlTiddlerDatabase.getRecipeTiddlerAttachmentBlob(incomingTiddlerFields.title, recipe_name); + const{ tiddlerFields, attachment_blob } = await this.processIncomingTiddler(incomingTiddlerFields, existing_attachment_blob, incomingTiddlerFields._canonical_uri); + const result = await this.sqlTiddlerDatabase.saveRecipeTiddler(tiddlerFields, recipe_name, attachment_blob); + this.dispatchEvent("change"); + return result; + } + + async deleteTiddler(title, bag_name) { + const result = await this.sqlTiddlerDatabase.deleteTiddler(title, bag_name); + this.dispatchEvent("change"); + return result; + } + + /* + returns {tiddler_id:,tiddler:} + */ + async getBagTiddler(title, bag_name) { + var tiddlerInfo = await this.sqlTiddlerDatabase.getBagTiddler(title, bag_name); + if(tiddlerInfo) { + return Object.assign( + {}, + tiddlerInfo, + { + tiddler: this.processOutgoingTiddler(tiddlerInfo.tiddler, tiddlerInfo.tiddler_id, bag_name, tiddlerInfo.attachment_blob) + }); + } else { + return null; + } + } + + /* + Get an attachment ready to stream. Returns null if there is an error or: + tiddler_id: revision of tiddler + stream: stream of file + type: type of file + Returns {tiddler_id:,bag_name:} + */ + async getBagTiddlerStream(title, bag_name) { + const tiddlerInfo = await this.sqlTiddlerDatabase.getBagTiddler(title, bag_name); + if(tiddlerInfo) { + if(tiddlerInfo.attachment_blob) { + return $tw.utils.extend( + {}, + this.attachmentStore.getAttachmentStream(tiddlerInfo.attachment_blob), + { + tiddler_id: tiddlerInfo.tiddler_id, + bag_name: bag_name + } + ); + } else { + const{ Readable } = require("stream"); + const stream = new Readable(); + stream._read = function () { + // Push data + const type = tiddlerInfo.tiddler.type || "text/plain"; + stream.push(tiddlerInfo.tiddler.text || "", ($tw.config.contentTypeInfo[type] || { encoding: "utf8" }).encoding); + // Push null to indicate the end of the stream + stream.push(null); + }; + return { + tiddler_id: tiddlerInfo.tiddler_id, + bag_name: bag_name, + stream: stream, + type: tiddlerInfo.tiddler.type || "text/plain" + }; } - ); - } else { - const { Readable } = require('stream'); - const stream = new Readable(); - stream._read = function() { - // Push data - const type = tiddlerInfo.tiddler.type || "text/plain"; - stream.push(tiddlerInfo.tiddler.text || "",($tw.config.contentTypeInfo[type] ||{encoding: "utf8"}).encoding); - // Push null to indicate the end of the stream - stream.push(null); - }; - return { - tiddler_id: tiddlerInfo.tiddler_id, - bag_name: bag_name, - stream: stream, - type: tiddlerInfo.tiddler.type || "text/plain" + } else { + return null; } } - } else { - return null; - } -}; - -/* -Returns {bag_name:, tiddler: {fields}, tiddler_id:} -*/ -SqlTiddlerStore.prototype.getRecipeTiddler = function(title,recipe_name) { - var tiddlerInfo = this.sqlTiddlerDatabase.getRecipeTiddler(title,recipe_name); - if(tiddlerInfo) { - return Object.assign( - {}, - tiddlerInfo, - { - tiddler: this.processOutgoingTiddler(tiddlerInfo.tiddler,tiddlerInfo.tiddler_id,tiddlerInfo.bag_name,tiddlerInfo.attachment_blob) + + /* + Returns {bag_name:, tiddler: {fields}, tiddler_id:} + */ + async getRecipeTiddler(title, recipe_name) { + var tiddlerInfo = await this.sqlTiddlerDatabase.getRecipeTiddler(title, recipe_name); + if(tiddlerInfo) { + return Object.assign( + {}, + tiddlerInfo, + { + tiddler: this.processOutgoingTiddler(tiddlerInfo.tiddler, tiddlerInfo.tiddler_id, tiddlerInfo.bag_name, tiddlerInfo.attachment_blob) + }); + } else { + return null; + } + } + + /* + Get the titles of the tiddlers in a bag. Returns an empty array for bags that do not exist + */ + async getBagTiddlers(bag_name) { + return await this.sqlTiddlerDatabase.getBagTiddlers(bag_name); + } + + /* + Get the tiddler_id of the newest tiddler in a bag. Returns null for bags that do not exist + */ + async getBagLastTiddlerId(bag_name) { + return await this.sqlTiddlerDatabase.getBagLastTiddlerId(bag_name); + } + + /* + Get the titles of the tiddlers in a recipe as {title:,bag_name:}. Returns null for recipes that do not exist + */ + async getRecipeTiddlers(recipe_name, options) { + return await this.sqlTiddlerDatabase.getRecipeTiddlers(recipe_name, options); + } + + /* + Get the tiddler_id of the newest tiddler in a recipe. Returns null for recipes that do not exist + */ + async getRecipeLastTiddlerId(recipe_name) { + return await this.sqlTiddlerDatabase.getRecipeLastTiddlerId(recipe_name); + } + + async deleteAllTiddlersInBag(bag_name) { + var self = this; + return await this.sqlTiddlerDatabase.transaction(async function () { + const result = await self.sqlTiddlerDatabase.deleteAllTiddlersInBag(bag_name); + self.dispatchEvent("change"); + return result; }); - } else { - return null; + } + + /* + Get the names of the bags in a recipe. Returns an empty array for recipes that do not exist + */ + async getRecipeBags(recipe_name) { + return await this.sqlTiddlerDatabase.getRecipeBags(recipe_name); + } } -}; - -/* -Get the titles of the tiddlers in a bag. Returns an empty array for bags that do not exist -*/ -SqlTiddlerStore.prototype.getBagTiddlers = function(bag_name) { - return this.sqlTiddlerDatabase.getBagTiddlers(bag_name); -}; - -/* -Get the tiddler_id of the newest tiddler in a bag. Returns null for bags that do not exist -*/ -SqlTiddlerStore.prototype.getBagLastTiddlerId = function(bag_name) { - return this.sqlTiddlerDatabase.getBagLastTiddlerId(bag_name); -}; - -/* -Get the titles of the tiddlers in a recipe as {title:,bag_name:}. Returns null for recipes that do not exist -*/ -SqlTiddlerStore.prototype.getRecipeTiddlers = function(recipe_name,options) { - return this.sqlTiddlerDatabase.getRecipeTiddlers(recipe_name,options); -}; - -/* -Get the tiddler_id of the newest tiddler in a recipe. Returns null for recipes that do not exist -*/ -SqlTiddlerStore.prototype.getRecipeLastTiddlerId = function(recipe_name) { - return this.sqlTiddlerDatabase.getRecipeLastTiddlerId(recipe_name); -}; - -SqlTiddlerStore.prototype.deleteAllTiddlersInBag = function(bag_name) { - var self = this; - return this.sqlTiddlerDatabase.transaction(function() { - const result = self.sqlTiddlerDatabase.deleteAllTiddlersInBag(bag_name); - self.dispatchEvent("change"); - return result; - }); -}; - -/* -Get the names of the bags in a recipe. Returns an empty array for recipes that do not exist -*/ -SqlTiddlerStore.prototype.getRecipeBags = function(recipe_name) { - return this.sqlTiddlerDatabase.getRecipeBags(recipe_name); -}; - -exports.SqlTiddlerStore = SqlTiddlerStore; + + exports.SqlTiddlerStore = SqlTiddlerStore; })(); \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/tests-sql-tiddler-database.js b/plugins/tiddlywiki/multiwikiserver/modules/store/tests-sql-tiddler-database.js index 563f86bf517..d2da1847061 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/tests-sql-tiddler-database.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/tests-sql-tiddler-database.js @@ -6,6 +6,7 @@ tags: [[$:/tags/test-spec]] Tests the SQL tiddler database layer \*/ +/// if($tw.node) { (function(){ @@ -13,50 +14,50 @@ if($tw.node) { /*global $tw: false */ "use strict"; -describe("SQL tiddler database with node built-in sqlite", function() { - runSqlDatabaseTests("node"); +describe("SQL tiddler database with node built-in sqlite", function () { + void runSqlDatabaseTests("node").catch(console.error); }); -describe("SQL tiddler database with node-sqlite3-wasm", function() { - runSqlDatabaseTests("wasm"); +describe("SQL tiddler database with node-sqlite3-wasm", function () { + void runSqlDatabaseTests("wasm").catch(console.error); }); -describe("SQL tiddler database with better-sqlite3", function() { - runSqlDatabaseTests("better"); +describe("SQL tiddler database with better-sqlite3", function () { + void runSqlDatabaseTests("better").catch(console.error); }); -function runSqlDatabaseTests(engine) { +async function runSqlDatabaseTests(engine) { // Create and initialise the tiddler store var SqlTiddlerDatabase = require("$:/plugins/tiddlywiki/multiwikiserver/store/sql-tiddler-database.js").SqlTiddlerDatabase; const sqlTiddlerDatabase = new SqlTiddlerDatabase({ engine: engine }); - sqlTiddlerDatabase.createTables(); + await sqlTiddlerDatabase.createTables(); // Tear down - afterAll(function() { + afterAll(async function() { // Close the database - sqlTiddlerDatabase.close(); + await sqlTiddlerDatabase.close(); }); // Run tests - it("should save and retrieve tiddlers using engine: " + engine, function() { + it("should save and retrieve tiddlers using engine: " + engine, async function() { // Create bags and recipes expect(sqlTiddlerDatabase.createBag("bag-alpha","Bag alpha")).toEqual(1); expect(sqlTiddlerDatabase.createBag("bag-beta","Bag beta")).toEqual(2); expect(sqlTiddlerDatabase.createBag("bag-gamma","Bag gamma")).toEqual(3); expect(sqlTiddlerDatabase.listBags()).toEqual([ - { bag_name: 'bag-alpha', bag_id: 1, accesscontrol: '', description: "Bag alpha" }, - { bag_name: 'bag-beta', bag_id: 2, accesscontrol: '', description: "Bag beta" }, - { bag_name: 'bag-gamma', bag_id: 3, accesscontrol: '', description: "Bag gamma" } + { bag_name: "bag-alpha", bag_id: 1, accesscontrol: "", description: "Bag alpha" }, + { bag_name: "bag-beta", bag_id: 2, accesscontrol: "", description: "Bag beta" }, + { bag_name: "bag-gamma", bag_id: 3, accesscontrol: "", description: "Bag gamma" } ]); expect(sqlTiddlerDatabase.createRecipe("recipe-rho",["bag-alpha","bag-beta"],"Recipe rho")).toEqual(1); expect(sqlTiddlerDatabase.createRecipe("recipe-sigma",["bag-alpha","bag-gamma"],"Recipe sigma")).toEqual(2); expect(sqlTiddlerDatabase.createRecipe("recipe-tau",["bag-alpha"],"Recipe tau")).toEqual(3); expect(sqlTiddlerDatabase.createRecipe("recipe-upsilon",["bag-alpha","bag-gamma","bag-beta"],"Recipe upsilon")).toEqual(4); expect(sqlTiddlerDatabase.listRecipes()).toEqual([ - { recipe_name: 'recipe-rho', recipe_id: 1, bag_names: ["bag-alpha","bag-beta"], description: "Recipe rho", owner_id: null }, - { recipe_name: 'recipe-sigma', recipe_id: 2, bag_names: ["bag-alpha","bag-gamma"], description: "Recipe sigma", owner_id: null }, - { recipe_name: 'recipe-tau', recipe_id: 3, bag_names: ["bag-alpha"], description: "Recipe tau", owner_id: null }, - { recipe_name: 'recipe-upsilon', recipe_id: 4, bag_names: ["bag-alpha","bag-gamma","bag-beta"], description: "Recipe upsilon", owner_id: null } + { recipe_name: "recipe-rho", recipe_id: 1, bag_names: ["bag-alpha","bag-beta"], description: "Recipe rho", owner_id: null }, + { recipe_name: "recipe-sigma", recipe_id: 2, bag_names: ["bag-alpha","bag-gamma"], description: "Recipe sigma", owner_id: null }, + { recipe_name: "recipe-tau", recipe_id: 3, bag_names: ["bag-alpha"], description: "Recipe tau", owner_id: null }, + { recipe_name: "recipe-upsilon", recipe_id: 4, bag_names: ["bag-alpha","bag-gamma","bag-beta"], description: "Recipe upsilon", owner_id: null } ]); expect(sqlTiddlerDatabase.getRecipeBags("recipe-rho")).toEqual(["bag-alpha","bag-beta"]); expect(sqlTiddlerDatabase.getRecipeBags("recipe-sigma")).toEqual(["bag-alpha","bag-gamma"]); @@ -77,46 +78,46 @@ function runSqlDatabaseTests(engine) { }); // Verify what we've got expect(sqlTiddlerDatabase.getRecipeTiddlers("recipe-rho")).toEqual([ - { title: 'Another Tiddler', tiddler_id: 1, bag_name: 'bag-alpha', is_deleted: 0 }, - { title: 'Hello There', tiddler_id: 3, bag_name: 'bag-beta', is_deleted: 0 } + { title: "Another Tiddler", tiddler_id: 1, bag_name: "bag-alpha", is_deleted: 0 }, + { title: "Hello There", tiddler_id: 3, bag_name: "bag-beta", is_deleted: 0 } ]); expect(sqlTiddlerDatabase.getRecipeTiddlers("recipe-sigma")).toEqual([ - { title: 'Another Tiddler', tiddler_id: 1, bag_name: 'bag-alpha', is_deleted: 0 }, - { title: 'Hello There', tiddler_id: 4, bag_name: 'bag-gamma', is_deleted: 0 } + { title: "Another Tiddler", tiddler_id: 1, bag_name: "bag-alpha", is_deleted: 0 }, + { title: "Hello There", tiddler_id: 4, bag_name: "bag-gamma", is_deleted: 0 } ]); - expect(sqlTiddlerDatabase.getRecipeTiddler("Hello There","recipe-rho").tiddler).toEqual({ title: "Hello There", text: "I'm in beta", tags: "four five six" }); + expect((await sqlTiddlerDatabase.getRecipeTiddler("Hello There","recipe-rho")).tiddler).toEqual({ title: "Hello There", text: "I'm in beta", tags: "four five six" }); expect(sqlTiddlerDatabase.getRecipeTiddler("Missing Tiddler","recipe-rho")).toEqual(null); - expect(sqlTiddlerDatabase.getRecipeTiddler("Another Tiddler","recipe-rho").tiddler).toEqual({ title: "Another Tiddler", text: "I'm in alpha", tags: "one two three" }); - expect(sqlTiddlerDatabase.getRecipeTiddler("Hello There","recipe-sigma").tiddler).toEqual({ title: "Hello There", text: "I'm in gamma", tags: "seven eight nine" }); - expect(sqlTiddlerDatabase.getRecipeTiddler("Another Tiddler","recipe-sigma").tiddler).toEqual({ title: "Another Tiddler", text: "I'm in alpha", tags: "one two three" }); - expect(sqlTiddlerDatabase.getRecipeTiddler("Hello There","recipe-upsilon").tiddler).toEqual({title: "Hello There",text: "I'm in beta",tags: "four five six"}); + expect((await sqlTiddlerDatabase.getRecipeTiddler("Another Tiddler","recipe-rho")).tiddler).toEqual({ title: "Another Tiddler", text: "I'm in alpha", tags: "one two three" }); + expect((await sqlTiddlerDatabase.getRecipeTiddler("Hello There","recipe-sigma")).tiddler).toEqual({ title: "Hello There", text: "I'm in gamma", tags: "seven eight nine" }); + expect((await sqlTiddlerDatabase.getRecipeTiddler("Another Tiddler","recipe-sigma")).tiddler).toEqual({ title: "Another Tiddler", text: "I'm in alpha", tags: "one two three" }); + expect((await sqlTiddlerDatabase.getRecipeTiddler("Hello There","recipe-upsilon")).tiddler).toEqual({title: "Hello There",text: "I'm in beta",tags: "four five six"}); // Delete a tiddlers to ensure the underlying tiddler in the recipe shows through - sqlTiddlerDatabase.deleteTiddler("Hello There","bag-beta"); + await sqlTiddlerDatabase.deleteTiddler("Hello There","bag-beta"); expect(sqlTiddlerDatabase.getRecipeTiddlers("recipe-rho")).toEqual([ - { title: 'Another Tiddler', tiddler_id: 1, bag_name: 'bag-alpha', is_deleted: 0 }, - { title: 'Hello There', tiddler_id: 2, bag_name: 'bag-alpha', is_deleted: 0 } + { title: "Another Tiddler", tiddler_id: 1, bag_name: "bag-alpha", is_deleted: 0 }, + { title: "Hello There", tiddler_id: 2, bag_name: "bag-alpha", is_deleted: 0 } ]); expect(sqlTiddlerDatabase.getRecipeTiddlers("recipe-sigma")).toEqual([ - { title: 'Another Tiddler', tiddler_id: 1, bag_name: 'bag-alpha', is_deleted: 0 }, - { title: 'Hello There', tiddler_id: 4, bag_name: 'bag-gamma', is_deleted: 0 } + { title: "Another Tiddler", tiddler_id: 1, bag_name: "bag-alpha", is_deleted: 0 }, + { title: "Hello There", tiddler_id: 4, bag_name: "bag-gamma", is_deleted: 0 } ]); expect(sqlTiddlerDatabase.getRecipeTiddler("Hello There","recipe-beta")).toEqual(null); - sqlTiddlerDatabase.deleteTiddler("Another Tiddler","bag-alpha"); - expect(sqlTiddlerDatabase.getRecipeTiddlers("recipe-rho")).toEqual([ { title: 'Hello There', tiddler_id: 2, bag_name: 'bag-alpha', is_deleted: 0 } ]); - expect(sqlTiddlerDatabase.getRecipeTiddlers("recipe-sigma")).toEqual([ { title: 'Hello There', tiddler_id: 4, bag_name: 'bag-gamma', is_deleted: 0 } ]); + await sqlTiddlerDatabase.deleteTiddler("Another Tiddler","bag-alpha"); + expect(sqlTiddlerDatabase.getRecipeTiddlers("recipe-rho")).toEqual([ { title: "Hello There", tiddler_id: 2, bag_name: "bag-alpha", is_deleted: 0 } ]); + expect(sqlTiddlerDatabase.getRecipeTiddlers("recipe-sigma")).toEqual([ { title: "Hello There", tiddler_id: 4, bag_name: "bag-gamma", is_deleted: 0 } ]); // Save a recipe tiddler - expect(sqlTiddlerDatabase.saveRecipeTiddler({title: "More", text: "None"},"recipe-rho")).toEqual({tiddler_id: 7, bag_name: 'bag-beta'}); - expect(sqlTiddlerDatabase.getRecipeTiddler("More","recipe-rho").tiddler).toEqual({title: "More", text: "None"}); + expect(sqlTiddlerDatabase.saveRecipeTiddler({title: "More", text: "None"},"recipe-rho")).toEqual({tiddler_id: 7, bag_name: "bag-beta"}); + expect((await sqlTiddlerDatabase.getRecipeTiddler("More","recipe-rho")).tiddler).toEqual({title: "More", text: "None"}); }); - it("should manage users correctly", function() { + it("should manage users correctly", async function() { console.log("should manage users correctly") // Create users const userId1 = sqlTiddlerDatabase.createUser("john_doe", "john@example.com", "pass123"); const userId2 = sqlTiddlerDatabase.createUser("jane_doe", "jane@example.com", "pass123"); // Retrieve users - const user1 = sqlTiddlerDatabase.getUser(userId1); + const user1 = await sqlTiddlerDatabase.getUser(userId1); expect(user1.user_id).toBe(userId1); expect(user1.username).toBe("john_doe"); expect(user1.email).toBe("john@example.com"); @@ -124,22 +125,22 @@ function runSqlDatabaseTests(engine) { expect(user1.last_login).toBeNull(); // Update user - sqlTiddlerDatabase.updateUser(userId1, "john_updated", "john_updated@example.com"); - expect(sqlTiddlerDatabase.getUser(userId1).username).toBe("john_updated"); - expect(sqlTiddlerDatabase.getUser(userId1).email).toBe("john_updated@example.com"); + await sqlTiddlerDatabase.updateUser(userId1, "john_updated", "john_updated@example.com"); + expect((await sqlTiddlerDatabase.getUser(userId1)).username).toBe("john_updated"); + expect((await sqlTiddlerDatabase.getUser(userId1)).email).toBe("john_updated@example.com"); // List users - const users = sqlTiddlerDatabase.listUsers(); + const users = await sqlTiddlerDatabase.listUsers(); expect(users.length).toBe(2); expect(users[0].username).toBe("jane_doe"); expect(users[1].username).toBe("john_updated"); // Delete user - sqlTiddlerDatabase.deleteUser(userId2); + await sqlTiddlerDatabase.deleteUser(userId2); // expect(sqlTiddlerDatabase.getUser(userId2)).toBe(null || undefined); }); - it("should manage groups correctly", function() { + it("should manage groups correctly", async function() { console.log("should manage groups correctly") // Create groups const groupId1 = sqlTiddlerDatabase.createGroup("Editors", "Can edit content"); @@ -153,23 +154,23 @@ function runSqlDatabaseTests(engine) { }); // Update group - sqlTiddlerDatabase.updateGroup(groupId1, "Super Editors", "Can edit all content"); - expect(sqlTiddlerDatabase.getGroup(groupId1).group_name).toBe("Super Editors"); - expect(sqlTiddlerDatabase.getGroup(groupId1).description).toBe("Can edit all content"); + await sqlTiddlerDatabase.updateGroup(groupId1, "Super Editors", "Can edit all content"); + expect((await sqlTiddlerDatabase.getGroup(groupId1)).group_name).toBe("Super Editors"); + expect((await sqlTiddlerDatabase.getGroup(groupId1)).description).toBe("Can edit all content"); // List groups - const groups = sqlTiddlerDatabase.listGroups(); + const groups = await sqlTiddlerDatabase.listGroups(); expect(groups.length).toBe(2); expect(groups[0].group_name).toBe("Super Editors"); expect(groups[1].group_name).toBe("Viewers"); // Delete group - sqlTiddlerDatabase.deleteGroup(groupId2); + await sqlTiddlerDatabase.deleteGroup(groupId2); // expect(sqlTiddlerDatabase.getGroup(groupId2)).toBe(null || undefined); }); - it("should manage roles correctly", function() { + it("should manage roles correctly", async function() { console.log("should manage roles correctly") // Create roles const roleId1 = sqlTiddlerDatabase.createRole("Admin" + Date.now(), "Full access"); @@ -183,22 +184,22 @@ function runSqlDatabaseTests(engine) { }); // Update role - sqlTiddlerDatabase.updateRole(roleId1, "Super Admin" + Date.now(), "God-like powers"); - expect(sqlTiddlerDatabase.getRole(roleId1).role_name).toMatch(/^Super Admin\d+$/); - expect(sqlTiddlerDatabase.getRole(roleId1).description).toBe("God-like powers"); + await sqlTiddlerDatabase.updateRole(roleId1, "Super Admin" + Date.now(), "God-like powers"); + expect((await sqlTiddlerDatabase.getRole(roleId1)).role_name).toMatch(/^Super Admin\d+$/); + expect((await sqlTiddlerDatabase.getRole(roleId1)).description).toBe("God-like powers"); // List roles - const roles = sqlTiddlerDatabase.listRoles(); + const roles = await sqlTiddlerDatabase.listRoles(); expect(roles.length).toBeGreaterThan(0); // expect(roles[0].role_name).toMatch(/^Editor\d+$/); // expect(roles[1].role_name).toMatch(/^Super Admin\d+$/); // Delete role - sqlTiddlerDatabase.deleteRole(roleId2); + await sqlTiddlerDatabase.deleteRole(roleId2); // expect(sqlTiddlerDatabase.getRole(roleId2)).toBeUndefined(); }); - it("should manage permissions correctly", function() { + it("should manage permissions correctly", async function() { console.log("should manage permissions correctly") // Create permissions const permissionId1 = sqlTiddlerDatabase.createPermission("read_tiddlers" + Date.now(), "Can read tiddlers"); @@ -212,18 +213,18 @@ function runSqlDatabaseTests(engine) { }); // Update permission - sqlTiddlerDatabase.updatePermission(permissionId1, "read_all_tiddlers" + Date.now(), "Can read all tiddlers"); - expect(sqlTiddlerDatabase.getPermission(permissionId1).permission_name).toMatch(/^read_all_tiddlers\d+$/); - expect(sqlTiddlerDatabase.getPermission(permissionId1).description).toBe("Can read all tiddlers"); + await sqlTiddlerDatabase.updatePermission(permissionId1, "read_all_tiddlers" + Date.now(), "Can read all tiddlers"); + expect((await sqlTiddlerDatabase.getPermission(permissionId1)).permission_name).toMatch(/^read_all_tiddlers\d+$/); + expect((await sqlTiddlerDatabase.getPermission(permissionId1)).description).toBe("Can read all tiddlers"); // List permissions - const permissions = sqlTiddlerDatabase.listPermissions(); + const permissions = await sqlTiddlerDatabase.listPermissions(); expect(permissions.length).toBeGreaterThan(0); expect(permissions[0].permission_name).toMatch(/^read_all_tiddlers\d+$/); expect(permissions[1].permission_name).toMatch(/^write_tiddlers\d+$/); // Delete permission - sqlTiddlerDatabase.deletePermission(permissionId2); + await sqlTiddlerDatabase.deletePermission(permissionId2); // expect(sqlTiddlerDatabase.getPermission(permissionId2)).toBeUndefined(); }); } diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/tests-sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/store/tests-sql-tiddler-store.js index c5888b2ce16..3f13b5fec25 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/tests-sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/tests-sql-tiddler-store.js @@ -26,11 +26,12 @@ function runSqlStoreTests(engine) { var store; - beforeEach(function() { + beforeEach(async function() { store = new SqlTiddlerStore({ databasePath: ":memory:", engine: engine }); + await store.initCheck(); }); afterEach(function() { @@ -38,14 +39,14 @@ function runSqlStoreTests(engine) { store = null; }); - it("should return empty results without failure on an empty store", function() { - expect(store.listBags()).toEqual([]); - expect(store.listRecipes()).toEqual([]); + it("should return empty results without failure on an empty store", async function() { + expect(await store.listBags()).toEqual([]); + expect(await store.listRecipes()).toEqual([]); }); - it("should return a single bag after creating a bag", function() { - expect(store.createBag("bag-alpha", "Bag alpha")).toEqual(null); - expect(store.listBags()).toEqual([{ + it("should return a single bag after creating a bag", async function() { + expect(await store.createBag("bag-alpha", "Bag alpha")).toEqual(null); + expect(await store.listBags()).toEqual([{ bag_name: "bag-alpha", bag_id: 1, accesscontrol: "", @@ -53,17 +54,17 @@ function runSqlStoreTests(engine) { }]); }); - it("should return empty results after failing to create a bag with an invalid name", function() { - expect(store.createBag("bag alpha", "Bag alpha")).toEqual({ + it("should return empty results after failing to create a bag with an invalid name", async function() { + expect(await store.createBag("bag alpha", "Bag alpha")).toEqual({ message: "Invalid character(s)" }); - expect(store.listBags()).toEqual([]); + expect(await store.listBags()).toEqual([]); }); - it("should return a bag with new description after re-creating", function() { - expect(store.createBag("bag-alpha", "Bag alpha")).toEqual(null); - expect(store.createBag("bag-alpha", "Different description")).toEqual(null); - expect(store.listBags()).toEqual([{ + it("should return a bag with new description after re-creating", async function() { + expect(await store.createBag("bag-alpha", "Bag alpha")).toEqual(null); + expect(await store.createBag("bag-alpha", "Different description")).toEqual(null); + expect(await store.listBags()).toEqual([{ bag_name: "bag-alpha", bag_id: 1, accesscontrol: "", @@ -71,8 +72,8 @@ function runSqlStoreTests(engine) { }]); }); - it("should return a saved tiddler within a bag", function() { - expect(store.createBag("bag-alpha", "Bag alpha")).toEqual(null); + it("should return a saved tiddler within a bag", async function() { + expect(await store.createBag("bag-alpha", "Bag alpha")).toEqual(null); var saveBagResult = store.saveBagTiddler({ title: "Another Tiddler", text: "I'm in alpha", diff --git a/plugins/tiddlywiki/multiwikiserver/modules/tests/test-attachment.js b/plugins/tiddlywiki/multiwikiserver/modules/tests/test-attachment.js index 9298c43f8a3..39d09c12195 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/tests/test-attachment.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/tests/test-attachment.js @@ -6,30 +6,30 @@ tags: [[$:/tags/test-spec]] Tests attachments. \*/ -if(typeof window === 'undefined' && typeof process !== 'undefined' && process.versions && process.versions.node) { +if(typeof window === "undefined" && typeof process !== "undefined" && process.versions && process.versions.node) { (function(){ - var fs = require('fs'); - var path = require('path'); - var assert = require('assert'); - var AttachmentStore = require('$:/plugins/tiddlywiki/multiwikiserver/store/attachments.js').AttachmentStore; - const {Buffer} = require('buffer'); + var fs = require("fs"); + var path = require("path"); + var assert = require("assert"); + var AttachmentStore = require("$:/plugins/tiddlywiki/multiwikiserver/store/attachments.js").AttachmentStore; + const{Buffer} = require("buffer"); - function generateFileWithSize(filePath, sizeInBytes) { - return new Promise((resolve, reject) => { + async function generateFileWithSize(filePath, sizeInBytes) { + return await new Promise((resolve, reject) => { var buffer = Buffer.alloc(sizeInBytes); for(var i = 0; i < sizeInBytes; i++) { buffer[i] = Math.floor(Math.random() * 256); } - fs.writeFile(filePath, buffer, (err) => { + fs.writeFile(filePath, buffer, err => { if(err) { - console.error('Error writing file:', err); + console.error("Error writing file:", err); reject(err); } else { - console.log('File '+filePath+' generated with size '+sizeInBytes+' bytes'); + console.log("File "+filePath+" generated with size "+sizeInBytes+" bytes"); fs.readFile(filePath, (err, data) => { if(err) { - console.error('Error reading file:', err); + console.error("Error reading file:", err); reject(err); } else { resolve(data); @@ -41,10 +41,10 @@ if(typeof window === 'undefined' && typeof process !== 'undefined' && process.ve } (function() { - 'use strict'; + "use strict"; if($tw.node) { - describe('AttachmentStore', function() { - var storePath = './editions/test/test-store'; + describe("AttachmentStore", function() { + var storePath = "./editions/test/test-store"; var attachmentStore = new AttachmentStore({ storePath: storePath }); var originalTimeout; @@ -69,111 +69,111 @@ if(typeof window === 'undefined' && typeof process !== 'undefined' && process.ve }); }); - it('isValidAttachmentName', function() { - expect(attachmentStore.isValidAttachmentName('abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890')).toBe(true); - expect(attachmentStore.isValidAttachmentName('invalid-name')).toBe(false); + it("isValidAttachmentName", function() { + expect(attachmentStore.isValidAttachmentName("abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890")).toBe(true); + expect(attachmentStore.isValidAttachmentName("invalid-name")).toBe(false); }); - it('saveAttachment', function() { + it("saveAttachment", function() { var options = { - text: 'Hello, World!', - type: 'text/plain', - reference: 'test-reference', + text: "Hello, World!", + type: "text/plain", + reference: "test-reference", }; var contentHash = attachmentStore.saveAttachment(options); assert.strictEqual(contentHash.length, 64); - assert.strictEqual(fs.existsSync(path.resolve(storePath, 'files', contentHash)), true); + assert.strictEqual(fs.existsSync(path.resolve(storePath, "files", contentHash)), true); }); - it('adoptAttachment', function() { - var incomingFilepath = path.resolve(storePath, 'incoming-file.txt'); - fs.writeFileSync(incomingFilepath, 'Hello, World!'); - var type = 'text/plain'; - var hash = 'abcdef0123456789abcdef0123456789'; - var _canonical_uri = 'test-canonical-uri'; + it("adoptAttachment", function() { + var incomingFilepath = path.resolve(storePath, "incoming-file.txt"); + fs.writeFileSync(incomingFilepath, "Hello, World!"); + var type = "text/plain"; + var hash = "abcdef0123456789abcdef0123456789"; + var _canonical_uri = "test-canonical-uri"; attachmentStore.adoptAttachment(incomingFilepath, type, hash, _canonical_uri); - expect(fs.existsSync(path.resolve(storePath, 'files', hash))).toBe(true); + expect(fs.existsSync(path.resolve(storePath, "files", hash))).toBe(true); }); - it('getAttachmentStream', function() { + it("getAttachmentStream", function() { var options = { - text: 'Hello, World!', - type: 'text/plain', - filename: 'data.txt', + text: "Hello, World!", + type: "text/plain", + filename: "data.txt", }; var contentHash = attachmentStore.saveAttachment(options); var stream = attachmentStore.getAttachmentStream(contentHash); expect(stream).not.toBeNull(); - expect(stream.type).toBe('text/plain'); + expect(stream.type).toBe("text/plain"); }); - it('getAttachmentFileSize', function() { + it("getAttachmentFileSize", function() { var options = { - text: 'Hello, World!', - type: 'text/plain', - reference: 'test-reference', + text: "Hello, World!", + type: "text/plain", + reference: "test-reference", }; var contentHash = attachmentStore.saveAttachment(options); var fileSize = attachmentStore.getAttachmentFileSize(contentHash); expect(fileSize).toBe(13); }); - it('getAttachmentMetadata', function() { + it("getAttachmentMetadata", function() { var options = { - text: 'Hello, World!', - type: 'text/plain', - filename: 'data.txt', + text: "Hello, World!", + type: "text/plain", + filename: "data.txt", }; var contentHash = attachmentStore.saveAttachment(options); var metadata = attachmentStore.getAttachmentMetadata(contentHash); expect(metadata).not.toBeNull(); - expect(metadata.type).toBe('text/plain'); - expect(metadata.filename).toBe('data.txt'); + expect(metadata.type).toBe("text/plain"); + expect(metadata.filename).toBe("data.txt"); }); - it('saveAttachment large file', async function() { + it("saveAttachment large file", async function() { var sizeInMB = 10 - const file = await generateFileWithSize('./editions/test/test-store/large-file.txt', 1024 * 1024 * sizeInMB) + const file = await generateFileWithSize("./editions/test/test-store/large-file.txt", 1024 * 1024 * sizeInMB) var options = { text: file, - type: 'application/octet-stream', - reference: 'test-reference', + type: "application/octet-stream", + reference: "test-reference", }; var contentHash = attachmentStore.saveAttachment(options); assert.strictEqual(contentHash.length, 64); - assert.strictEqual(fs.existsSync(path.resolve(storePath, 'files', contentHash)), true); + assert.strictEqual(fs.existsSync(path.resolve(storePath, "files", contentHash)), true); }); - it('saveAttachment multiple large files', async function() { + it("saveAttachment multiple large files", async function() { var sizeInMB = 10; var numFiles = 5; - for (var i = 0; i < numFiles; i++) { + for(var i = 0; i < numFiles; i++) { const file = await generateFileWithSize(`./editions/test/test-store/large-file-${i}.txt`, 1024 * 1024 * sizeInMB); var options = { text: file, - type: 'application/octet-stream', + type: "application/octet-stream", reference: `test-reference-${i}`, }; var contentHash = attachmentStore.saveAttachment(options); assert.strictEqual(contentHash.length, 64); - assert.strictEqual(fs.existsSync(path.resolve(storePath, 'files', contentHash)), true); + assert.strictEqual(fs.existsSync(path.resolve(storePath, "files", contentHash)), true); } }); - it('getAttachmentStream multiple large files', async function() { + it("getAttachmentStream multiple large files", async function() { var sizeInMB = 10; var numFiles = 5; - for (var i = 0; i < numFiles; i++) { + for(var i = 0; i < numFiles; i++) { const file = await generateFileWithSize(`./editions/test/test-store/large-file-${i}.txt`, 1024 * 1024 * sizeInMB); var options = { text: file, - type: 'application/octet-stream', + type: "application/octet-stream", reference: `test-reference-${i}`, }; var contentHash = attachmentStore.saveAttachment(options); var stream = attachmentStore.getAttachmentStream(contentHash); assert.notStrictEqual(stream, null); - assert.strictEqual(stream.type, 'application/octet-stream'); + assert.strictEqual(stream.type, "application/octet-stream"); } }); });