From 12d84c43c9bcabb8e3b61e8a4877447eeb863f75 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 2 Jan 2024 14:39:14 +0000 Subject: [PATCH 001/213] Initial Commit --- editions/prerelease/tiddlywiki.info | 3 +- editions/test/tiddlywiki.info | 3 +- .../multiwikiserver/docs/readme.tid | 9 + .../multiwikiserver/modules/init.js | 53 ++++ .../multiwikiserver/modules/route-wiki.js | 60 ++++ .../modules/sql-tiddler-store.js | 263 ++++++++++++++++++ .../modules/tests-sql-tiddler-store.js | 61 ++++ .../tiddlywiki/multiwikiserver/plugin.info | 7 + 8 files changed, 457 insertions(+), 2 deletions(-) create mode 100644 plugins/tiddlywiki/multiwikiserver/docs/readme.tid create mode 100644 plugins/tiddlywiki/multiwikiserver/modules/init.js create mode 100644 plugins/tiddlywiki/multiwikiserver/modules/route-wiki.js create mode 100644 plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js create mode 100644 plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js create mode 100644 plugins/tiddlywiki/multiwikiserver/plugin.info diff --git a/editions/prerelease/tiddlywiki.info b/editions/prerelease/tiddlywiki.info index 168fbb41f67..060e4fb8a6e 100644 --- a/editions/prerelease/tiddlywiki.info +++ b/editions/prerelease/tiddlywiki.info @@ -14,7 +14,8 @@ "tiddlywiki/dynannotate", "tiddlywiki/codemirror", "tiddlywiki/menubar", - "tiddlywiki/jszip" + "tiddlywiki/jszip", + "tiddlywiki/multiwikiserver" ], "themes": [ "tiddlywiki/vanilla", diff --git a/editions/test/tiddlywiki.info b/editions/test/tiddlywiki.info index afb9c0514c3..574d196e17a 100644 --- a/editions/test/tiddlywiki.info +++ b/editions/test/tiddlywiki.info @@ -1,7 +1,8 @@ { "description": "TiddlyWiki core tests", "plugins": [ - "tiddlywiki/jasmine" + "tiddlywiki/jasmine", + "tiddlywiki/multiwikiserver" ], "themes": [ "tiddlywiki/vanilla", diff --git a/plugins/tiddlywiki/multiwikiserver/docs/readme.tid b/plugins/tiddlywiki/multiwikiserver/docs/readme.tid new file mode 100644 index 00000000000..dcc49c06d34 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/docs/readme.tid @@ -0,0 +1,9 @@ +title: $:/plugins/tiddlywiki/multiwikiserver/readme + +This plugin extends the TiddlyWiki 5 server running on Node.js to be able to host multiple wikis, which can share content or be independent. + +Before using the plugin, it is necessary to install dependencies by running the following command in the root of the ~TiddlyWiki5 repository: + +``` +npm install better-sqlite3 +``` diff --git a/plugins/tiddlywiki/multiwikiserver/modules/init.js b/plugins/tiddlywiki/multiwikiserver/modules/init.js new file mode 100644 index 00000000000..99055292481 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/init.js @@ -0,0 +1,53 @@ +/*\ +title: $:/plugins/tiddlywiki/multiwikiserver/init.js +type: application/javascript +module-type: startup + +Multi wiki server initialisation + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +// Export name and synchronous status +exports.name = "multiwikiserver"; +exports.platforms = ["node"]; +exports.before = ["story"]; +exports.synchronous = true; + +exports.startup = function() { + // Install the sqlite3 global namespace + $tw.sqlite3 = { + Database: null + }; + // Check that better-sqlite3 is installed + var logger = new $tw.utils.Logger("multiwikiserver"); + try { + $tw.sqlite3.Database = require("better-sqlite3"); + } catch(e) { + } + if(!$tw.sqlite3.Database) { + logger.alert("The plugin 'tiddlywiki/multiwikiserver' requires the better-sqlite3 npm package to be installed. Run 'npm install better-sqlite3' in the root of the TiddlyWiki repository"); + return; + } + // Create and initialise the tiddler store + var SqlTiddlerStore = require("$:/plugins/tiddlywiki/multiwikiserver/sql-tiddler-store.js").SqlTiddlerStore; + $tw.sqlTiddlerStore = new SqlTiddlerStore({}); + $tw.sqlTiddlerStore.createTables(); + // Create bags and recipes + $tw.sqlTiddlerStore.saveBag("bag-alpha"); + $tw.sqlTiddlerStore.saveBag("bag-beta"); + $tw.sqlTiddlerStore.saveBag("bag-gamma"); + $tw.sqlTiddlerStore.saveRecipe("recipe-rho",["bag-alpha","bag-beta"]); + $tw.sqlTiddlerStore.saveRecipe("recipe-sigma",["bag-alpha","bag-gamma"]); + // Save tiddlers + $tw.sqlTiddlerStore.saveTiddler({title: "Another Tiddler",text: "I'm in alpha",tags: "one two three"},"bag-alpha"); + $tw.sqlTiddlerStore.saveTiddler({title: "Hello There",text: "I'm in alpha as well",tags: "one two three"},"bag-alpha"); + $tw.sqlTiddlerStore.saveTiddler({title: "Hello There",text: "I'm in beta",tags: "four five six"},"bag-beta"); + $tw.sqlTiddlerStore.saveTiddler({title: "Hello There",text: "I'm in gamma",tags: "seven eight nine"},"bag-gamma"); +}; + +})(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-wiki.js b/plugins/tiddlywiki/multiwikiserver/modules/route-wiki.js new file mode 100644 index 00000000000..883b16fcc7c --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-wiki.js @@ -0,0 +1,60 @@ +/*\ +title: $:/plugins/tiddlywiki/multiwikiserver/route-wiki.js +type: application/javascript +module-type: route + +GET /wikis/:recipe_name + +\*/ +(function() { + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +exports.method = "GET"; + +exports.path = /^\/wiki\/(.+)$/; + +exports.handler = function(request,response,state) { + // Get the recipe name from the parameters + var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]); + // Get the tiddlers in the recipe + var titles = $tw.sqlTiddlerStore.getRecipeTiddlers(recipe_name); + // Render the template + var template = $tw.wiki.renderTiddler("text/plain","$:/core/templates/tiddlywiki5.html",{ + variables: { + saveTiddlerFilter: ` + $:/boot/boot.css + $:/boot/boot.js + $:/boot/bootprefix.js + $:/core + $:/library/sjcl.js + $:/themes/tiddlywiki/snowwhite + $:/themes/tiddlywiki/vanilla + ` + } + }); + // Splice in our tiddlers + 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"); + } + var htmlParts = []; + htmlParts.push(template.substring(0,markerPos + marker.length)); + $tw.utils.each(titles,function(title) { + htmlParts.push(JSON.stringify($tw.sqlTiddlerStore.getTiddler(title,recipe_name))); + htmlParts.push(",") + }); + htmlParts.push(template.substring(markerPos + marker.length)) + // Send response + if(htmlParts) { + state.sendResponse(200,{"Content-Type": "text/html"},htmlParts.join("\n"),"utf8"); + } else { + response.writeHead(404); + response.end(); + } +}; + +}()); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js new file mode 100644 index 00000000000..6c1aa59cf0d --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js @@ -0,0 +1,263 @@ +/*\ +title: $:/plugins/tiddlywiki/multiwikiserver/sql-tiddler-store.js +type: application/javascript +module-type: library + +Functions to perform basic tiddler operations with a sqlite3 database + +\*/ + +(function() { + +function SqlTiddlerStore(options) { + // Create our database + this.db = new $tw.sqlite3.Database(":memory:",{verbose: undefined && console.log}); +} + +SqlTiddlerStore.prototype.close = function() { + this.db.close(); + this.db = undefined; +}; + +SqlTiddlerStore.prototype.runStatement = function(sql,params) { + params = params || {}; + const statement = this.db.prepare(sql); + statement.run(params); +}; + +SqlTiddlerStore.prototype.runStatementGet = function(sql,params) { + params = params || {}; + const statement = this.db.prepare(sql); + return statement.get(params); +}; + +SqlTiddlerStore.prototype.runStatementGetAll = function(sql,params) { + params = params || {}; + const statement = this.db.prepare(sql); + return statement.all(params); +}; + +SqlTiddlerStore.prototype.runStatements = function(sqlArray) { + for(const sql of sqlArray) { + this.runStatement(sql); + } +}; + +SqlTiddlerStore.prototype.createTables = function() { + this.runStatements([` + -- Bags have names and access control settings + CREATE TABLE IF NOT EXISTS bags ( + bag_id INTEGER PRIMARY KEY, + bag_name TEXT UNIQUE, + accesscontrol TEXT + ) + `,` + -- Recipes have names... + CREATE TABLE IF NOT EXISTS recipes ( + recipe_id INTEGER PRIMARY KEY, + recipe_name TEXT UNIQUE + ) + `,` + -- ...and recipes also have an ordered list of bags + CREATE TABLE IF NOT EXISTS recipe_bags ( + recipe_id INTEGER, + bag_id INTEGER, + position INTEGER, + FOREIGN KEY (recipe_id) REFERENCES recipes(recipe_id), + FOREIGN KEY (bag_id) REFERENCES bags(bag_id), + UNIQUE (recipe_id, bag_id) + ) + `,` + -- Tiddlers are contained in bags and have titles + CREATE TABLE IF NOT EXISTS tiddlers ( + tiddler_id INTEGER PRIMARY KEY, + bag_id INTEGER, + title TEXT, + FOREIGN KEY (bag_id) REFERENCES bags(bag_id), + 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, + field_name TEXT, + field_value TEXT, + FOREIGN KEY (tiddler_id) REFERENCES tiddlers(tiddler_id), + UNIQUE (tiddler_id, field_name) + ) + `]); +}; + +SqlTiddlerStore.prototype.logTables = function() { + var self = this; + function sqlLogTable(table) { + console.log(`TABLE ${table}:`); + let statement = self.db.prepare(`select * from ${table}`); + for(const row of statement.all()) { + console.log(row); + } + } + const tables = ["recipes","bags","recipe_bags","tiddlers","fields"]; + for(const table of tables) { + sqlLogTable(table); + } +}; + +SqlTiddlerStore.prototype.saveBag = function(bagname) { + // Run the queries + this.runStatement(` + INSERT OR REPLACE INTO bags (bag_name, accesscontrol) VALUES ($bag_name, $accesscontrol) + `,{ + bag_name: bagname, + accesscontrol: "[some access control stuff]" + }); +}; + +SqlTiddlerStore.prototype.saveRecipe = function(recipename,bagnames) { + // Run the queries + this.runStatement(` + -- Insert or replace the recipe with the given name + INSERT OR REPLACE INTO recipes (recipe_name) + VALUES ($recipe_name) + `,{ + recipe_name: recipename + }); + this.runStatement(` + -- Insert bag names into recipe_bags for the given recipe name + INSERT INTO recipe_bags (recipe_id, bag_id, position) + SELECT r.recipe_id, b.bag_id, j.key + FROM ( + SELECT * FROM json_each($bag_names) + ) AS j + JOIN bags AS b ON b.bag_name = j.value + JOIN recipes AS r ON r.recipe_name = $recipe_name; + `,{ + recipe_name: recipename, + bag_names: JSON.stringify(bagnames) + }); +}; + +SqlTiddlerStore.prototype.saveTiddler = function(tiddlerFields,bagname) { + // Run the queries + this.runStatement(` + INSERT OR REPLACE INTO tiddlers (bag_id, title) + VALUES ( + (SELECT bag_id FROM bags WHERE bag_name = $bag_name), + $title + ) + `,{ + title: tiddlerFields.title, + bag_name: bagname + }); + this.runStatement(` + INSERT OR REPLACE INTO fields (tiddler_id, field_name, field_value) + SELECT + t.tiddler_id, + json_each.key AS field_name, + json_each.value AS field_value + FROM ( + SELECT tiddler_id + FROM tiddlers + WHERE bag_id = ( + SELECT bag_id + FROM bags + WHERE bag_name = $bag_name + ) AND title = $title + ) AS t + JOIN json_each($field_values) AS json_each + `,{ + title: tiddlerFields.title, + bag_name: bagname, + field_values: JSON.stringify(Object.assign({},tiddlerFields,{title: undefined})) + }); +}; + +SqlTiddlerStore.prototype.deleteTiddler = function(title,bagname) { + // Run the queries + this.runStatement(` + DELETE FROM fields + WHERE tiddler_id IN ( + SELECT t.tiddler_id + FROM tiddlers AS t + 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: bagname + }); + this.runStatement(` + DELETE FROM tiddlers + WHERE bag_id = ( + SELECT bag_id + FROM bags + WHERE bag_name = $bag_name + ) AND title = $title + `,{ + title: title, + bag_name: bagname + }); +}; + +SqlTiddlerStore.prototype.getTiddler = function(title,recipename) { + const rows = this.runStatementGetAll(` + SELECT field_name, field_value + FROM fields + WHERE tiddler_id = ( + SELECT tt.tiddler_id + FROM ( + SELECT bb.bag_id, t.tiddler_id + FROM ( + SELECT b.bag_id + FROM bags AS b + INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id + INNER JOIN recipes AS r ON rb.recipe_id = r.recipe_id + WHERE r.recipe_name = $recipe_name + ORDER BY rb.position + ) AS bb + INNER JOIN tiddlers AS t ON bb.bag_id = t.bag_id + WHERE t.title = $title + ) AS tt + ORDER BY tt.tiddler_id DESC + LIMIT 1 + ) + `,{ + title: title, + recipe_name: recipename + }); + if(rows.length === 0) { + return null; + } else { + return rows.reduce((accumulator,value) => { + accumulator[value["field_name"]] = value.field_value; + return accumulator; + },{title: title}); + } +}; + +/* +Get the titles of the tiddlers in a recipe. Returns an empty array for recipes that do not exist +*/ +SqlTiddlerStore.prototype.getRecipeTiddlers = function(recipename) { + const rows = this.runStatementGetAll(` + SELECT DISTINCT title + FROM tiddlers + WHERE bag_id IN ( + SELECT bag_id + FROM recipe_bags + WHERE recipe_id = ( + SELECT recipe_id + FROM recipes + WHERE recipe_name = $recipe_name + ) + ) + ORDER BY title ASC + `,{ + recipe_name: recipename + }); + return rows.map(value => value.title); +}; + +exports.SqlTiddlerStore = SqlTiddlerStore; + +})(); \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js new file mode 100644 index 00000000000..f85ab640778 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js @@ -0,0 +1,61 @@ +/*\ +title: tests-sql-tiddler-store.js +type: application/javascript +tags: [[$:/tags/test-spec]] + +Tests the SQL tiddler store + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +if($tw.node) { + +describe("SQL tiddler store", function() { + // Create and initialise the tiddler store + var SqlTiddlerStore = require("$:/plugins/tiddlywiki/multiwikiserver/sql-tiddler-store.js").SqlTiddlerStore; + const sqlTiddlerStore = new SqlTiddlerStore({}); + sqlTiddlerStore.createTables(); + // Create bags and recipes + sqlTiddlerStore.saveBag("bag-alpha"); + sqlTiddlerStore.saveBag("bag-beta"); + sqlTiddlerStore.saveBag("bag-gamma"); + sqlTiddlerStore.saveRecipe("recipe-rho",["bag-alpha","bag-beta"]); + sqlTiddlerStore.saveRecipe("recipe-sigma",["bag-alpha","bag-gamma"]); + // Tear down + afterAll(function() { + // Close the database + sqlTiddlerStore.close(); + }); + // Run tests + it("should save and retrieve tiddlers", function() { + // Save tiddlers + sqlTiddlerStore.saveTiddler({title: "Another Tiddler",text: "I'm in alpha",tags: "one two three"},"bag-alpha"); + sqlTiddlerStore.saveTiddler({title: "Hello There",text: "I'm in alpha as well",tags: "one two three"},"bag-alpha"); + sqlTiddlerStore.saveTiddler({title: "Hello There",text: "I'm in beta",tags: "four five six"},"bag-beta"); + sqlTiddlerStore.saveTiddler({title: "Hello There",text: "I'm in gamma",tags: "seven eight nine"},"bag-gamma"); + // Verify what we've got + expect(sqlTiddlerStore.getRecipeTiddlers("recipe-rho")).toEqual([ "Another Tiddler", "Hello There"]); + expect(sqlTiddlerStore.getRecipeTiddlers("recipe-sigma")).toEqual([ "Another Tiddler", "Hello There"]); + expect(sqlTiddlerStore.getTiddler("Hello There","recipe-rho")).toEqual({ title: "Hello There", text: "I'm in beta", tags: "four five six" }); + expect(sqlTiddlerStore.getTiddler("Missing Tiddler","recipe-rho")).toEqual(null); + expect(sqlTiddlerStore.getTiddler("Another Tiddler","recipe-rho")).toEqual({ title: "Another Tiddler", text: "I'm in alpha", tags: "one two three" }); + expect(sqlTiddlerStore.getTiddler("Hello There","recipe-sigma")).toEqual({ title: "Hello There", text: "I'm in gamma", tags: "seven eight nine" }); + expect(sqlTiddlerStore.getTiddler("Another Tiddler","recipe-sigma")).toEqual({ title: "Another Tiddler", text: "I'm in alpha", tags: "one two three" }); + // Delete a tiddler to ensure the underlying tiddler in the recipe shows through + sqlTiddlerStore.deleteTiddler("Hello There","bag-beta"); + expect(sqlTiddlerStore.getRecipeTiddlers("recipe-rho")).toEqual([ "Another Tiddler", "Hello There"]); + expect(sqlTiddlerStore.getRecipeTiddlers("recipe-sigma")).toEqual([ "Another Tiddler", "Hello There"]); + expect(sqlTiddlerStore.getTiddler("Hello There","recipe-beta")).toEqual(null); + sqlTiddlerStore.deleteTiddler("Another Tiddler","bag-alpha"); + expect(sqlTiddlerStore.getRecipeTiddlers("recipe-rho")).toEqual([ "Hello There"]); + expect(sqlTiddlerStore.getRecipeTiddlers("recipe-sigma")).toEqual([ "Hello There"]); + }); +}); + +} + +})(); diff --git a/plugins/tiddlywiki/multiwikiserver/plugin.info b/plugins/tiddlywiki/multiwikiserver/plugin.info new file mode 100644 index 00000000000..73536e90b8c --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/plugin.info @@ -0,0 +1,7 @@ +{ + "title": "$:/plugins/tiddlywiki/multiwikiserver", + "name": "Multi Wiki Server", + "description": "Multiple Wiki Server Extension", + "list": "readme", + "dependents": [] +} From f8f8319324912b50c3d5bee410ec16e087021a42 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 2 Jan 2024 14:47:32 +0000 Subject: [PATCH 002/213] Add dependencies to package.json This is needed in order for our CI to be able to run the tests --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 0a25b2bfb34..981e1dd9320 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "devDependencies": { "eslint": "^7.32.0" }, - "bundleDependencies": [], "license": "BSD", "engines": { "node": ">=0.8.2" @@ -36,5 +35,8 @@ "test": "node ./tiddlywiki.js ./editions/test --verbose --version --build index", "lint:fix": "eslint . --fix", "lint": "eslint ." + }, + "dependencies": { + "better-sqlite3": "^9.2.2" } } From 993eb5c90d55be536b165fedae49f2467d2f342e Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 2 Jan 2024 21:41:06 +0000 Subject: [PATCH 003/213] Tests need npm install --- bin/ci-test.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bin/ci-test.sh b/bin/ci-test.sh index ffcae66b263..6f25a7378ea 100755 --- a/bin/ci-test.sh +++ b/bin/ci-test.sh @@ -2,6 +2,8 @@ # test TiddlyWiki5 for tiddlywiki.com +npm install + node ./tiddlywiki.js \ ./editions/test \ --verbose \ From f42d3e0536b01dc8eb929b4b6291abffb4fb6d5d Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 2 Jan 2024 21:41:25 +0000 Subject: [PATCH 004/213] Update usage instructions --- plugins/tiddlywiki/multiwikiserver/docs/readme.tid | 2 +- plugins/tiddlywiki/multiwikiserver/modules/init.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/tiddlywiki/multiwikiserver/docs/readme.tid b/plugins/tiddlywiki/multiwikiserver/docs/readme.tid index dcc49c06d34..15298f15af8 100644 --- a/plugins/tiddlywiki/multiwikiserver/docs/readme.tid +++ b/plugins/tiddlywiki/multiwikiserver/docs/readme.tid @@ -5,5 +5,5 @@ This plugin extends the TiddlyWiki 5 server running on Node.js to be able to hos Before using the plugin, it is necessary to install dependencies by running the following command in the root of the ~TiddlyWiki5 repository: ``` -npm install better-sqlite3 +npm install ``` diff --git a/plugins/tiddlywiki/multiwikiserver/modules/init.js b/plugins/tiddlywiki/multiwikiserver/modules/init.js index 99055292481..7dce59ef4bf 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/init.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/init.js @@ -30,7 +30,7 @@ exports.startup = function() { } catch(e) { } if(!$tw.sqlite3.Database) { - logger.alert("The plugin 'tiddlywiki/multiwikiserver' requires the better-sqlite3 npm package to be installed. Run 'npm install better-sqlite3' in the root of the TiddlyWiki repository"); + logger.alert("The plugin 'tiddlywiki/multiwikiserver' requires the better-sqlite3 npm package to be installed. Run 'npm install' in the root of the TiddlyWiki repository"); return; } // Create and initialise the tiddler store From 299781bdba567093a2626a96965c33c7bf9e33f6 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 2 Jan 2024 21:47:08 +0000 Subject: [PATCH 005/213] Update docs --- .../tiddlywiki/multiwikiserver/docs/readme.tid | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/plugins/tiddlywiki/multiwikiserver/docs/readme.tid b/plugins/tiddlywiki/multiwikiserver/docs/readme.tid index 15298f15af8..72fe003fa73 100644 --- a/plugins/tiddlywiki/multiwikiserver/docs/readme.tid +++ b/plugins/tiddlywiki/multiwikiserver/docs/readme.tid @@ -7,3 +7,20 @@ Before using the plugin, it is necessary to install dependencies by running the ``` npm install ``` + +To start the server: + +``` +tiddlywiki editions/prerelease --listen +``` + +Then visit the sample wikis in a browser: + +* http://127.0.0.1:8080/wiki/recipe-rho +* http://127.0.0.1:8080/wiki/recipe-sigma + +To run the tests: + +``` +./bin/test.sh +``` From a9803908706a12e708d10972a324da2f7b9498b9 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Wed, 3 Jan 2024 16:27:13 +0000 Subject: [PATCH 006/213] Implement APIs for client wikis to sync with the server It is now possible to create and edit tiddlers, using the existing tiddlywebadaptor syncing mechanism. There are a lot of hacks and lumpiness to make things compatible, so I think I will end up with an independent implementation --- editions/prerelease/tiddlywiki.info | 3 +- editions/tw5.com/tiddlywiki.info | 5 +- .../modules/route-delete-tiddler.js | 39 +++++++++++++ .../modules/route-get-status.js | 42 ++++++++++++++ .../modules/route-get-tiddler.js | 50 ++++++++++++++++ .../modules/route-get-tiddlers-json.js | 44 ++++++++++++++ .../{route-wiki.js => route-get-wiki.js} | 10 +++- .../modules/route-put-tiddler.js | 58 +++++++++++++++++++ .../modules/sql-tiddler-store.js | 55 ++++++++++++++++-- .../modules/tests-sql-tiddler-store.js | 6 +- .../tiddlywiki/tiddlyweb/tiddlywebadaptor.js | 3 +- 11 files changed, 302 insertions(+), 13 deletions(-) create mode 100644 plugins/tiddlywiki/multiwikiserver/modules/route-delete-tiddler.js create mode 100644 plugins/tiddlywiki/multiwikiserver/modules/route-get-status.js create mode 100644 plugins/tiddlywiki/multiwikiserver/modules/route-get-tiddler.js create mode 100644 plugins/tiddlywiki/multiwikiserver/modules/route-get-tiddlers-json.js rename plugins/tiddlywiki/multiwikiserver/modules/{route-wiki.js => route-get-wiki.js} (77%) create mode 100644 plugins/tiddlywiki/multiwikiserver/modules/route-put-tiddler.js diff --git a/editions/prerelease/tiddlywiki.info b/editions/prerelease/tiddlywiki.info index 060e4fb8a6e..168fbb41f67 100644 --- a/editions/prerelease/tiddlywiki.info +++ b/editions/prerelease/tiddlywiki.info @@ -14,8 +14,7 @@ "tiddlywiki/dynannotate", "tiddlywiki/codemirror", "tiddlywiki/menubar", - "tiddlywiki/jszip", - "tiddlywiki/multiwikiserver" + "tiddlywiki/jszip" ], "themes": [ "tiddlywiki/vanilla", diff --git a/editions/tw5.com/tiddlywiki.info b/editions/tw5.com/tiddlywiki.info index 5ce9a2f1bb1..e2a15866bb7 100644 --- a/editions/tw5.com/tiddlywiki.info +++ b/editions/tw5.com/tiddlywiki.info @@ -7,7 +7,10 @@ "tiddlywiki/evernote", "tiddlywiki/internals", "tiddlywiki/menubar", - "tiddlywiki/qrcode" + "tiddlywiki/qrcode", + "tiddlywiki/tiddlyweb", + "tiddlywiki/filesystem", + "tiddlywiki/multiwikiserver" ], "themes": [ "tiddlywiki/vanilla", diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-delete-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/route-delete-tiddler.js new file mode 100644 index 00000000000..41967630894 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-delete-tiddler.js @@ -0,0 +1,39 @@ +/*\ +title: $:/plugins/tiddlywiki/multiwikiserver/route-delete-tiddler.js +type: application/javascript +module-type: route + +DELETE /wikis/:recipe_name/recipes/:bag_name/tiddler/:title + +NOTE: Urls currently include the recipe name twice. This is temporary to minimise the changes to the TiddlyWeb plugin + +\*/ +(function() { + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +exports.method = "DELETE"; + +exports.path = /^\/wiki\/([^\/]+)\/bags\/([^\/]+)\/tiddlers\/([^\/]+)$/; + +exports.handler = function(request,response,state) { + // Get the parameters + var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]), + bag_name = $tw.utils.decodeURIComponentSafe(state.params[1]), + title = $tw.utils.decodeURIComponentSafe(state.params[2]); + var recipeBags = $tw.sqlTiddlerStore.getRecipeBags(recipe_name); + if(recipeBags.indexOf(bag_name) !== -1) { + $tw.sqlTiddlerStore.deleteTiddler(title,bag_name); + response.writeHead(204, "OK", { + "Content-Type": "text/plain" + }); + response.end(); + } else { + response.writeHead(404); + response.end(); + } +}; + +}()); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-status.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-status.js new file mode 100644 index 00000000000..438f76ec4b7 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-status.js @@ -0,0 +1,42 @@ +/*\ +title: $:/plugins/tiddlywiki/multiwikiserver/route-get-status.js +type: application/javascript +module-type: route + +GET /wikis/:recipe_name/status + +\*/ +(function() { + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +exports.method = "GET"; + +exports.path = /^\/wiki\/([^\/]+)\/status$/; + +exports.handler = function(request,response,state) { + // Get the recipe name from the parameters + var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]); + // Compose the response + var text = JSON.stringify({ + username: "Joe Bloggs", + anonymous: false, + read_only: false, + logout_is_available: false, + space: { + recipe: recipe_name + }, + tiddlywiki_version: $tw.version + }); + // Send response + if(text) { + state.sendResponse(200,{"Content-Type": "application/json"},text,"utf8"); + } else { + response.writeHead(404); + response.end(); + } +}; + +}()); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-tiddler.js new file mode 100644 index 00000000000..7a9fcea91eb --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-tiddler.js @@ -0,0 +1,50 @@ +/*\ +title: $:/plugins/tiddlywiki/multiwikiserver/route-get-tiddler.js +type: application/javascript +module-type: route + +GET /wikis/:recipe_name/recipes/:recipe_name/tiddler/:title + +NOTE: Urls currently include the recipe name twice. This is temporary to minimise the changes to the TiddlyWeb plugin + +\*/ +(function() { + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +exports.method = "GET"; + +exports.path = /^\/wiki\/([^\/]+)\/recipes\/([^\/]+)\/tiddlers\/([^\/]+)$/; + +exports.handler = function(request,response,state) { + // Get the parameters + var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]), + recipe_name_2 = $tw.utils.decodeURIComponentSafe(state.params[1]), + title = $tw.utils.decodeURIComponentSafe(state.params[2]); + if(recipe_name === recipe_name_2) { + var tiddler = $tw.sqlTiddlerStore.getTiddler(title,recipe_name), + tiddlerFields = {}, + knownFields = [ + "bag", "created", "creator", "modified", "modifier", "permissions", "recipe", "revision", "tags", "text", "title", "type", "uri" + ]; + $tw.utils.each(tiddler,function(value,name) { + if(knownFields.indexOf(name) !== -1) { + tiddlerFields[name] = value; + } else { + tiddlerFields.fields = tiddlerFields.fields || {}; + tiddlerFields.fields[name] = value; + } + }); + tiddlerFields.revision = "0"; + tiddlerFields.bag = "bag-gamma"; + tiddlerFields.type = tiddlerFields.type || "text/vnd.tiddlywiki"; + state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify(tiddlerFields),"utf8"); + } else { + response.writeHead(404); + response.end(); + } +}; + +}()); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-tiddlers-json.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-tiddlers-json.js new file mode 100644 index 00000000000..281534f0970 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-tiddlers-json.js @@ -0,0 +1,44 @@ +/*\ +title: $:/plugins/tiddlywiki/multiwikiserver/route-get-tiddlers-json.js +type: application/javascript +module-type: route + +PUT /wikis/:recipe_name/recipes/:recipe_name/tiddlers.json?filter=:filter + +NOTE: Urls currently include the recipe name twice. This is temporary to minimise the changes to the TiddlyWeb plugin + +\*/ +(function() { + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +exports.method = "GET"; + +exports.path = /^\/wiki\/([^\/]+)\/recipes\/([^\/]+)\/tiddlers.json$/; + +exports.handler = function(request,response,state) { + // Get the parameters + var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]), + recipe_name_2 = $tw.utils.decodeURIComponentSafe(state.params[1]); + if(recipe_name === recipe_name_2) { + // Get the tiddlers in the recipe + var titles = $tw.sqlTiddlerStore.getRecipeTiddlers(recipe_name); + // Get a skinny version of each tiddler + var tiddlers = []; + $tw.utils.each(titles,function(title) { + var tiddler = $tw.sqlTiddlerStore.getTiddler(title,recipe_name); + tiddlers.push(Object.assign({},tiddler,{text: undefined, revision: "0", bag: "bag-gamma"})); + }); + var text = JSON.stringify(tiddlers); + state.sendResponse(200,{"Content-Type": "application/json"},text,"utf8"); + return; + } + // Fail if something went wrong + response.writeHead(404); + response.end(); + +}; + +}()); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-wiki.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-wiki.js similarity index 77% rename from plugins/tiddlywiki/multiwikiserver/modules/route-wiki.js rename to plugins/tiddlywiki/multiwikiserver/modules/route-get-wiki.js index 883b16fcc7c..f520e53e8e1 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-wiki.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-wiki.js @@ -1,5 +1,5 @@ /*\ -title: $:/plugins/tiddlywiki/multiwikiserver/route-wiki.js +title: $:/plugins/tiddlywiki/multiwikiserver/route-get-wiki.js type: application/javascript module-type: route @@ -14,7 +14,7 @@ GET /wikis/:recipe_name exports.method = "GET"; -exports.path = /^\/wiki\/(.+)$/; +exports.path = /^\/wiki\/([^\/]+)$/; exports.handler = function(request,response,state) { // Get the recipe name from the parameters @@ -30,6 +30,7 @@ exports.handler = function(request,response,state) { $:/boot/bootprefix.js $:/core $:/library/sjcl.js + $:/plugins/tiddlywiki/tiddlyweb $:/themes/tiddlywiki/snowwhite $:/themes/tiddlywiki/vanilla ` @@ -44,9 +45,12 @@ exports.handler = function(request,response,state) { var htmlParts = []; htmlParts.push(template.substring(0,markerPos + marker.length)); $tw.utils.each(titles,function(title) { - htmlParts.push(JSON.stringify($tw.sqlTiddlerStore.getTiddler(title,recipe_name))); + var tiddler = $tw.sqlTiddlerStore.getTiddler(title,recipe_name); + htmlParts.push(JSON.stringify(Object.assign({},tiddler,{revision: "0", bag: "bag-gamma"}))); htmlParts.push(",") }); + htmlParts.push(JSON.stringify({title: "$:/config/tiddlyweb/host",text: "$protocol$//$host$$pathname$/"})); + htmlParts.push(",") htmlParts.push(template.substring(markerPos + marker.length)) // Send response if(htmlParts) { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-put-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/route-put-tiddler.js new file mode 100644 index 00000000000..e9d9d7ae137 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-put-tiddler.js @@ -0,0 +1,58 @@ +/*\ +title: $:/plugins/tiddlywiki/multiwikiserver/route-put-tiddler.js +type: application/javascript +module-type: route + +PUT /wikis/:recipe_name/recipes/:recipe_name/tiddlers/:title + +NOTE: Urls currently include the recipe name twice. This is temporary to minimise the changes to the TiddlyWeb plugin + +\*/ +(function() { + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +exports.method = "PUT"; + +exports.path = /^\/wiki\/([^\/]+)\/recipes\/([^\/]+)\/tiddlers\/([^\/]+)$/; + +exports.handler = function(request,response,state) { + // Get the parameters + var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]), + recipe_name_2 = $tw.utils.decodeURIComponentSafe(state.params[1]), + title = $tw.utils.decodeURIComponentSafe(state.params[2]), + fields = $tw.utils.parseJSONSafe(state.data); + // Pull up any subfields in the `fields` object + if(typeof fields.fields === "object") { + $tw.utils.each(fields.fields,function(field,name) { + fields[name] = field; + }); + delete fields.fields; + } + // Stringify any array fields + $tw.utils.each(fields,function(value,name) { + if($tw.utils.isArray(value)) { + fields[name] = $tw.utils.stringifyList(value); + } + }); + // Require the recipe names to match + if(recipe_name === recipe_name_2) { + $tw.sqlTiddlerStore.saveRecipeTiddler(fields,recipe_name); + var recipe_bags = $tw.sqlTiddlerStore.getRecipeBags(recipe_name), + top_bag = recipe_bags[recipe_bags.length - 1]; + response.writeHead(204, "OK",{ + Etag: "\"" + top_bag + "/" + encodeURIComponent(title) + "/" + 2222 + ":\"", + "Content-Type": "text/plain" + }); + response.end(); + return; + } + // Fail if something went wrong + response.writeHead(404); + response.end(); + +}; + +}()); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js index 6c1aa59cf0d..de33b6c7109 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js @@ -63,8 +63,8 @@ SqlTiddlerStore.prototype.createTables = function() { recipe_id INTEGER, bag_id INTEGER, position INTEGER, - FOREIGN KEY (recipe_id) REFERENCES recipes(recipe_id), - FOREIGN KEY (bag_id) REFERENCES bags(bag_id), + FOREIGN KEY (recipe_id) REFERENCES recipes(recipe_id) ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (bag_id) REFERENCES bags(bag_id) ON UPDATE CASCADE ON DELETE CASCADE, UNIQUE (recipe_id, bag_id) ) `,` @@ -73,7 +73,7 @@ SqlTiddlerStore.prototype.createTables = function() { tiddler_id INTEGER PRIMARY KEY, bag_id INTEGER, title TEXT, - FOREIGN KEY (bag_id) REFERENCES bags(bag_id), + FOREIGN KEY (bag_id) REFERENCES bags(bag_id) ON UPDATE CASCADE ON DELETE CASCADE, UNIQUE (bag_id, title) ) `,` @@ -82,7 +82,7 @@ SqlTiddlerStore.prototype.createTables = function() { tiddler_id INTEGER, field_name TEXT, field_value TEXT, - FOREIGN KEY (tiddler_id) REFERENCES tiddlers(tiddler_id), + FOREIGN KEY (tiddler_id) REFERENCES tiddlers(tiddler_id) ON UPDATE CASCADE ON DELETE CASCADE, UNIQUE (tiddler_id, field_name) ) `]); @@ -138,7 +138,7 @@ SqlTiddlerStore.prototype.saveRecipe = function(recipename,bagnames) { }; SqlTiddlerStore.prototype.saveTiddler = function(tiddlerFields,bagname) { - // Run the queries + // Update the tiddlers table this.runStatement(` INSERT OR REPLACE INTO tiddlers (bag_id, title) VALUES ( @@ -149,6 +149,7 @@ SqlTiddlerStore.prototype.saveTiddler = function(tiddlerFields,bagname) { title: tiddlerFields.title, bag_name: bagname }); + // Update the fields table this.runStatement(` INSERT OR REPLACE INTO fields (tiddler_id, field_name, field_value) SELECT @@ -172,6 +173,30 @@ SqlTiddlerStore.prototype.saveTiddler = function(tiddlerFields,bagname) { }); }; +SqlTiddlerStore.prototype.saveRecipeTiddler = function(tiddlerFields,recipename) { + // Find the topmost bag in the recipe + var row = this.runStatementGet(` + SELECT b.bag_name + FROM bags AS b + JOIN ( + SELECT rb.bag_id + FROM recipe_bags AS rb + WHERE rb.recipe_id = ( + SELECT recipe_id + FROM recipes + WHERE recipe_name = $recipe_name + ) + ORDER BY rb.position DESC + LIMIT 1 + ) AS selected_bag + ON b.bag_id = selected_bag.bag_id + `,{ + recipe_name: recipename + }); + // Save the tiddler to the topmost bag + this.saveTiddler(tiddlerFields,row.bag_name); +}; + SqlTiddlerStore.prototype.deleteTiddler = function(title,bagname) { // Run the queries this.runStatement(` @@ -258,6 +283,26 @@ SqlTiddlerStore.prototype.getRecipeTiddlers = function(recipename) { return rows.map(value => value.title); }; +/* +Get the names of the bags in a recipe. Returns an empty array for recipes that do not exist +*/ +SqlTiddlerStore.prototype.getRecipeBags = function(recipename) { + const rows = this.runStatementGetAll(` + SELECT bags.bag_name + FROM bags + JOIN ( + SELECT rb.bag_id + FROM recipe_bags AS rb + JOIN recipes AS r ON rb.recipe_id = r.recipe_id + WHERE r.recipe_name = $recipe_name + ORDER BY rb.position + ) AS bag_priority ON bags.bag_id = bag_priority.bag_id + `,{ + recipe_name: recipename + }); + return rows.map(value => value.bag_name); +}; + exports.SqlTiddlerStore = SqlTiddlerStore; })(); \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js index f85ab640778..abb77dba316 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js @@ -45,7 +45,7 @@ describe("SQL tiddler store", function() { expect(sqlTiddlerStore.getTiddler("Another Tiddler","recipe-rho")).toEqual({ title: "Another Tiddler", text: "I'm in alpha", tags: "one two three" }); expect(sqlTiddlerStore.getTiddler("Hello There","recipe-sigma")).toEqual({ title: "Hello There", text: "I'm in gamma", tags: "seven eight nine" }); expect(sqlTiddlerStore.getTiddler("Another Tiddler","recipe-sigma")).toEqual({ title: "Another Tiddler", text: "I'm in alpha", tags: "one two three" }); - // Delete a tiddler to ensure the underlying tiddler in the recipe shows through + // Delete a tiddlers to ensure the underlying tiddler in the recipe shows through sqlTiddlerStore.deleteTiddler("Hello There","bag-beta"); expect(sqlTiddlerStore.getRecipeTiddlers("recipe-rho")).toEqual([ "Another Tiddler", "Hello There"]); expect(sqlTiddlerStore.getRecipeTiddlers("recipe-sigma")).toEqual([ "Another Tiddler", "Hello There"]); @@ -53,6 +53,10 @@ describe("SQL tiddler store", function() { sqlTiddlerStore.deleteTiddler("Another Tiddler","bag-alpha"); expect(sqlTiddlerStore.getRecipeTiddlers("recipe-rho")).toEqual([ "Hello There"]); expect(sqlTiddlerStore.getRecipeTiddlers("recipe-sigma")).toEqual([ "Hello There"]); + // Save a recipe tiddler + sqlTiddlerStore.saveRecipeTiddler({title: "More", text: "None"},"recipe-rho"); + expect(sqlTiddlerStore.getTiddler("More","recipe-rho")).toEqual({title: "More", text: "None"}); + }); }); diff --git a/plugins/tiddlywiki/tiddlyweb/tiddlywebadaptor.js b/plugins/tiddlywiki/tiddlyweb/tiddlywebadaptor.js index 15fbaa4fd20..36758e87389 100644 --- a/plugins/tiddlywiki/tiddlyweb/tiddlywebadaptor.js +++ b/plugins/tiddlywiki/tiddlyweb/tiddlywebadaptor.js @@ -42,7 +42,8 @@ TiddlyWebAdaptor.prototype.getHost = function() { var text = this.wiki.getTiddlerText(CONFIG_HOST_TIDDLER,DEFAULT_HOST_TIDDLER), substitutions = [ {name: "protocol", value: document.location.protocol}, - {name: "host", value: document.location.host} + {name: "host", value: document.location.host}, + {name: "pathname", value: document.location.pathname} ]; for(var t=0; t Date: Wed, 3 Jan 2024 16:47:20 +0000 Subject: [PATCH 007/213] Add new multiwikiserver edition --- editions/multiwikiserver/tiddlywiki.info | 22 +++++++++++++++++++ editions/tw5.com/tiddlywiki.info | 3 +-- .../multiwikiserver/docs/readme.tid | 2 +- 3 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 editions/multiwikiserver/tiddlywiki.info diff --git a/editions/multiwikiserver/tiddlywiki.info b/editions/multiwikiserver/tiddlywiki.info new file mode 100644 index 00000000000..95538232953 --- /dev/null +++ b/editions/multiwikiserver/tiddlywiki.info @@ -0,0 +1,22 @@ +{ + "description": "Multiple wiki client-server edition", + "plugins": [ + "tiddlywiki/tiddlyweb", + "tiddlywiki/filesystem", + "tiddlywiki/highlight", + "tiddlywiki/multiwikiserver" + ], + "themes": [ + "tiddlywiki/vanilla", + "tiddlywiki/snowwhite" + ], + "build": { + "index": [ + "--render","$:/plugins/tiddlywiki/tiddlyweb/save/offline","index.html","text/plain"], + "static": [ + "--render","$:/core/templates/static.template.html","static.html","text/plain", + "--render","$:/core/templates/alltiddlers.template.html","alltiddlers.html","text/plain", + "--render","[!is[system]]","[encodeuricomponent[]addprefix[static/]addsuffix[.html]]","text/plain","$:/core/templates/static.tiddler.html", + "--render","$:/core/templates/static.template.css","static/static.css","text/plain"] + } +} \ No newline at end of file diff --git a/editions/tw5.com/tiddlywiki.info b/editions/tw5.com/tiddlywiki.info index e2a15866bb7..14e1bdd2294 100644 --- a/editions/tw5.com/tiddlywiki.info +++ b/editions/tw5.com/tiddlywiki.info @@ -9,8 +9,7 @@ "tiddlywiki/menubar", "tiddlywiki/qrcode", "tiddlywiki/tiddlyweb", - "tiddlywiki/filesystem", - "tiddlywiki/multiwikiserver" + "tiddlywiki/filesystem" ], "themes": [ "tiddlywiki/vanilla", diff --git a/plugins/tiddlywiki/multiwikiserver/docs/readme.tid b/plugins/tiddlywiki/multiwikiserver/docs/readme.tid index 72fe003fa73..4a027016c28 100644 --- a/plugins/tiddlywiki/multiwikiserver/docs/readme.tid +++ b/plugins/tiddlywiki/multiwikiserver/docs/readme.tid @@ -11,7 +11,7 @@ npm install To start the server: ``` -tiddlywiki editions/prerelease --listen +tiddlywiki editions/multiwikiserver --listen ``` Then visit the sample wikis in a browser: From 68a89b615de240ef23d17127bd4433b7c3ae5ba6 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Fri, 5 Jan 2024 10:58:07 +0000 Subject: [PATCH 008/213] Use a persistent disk-based database --- .../multiwikiserver/modules/init.js | 18 ++++--- .../modules/sql-tiddler-store.js | 50 ++++++++++++++----- .../modules/tests-sql-tiddler-store.js | 11 ++-- 3 files changed, 55 insertions(+), 24 deletions(-) diff --git a/plugins/tiddlywiki/multiwikiserver/modules/init.js b/plugins/tiddlywiki/multiwikiserver/modules/init.js index 7dce59ef4bf..8b0624ecec8 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/init.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/init.js @@ -19,6 +19,7 @@ exports.before = ["story"]; exports.synchronous = true; exports.startup = function() { + var path = require("path"); // Install the sqlite3 global namespace $tw.sqlite3 = { Database: null @@ -33,16 +34,21 @@ exports.startup = function() { logger.alert("The plugin 'tiddlywiki/multiwikiserver' requires the better-sqlite3 npm package to be installed. Run 'npm install' in the root of the TiddlyWiki repository"); return; } + // Compute the database path + var databasePath = path.resolve($tw.boot.wikiPath,"database.sqlite"); // Create and initialise the tiddler store var SqlTiddlerStore = require("$:/plugins/tiddlywiki/multiwikiserver/sql-tiddler-store.js").SqlTiddlerStore; - $tw.sqlTiddlerStore = new SqlTiddlerStore({}); + $tw.sqlTiddlerStore = new SqlTiddlerStore({ + databasePath: databasePath + }); $tw.sqlTiddlerStore.createTables(); // Create bags and recipes - $tw.sqlTiddlerStore.saveBag("bag-alpha"); - $tw.sqlTiddlerStore.saveBag("bag-beta"); - $tw.sqlTiddlerStore.saveBag("bag-gamma"); - $tw.sqlTiddlerStore.saveRecipe("recipe-rho",["bag-alpha","bag-beta"]); - $tw.sqlTiddlerStore.saveRecipe("recipe-sigma",["bag-alpha","bag-gamma"]); + $tw.sqlTiddlerStore.createBag("bag-alpha"); + $tw.sqlTiddlerStore.createBag("bag-beta"); + $tw.sqlTiddlerStore.createBag("bag-gamma"); + $tw.sqlTiddlerStore.createRecipe("recipe-rho",["bag-alpha","bag-beta"]); + $tw.sqlTiddlerStore.createRecipe("recipe-sigma",["bag-alpha","bag-gamma"]); + $tw.sqlTiddlerStore.createRecipe("recipe-tau",["bag-alpha"]); // Save tiddlers $tw.sqlTiddlerStore.saveTiddler({title: "Another Tiddler",text: "I'm in alpha",tags: "one two three"},"bag-alpha"); $tw.sqlTiddlerStore.saveTiddler({title: "Hello There",text: "I'm in alpha as well",tags: "one two three"},"bag-alpha"); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js index de33b6c7109..f6f6dc10f34 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js @@ -9,9 +9,16 @@ Functions to perform basic tiddler operations with a sqlite3 database (function() { +/* +Create a tiddler store. Options include: + +databasePath - path to the database file (can be ":memory:" to get a temporary database) +*/ function SqlTiddlerStore(options) { + options = options || {}; + var databasePath = options.databasePath || ":memory:"; // Create our database - this.db = new $tw.sqlite3.Database(":memory:",{verbose: undefined && console.log}); + this.db = new $tw.sqlite3.Database(databasePath,{verbose: undefined && console.log}); } SqlTiddlerStore.prototype.close = function() { @@ -103,34 +110,51 @@ SqlTiddlerStore.prototype.logTables = function() { } }; -SqlTiddlerStore.prototype.saveBag = function(bagname) { +SqlTiddlerStore.prototype.createBag = function(bagname) { // Run the queries this.runStatement(` - INSERT OR REPLACE INTO bags (bag_name, accesscontrol) VALUES ($bag_name, $accesscontrol) + INSERT OR IGNORE INTO bags (bag_name, accesscontrol) + VALUES ($bag_name, '') + `,{ + bag_name: bagname + }); + this.runStatement(` + UPDATE bags + SET accesscontrol = $accesscontrol + WHERE bag_name = $bag_name `,{ bag_name: bagname, accesscontrol: "[some access control stuff]" }); }; -SqlTiddlerStore.prototype.saveRecipe = function(recipename,bagnames) { +SqlTiddlerStore.prototype.createRecipe = function(recipename,bagnames) { // Run the queries this.runStatement(` - -- Insert or replace the recipe with the given name - INSERT OR REPLACE INTO recipes (recipe_name) + -- Create the entry in the recipes table if required + INSERT OR IGNORE INTO recipes (recipe_name) VALUES ($recipe_name) `,{ recipe_name: recipename }); this.runStatement(` - -- Insert bag names into recipe_bags for the given recipe name + -- 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: recipename + }); + console.log(this.runStatementGetAll(` + SELECT * FROM json_each($bag_names) AS bag + `,{ + bag_names: JSON.stringify(bagnames) + })); + this.runStatement(` INSERT INTO recipe_bags (recipe_id, bag_id, position) - SELECT r.recipe_id, b.bag_id, j.key - FROM ( - SELECT * FROM json_each($bag_names) - ) AS j - JOIN bags AS b ON b.bag_name = j.value - JOIN recipes AS r ON r.recipe_name = $recipe_name; + SELECT r.recipe_id, b.bag_id, j.key as position + FROM recipes r + JOIN bags b + LEFT JOIN json_each($bag_names) AS j ON j.value = b.bag_name + WHERE r.recipe_name = $recipe_name `,{ recipe_name: recipename, bag_names: JSON.stringify(bagnames) diff --git a/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js index abb77dba316..ed630a9a53b 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js @@ -20,11 +20,12 @@ describe("SQL tiddler store", function() { const sqlTiddlerStore = new SqlTiddlerStore({}); sqlTiddlerStore.createTables(); // Create bags and recipes - sqlTiddlerStore.saveBag("bag-alpha"); - sqlTiddlerStore.saveBag("bag-beta"); - sqlTiddlerStore.saveBag("bag-gamma"); - sqlTiddlerStore.saveRecipe("recipe-rho",["bag-alpha","bag-beta"]); - sqlTiddlerStore.saveRecipe("recipe-sigma",["bag-alpha","bag-gamma"]); + sqlTiddlerStore.createBag("bag-alpha"); + sqlTiddlerStore.createBag("bag-beta"); + sqlTiddlerStore.createBag("bag-gamma"); + sqlTiddlerStore.createRecipe("recipe-rho",["bag-gamma"]); + sqlTiddlerStore.createRecipe("recipe-rho",["bag-alpha","bag-beta"]); + sqlTiddlerStore.createRecipe("recipe-sigma",["bag-alpha","bag-gamma"]); // Tear down afterAll(function() { // Close the database From 8543dda4aac27633ce7deb30d0235f5c4ac2cc33 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Fri, 5 Jan 2024 11:01:10 +0000 Subject: [PATCH 009/213] Fix broken test --- .../multiwikiserver/modules/tests-sql-tiddler-store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js index ed630a9a53b..6a9a0d88183 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js @@ -41,7 +41,7 @@ describe("SQL tiddler store", function() { // Verify what we've got expect(sqlTiddlerStore.getRecipeTiddlers("recipe-rho")).toEqual([ "Another Tiddler", "Hello There"]); expect(sqlTiddlerStore.getRecipeTiddlers("recipe-sigma")).toEqual([ "Another Tiddler", "Hello There"]); - expect(sqlTiddlerStore.getTiddler("Hello There","recipe-rho")).toEqual({ title: "Hello There", text: "I'm in beta", tags: "four five six" }); + expect(sqlTiddlerStore.getTiddler("Hello There","recipe-rho")).toEqual({ title: "Hello There", text: "I'm in gamma", tags: "seven eight nine" }); expect(sqlTiddlerStore.getTiddler("Missing Tiddler","recipe-rho")).toEqual(null); expect(sqlTiddlerStore.getTiddler("Another Tiddler","recipe-rho")).toEqual({ title: "Another Tiddler", text: "I'm in alpha", tags: "one two three" }); expect(sqlTiddlerStore.getTiddler("Hello There","recipe-sigma")).toEqual({ title: "Hello There", text: "I'm in gamma", tags: "seven eight nine" }); From 3f1f7c7ef70d5fa3d38191f0d437f0fc3593744d Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Fri, 5 Jan 2024 11:08:33 +0000 Subject: [PATCH 010/213] Remove debugging code --- .../tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js index f6f6dc10f34..0d61e1064fa 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js @@ -143,11 +143,6 @@ SqlTiddlerStore.prototype.createRecipe = function(recipename,bagnames) { `,{ recipe_name: recipename }); - console.log(this.runStatementGetAll(` - SELECT * FROM json_each($bag_names) AS bag - `,{ - bag_names: JSON.stringify(bagnames) - })); this.runStatement(` INSERT INTO recipe_bags (recipe_id, bag_id, position) SELECT r.recipe_id, b.bag_id, j.key as position From 1eed61397b609521607a19d703e1ff451cb17840 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Fri, 5 Jan 2024 15:37:48 +0000 Subject: [PATCH 011/213] Fix create recipe SQL bug --- plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js index 0d61e1064fa..9fb886c7d1e 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js @@ -148,7 +148,7 @@ SqlTiddlerStore.prototype.createRecipe = function(recipename,bagnames) { SELECT r.recipe_id, b.bag_id, j.key as position FROM recipes r JOIN bags b - LEFT JOIN json_each($bag_names) AS j ON j.value = b.bag_name + INNER JOIN json_each($bag_names) AS j ON j.value = b.bag_name WHERE r.recipe_name = $recipe_name `,{ recipe_name: recipename, From 0799177cf4469724b225dba0579fe38e5484fcfc Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Fri, 5 Jan 2024 15:40:39 +0000 Subject: [PATCH 012/213] Add another recipe, improve docs --- plugins/tiddlywiki/multiwikiserver/docs/readme.tid | 10 ++++++++-- plugins/tiddlywiki/multiwikiserver/modules/init.js | 8 ++++---- .../multiwikiserver/modules/tests-sql-tiddler-store.js | 3 ++- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/plugins/tiddlywiki/multiwikiserver/docs/readme.tid b/plugins/tiddlywiki/multiwikiserver/docs/readme.tid index 4a027016c28..21d5799ce41 100644 --- a/plugins/tiddlywiki/multiwikiserver/docs/readme.tid +++ b/plugins/tiddlywiki/multiwikiserver/docs/readme.tid @@ -16,8 +16,14 @@ tiddlywiki editions/multiwikiserver --listen Then visit the sample wikis in a browser: -* http://127.0.0.1:8080/wiki/recipe-rho -* http://127.0.0.1:8080/wiki/recipe-sigma +* http://127.0.0.1:8080/wiki/recipe-rho - bag-alpha, bag-beta +* http://127.0.0.1:8080/wiki/recipe-sigma - bag-alpha, bag-gamma +* http://127.0.0.1:8080/wiki/recipe-tau - bag-alpha +* http://127.0.0.1:8080/wiki/recipe-upsilon - bag-alpha, bag-gamma, bag-beta + +Note that changes are written to the topmost bag in a recipe. + +Note that until syncing is improved it is necessary to use "Get latest changes from the server" to speed up propogation of changes. To run the tests: diff --git a/plugins/tiddlywiki/multiwikiserver/modules/init.js b/plugins/tiddlywiki/multiwikiserver/modules/init.js index 8b0624ecec8..2a9acd7cb0a 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/init.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/init.js @@ -49,11 +49,11 @@ exports.startup = function() { $tw.sqlTiddlerStore.createRecipe("recipe-rho",["bag-alpha","bag-beta"]); $tw.sqlTiddlerStore.createRecipe("recipe-sigma",["bag-alpha","bag-gamma"]); $tw.sqlTiddlerStore.createRecipe("recipe-tau",["bag-alpha"]); + $tw.sqlTiddlerStore.createRecipe("recipe-upsilon",["bag-alpha","bag-gamma","bag-beta"]); // Save tiddlers - $tw.sqlTiddlerStore.saveTiddler({title: "Another Tiddler",text: "I'm in alpha",tags: "one two three"},"bag-alpha"); - $tw.sqlTiddlerStore.saveTiddler({title: "Hello There",text: "I'm in alpha as well",tags: "one two three"},"bag-alpha"); - $tw.sqlTiddlerStore.saveTiddler({title: "Hello There",text: "I'm in beta",tags: "four five six"},"bag-beta"); - $tw.sqlTiddlerStore.saveTiddler({title: "Hello There",text: "I'm in gamma",tags: "seven eight nine"},"bag-gamma"); + $tw.sqlTiddlerStore.saveTiddler({title: "$:/SiteTitle",text: "Bag Alpha"},"bag-alpha"); + $tw.sqlTiddlerStore.saveTiddler({title: "$:/SiteTitle",text: "Bag Beta"},"bag-beta"); + $tw.sqlTiddlerStore.saveTiddler({title: "$:/SiteTitle",text: "Bag Gamma"},"bag-gamma"); }; })(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js index 6a9a0d88183..570ad455650 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js @@ -23,9 +23,10 @@ describe("SQL tiddler store", function() { sqlTiddlerStore.createBag("bag-alpha"); sqlTiddlerStore.createBag("bag-beta"); sqlTiddlerStore.createBag("bag-gamma"); - sqlTiddlerStore.createRecipe("recipe-rho",["bag-gamma"]); sqlTiddlerStore.createRecipe("recipe-rho",["bag-alpha","bag-beta"]); sqlTiddlerStore.createRecipe("recipe-sigma",["bag-alpha","bag-gamma"]); + sqlTiddlerStore.createRecipe("recipe-tau",["bag-alpha"]); + sqlTiddlerStore.createRecipe("recipe-upsilon",["bag-alpha","bag-gamma","bag-beta"]); // Tear down afterAll(function() { // Close the database From 1fb8b2e2795e6d087dcb4a77677b29ffcb34fda7 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Fri, 5 Jan 2024 15:45:40 +0000 Subject: [PATCH 013/213] Fix broken test --- .../multiwikiserver/modules/tests-sql-tiddler-store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js index 570ad455650..d41613af6da 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js @@ -42,7 +42,7 @@ describe("SQL tiddler store", function() { // Verify what we've got expect(sqlTiddlerStore.getRecipeTiddlers("recipe-rho")).toEqual([ "Another Tiddler", "Hello There"]); expect(sqlTiddlerStore.getRecipeTiddlers("recipe-sigma")).toEqual([ "Another Tiddler", "Hello There"]); - expect(sqlTiddlerStore.getTiddler("Hello There","recipe-rho")).toEqual({ title: "Hello There", text: "I'm in gamma", tags: "seven eight nine" }); + expect(sqlTiddlerStore.getTiddler("Hello There","recipe-rho")).toEqual({ title: "Hello There", text: "I'm in beta", tags: "four five six" }); expect(sqlTiddlerStore.getTiddler("Missing Tiddler","recipe-rho")).toEqual(null); expect(sqlTiddlerStore.getTiddler("Another Tiddler","recipe-rho")).toEqual({ title: "Another Tiddler", text: "I'm in alpha", tags: "one two three" }); expect(sqlTiddlerStore.getTiddler("Hello There","recipe-sigma")).toEqual({ title: "Hello There", text: "I'm in gamma", tags: "seven eight nine" }); From 615dc0c4a3fedd29c8877a80220f08cd302110dd Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Wed, 17 Jan 2024 22:41:41 +0000 Subject: [PATCH 014/213] First pass at admin user interface --- .../multiwikiserver/admin-ui/AdminLayout.tid | 16 ++++++++ .../admin-ui/DefaultTiddlers.tid | 2 + .../MultiWikiServer Administration.tid | 22 +++++++++++ .../admin-ui/SideBarSegment.tid | 10 +++++ .../multiwikiserver/admin-ui/Styles.tid | 5 +++ .../multiwikiserver/admin-ui/layout.tid | 2 + .../modules/sql-tiddler-store.js | 38 ++++++++++++++++++- 7 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 plugins/tiddlywiki/multiwikiserver/admin-ui/AdminLayout.tid create mode 100644 plugins/tiddlywiki/multiwikiserver/admin-ui/DefaultTiddlers.tid create mode 100644 plugins/tiddlywiki/multiwikiserver/admin-ui/MultiWikiServer Administration.tid create mode 100644 plugins/tiddlywiki/multiwikiserver/admin-ui/SideBarSegment.tid create mode 100644 plugins/tiddlywiki/multiwikiserver/admin-ui/Styles.tid create mode 100644 plugins/tiddlywiki/multiwikiserver/admin-ui/layout.tid diff --git a/plugins/tiddlywiki/multiwikiserver/admin-ui/AdminLayout.tid b/plugins/tiddlywiki/multiwikiserver/admin-ui/AdminLayout.tid new file mode 100644 index 00000000000..ff1c2c19f4e --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/admin-ui/AdminLayout.tid @@ -0,0 +1,16 @@ +title: $:/MultiWikiServer/AdminLayout +tags: $:/tags/Layout +name: MultiWikiServer +description: Admin Layout +icon: $:/favicon.ico + +\import [subfilter{$:/core/config/GlobalImportFilter}] +
+{{MultiWikiServer Administration}} +
+<$button> +<$action-setfield $tiddler="$:/layout" text="$:/core/ui/PageTemplate"/> +Switch to TiddlyWiki default user interface + +
+
diff --git a/plugins/tiddlywiki/multiwikiserver/admin-ui/DefaultTiddlers.tid b/plugins/tiddlywiki/multiwikiserver/admin-ui/DefaultTiddlers.tid new file mode 100644 index 00000000000..2438953264e --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/admin-ui/DefaultTiddlers.tid @@ -0,0 +1,2 @@ +title: $:/DefaultTiddlers +text: [[MultiWikiServer Administration]] diff --git a/plugins/tiddlywiki/multiwikiserver/admin-ui/MultiWikiServer Administration.tid b/plugins/tiddlywiki/multiwikiserver/admin-ui/MultiWikiServer Administration.tid new file mode 100644 index 00000000000..03f55e88317 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/admin-ui/MultiWikiServer Administration.tid @@ -0,0 +1,22 @@ +title: MultiWikiServer Administration + +
+

Recipes

+ +

Bags

+
    +<$list filter="[prefix[$:/state/multiwikiserver/bags/]]"> +
  • +<$text text={{!!bag-name}}/> +
  • + +
+
\ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/admin-ui/SideBarSegment.tid b/plugins/tiddlywiki/multiwikiserver/admin-ui/SideBarSegment.tid new file mode 100644 index 00000000000..ec677b034a0 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/admin-ui/SideBarSegment.tid @@ -0,0 +1,10 @@ +title: $:/MultiWikiServer/SideBarSegment +tags: $:/tags/SideBarSegment +list-before: $:/core/ui/SideBarSegments/page-controls + +
+<$button> +<$action-setfield $tiddler="$:/layout" text="$:/MultiWikiServer/AdminLayout"/> +Switch back to ~MultiWikiServer administration user interface + +
diff --git a/plugins/tiddlywiki/multiwikiserver/admin-ui/Styles.tid b/plugins/tiddlywiki/multiwikiserver/admin-ui/Styles.tid new file mode 100644 index 00000000000..07ac4d1ed39 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/admin-ui/Styles.tid @@ -0,0 +1,5 @@ +title: $:/MultiWikiServer/Styles +tags: $:/tags/Stylesheet + +\rules only filteredtranscludeinline transcludeinline macrodef macrocallinline macrocallblock + diff --git a/plugins/tiddlywiki/multiwikiserver/admin-ui/layout.tid b/plugins/tiddlywiki/multiwikiserver/admin-ui/layout.tid new file mode 100644 index 00000000000..72a28d2e449 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/admin-ui/layout.tid @@ -0,0 +1,2 @@ +title: $:/layout +text: $:/MultiWikiServer/AdminLayout diff --git a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js index 9fb886c7d1e..4274c369050 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js @@ -13,11 +13,14 @@ Functions to perform basic tiddler operations with a sqlite3 database 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 into which entity state tiddlers should be saved */ function SqlTiddlerStore(options) { options = options || {}; + this.adminWiki = options.adminWiki || $tw.wiki; + this.entityStateTiddlerPrefix = "$:/state/multiwikiserver/"; + // Create the database var databasePath = options.databasePath || ":memory:"; - // Create our database this.db = new $tw.sqlite3.Database(databasePath,{verbose: undefined && console.log}); } @@ -50,6 +53,10 @@ SqlTiddlerStore.prototype.runStatements = function(sqlArray) { } }; +SqlTiddlerStore.prototype.saveEntityStateTiddler = function(tiddler) { + this.adminWiki.addTiddler(new $tw.Tiddler(tiddler,{title: this.entityStateTiddlerPrefix + tiddler.title})); +}; + SqlTiddlerStore.prototype.createTables = function() { this.runStatements([` -- Bags have names and access control settings @@ -110,6 +117,15 @@ SqlTiddlerStore.prototype.logTables = function() { } }; +SqlTiddlerStore.prototype.listBags = function() { + const rows = this.runStatementGetAll(` + SELECT bag_name, accesscontrol + FROM bags + ORDER BY bag_name + `); + return rows; +}; + SqlTiddlerStore.prototype.createBag = function(bagname) { // Run the queries this.runStatement(` @@ -126,6 +142,20 @@ SqlTiddlerStore.prototype.createBag = function(bagname) { bag_name: bagname, accesscontrol: "[some access control stuff]" }); + this.saveEntityStateTiddler({ + title: "bags/" + bagname, + "bag-name": bagname, + text: "" + }); +}; + +SqlTiddlerStore.prototype.listRecipes = function() { + const rows = this.runStatementGetAll(` + SELECT recipe_name + FROM recipes + ORDER BY recipe_name + `); + return rows; }; SqlTiddlerStore.prototype.createRecipe = function(recipename,bagnames) { @@ -154,6 +184,12 @@ SqlTiddlerStore.prototype.createRecipe = function(recipename,bagnames) { recipe_name: recipename, bag_names: JSON.stringify(bagnames) }); + this.saveEntityStateTiddler({ + title: "recipes/" + recipename, + "recipe-name": recipename, + text: "", + list: $tw.utils.stringifyList(bagnames) + }); }; SqlTiddlerStore.prototype.saveTiddler = function(tiddlerFields,bagname) { From 8941bd1747de6d842085d001092b4c588a81cdc4 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Wed, 17 Jan 2024 22:42:01 +0000 Subject: [PATCH 015/213] Server extension framework May not actually be needed --- core/modules/server/server.js | 16 ++++++++++++ .../modules/server-extension.js | 25 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 plugins/tiddlywiki/multiwikiserver/modules/server-extension.js diff --git a/core/modules/server/server.js b/core/modules/server/server.js index 258ddfa3179..65fc6d47550 100644 --- a/core/modules/server/server.js +++ b/core/modules/server/server.js @@ -43,6 +43,14 @@ function Server(options) { } } } + // Register server extensions + this.extensions = []; + $tw.modules.forEachModuleOfType("server-extension",function(title,exports) { + var extension = new exports.Extension(self); + self.extensions.push(extension); + }); + // Initialise server extensions + this.invokeExtensionHook("server-start-initialisation"); // Setup the default required plugins this.requiredPlugins = this.get("required-plugins").split(','); // Initialise CSRF @@ -96,8 +104,16 @@ function Server(options) { this.servername = $tw.utils.transliterateToSafeASCII(this.get("server-name") || this.wiki.getTiddlerText("$:/SiteTitle") || "TiddlyWiki5"); this.boot.origin = this.get("origin")? this.get("origin"): this.protocol+"://"+this.get("host")+":"+this.get("port"); this.boot.pathPrefix = this.get("path-prefix") || ""; + // Complete initialisation of server extensions + this.invokeExtensionHook("server-completed-initialisation"); } +Server.prototype.invokeExtensionHook = function(hookName) { + $tw.utils.each(this.extensions,function(extension) { + extension.hook(hookName); + }); +}; + /* Send a response to the client. This method checks if the response must be sent or if the client alrady has the data cached. If that's the case only a 304 diff --git a/plugins/tiddlywiki/multiwikiserver/modules/server-extension.js b/plugins/tiddlywiki/multiwikiserver/modules/server-extension.js new file mode 100644 index 00000000000..41be69f0062 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/server-extension.js @@ -0,0 +1,25 @@ +/*\ +title: $:/plugins/tiddlywiki/multiwikiserver/server-extension.js +type: application/javascript +module-type: server-extension + +Multi wiki server extension for the core server object + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +function Extension(server) { + this.server = server; +} + +Extension.prototype.hook = function(name) { + +}; + +exports.Extension = Extension; + +})(); From 50d0b1412d4ba9d102fed0b4e11d35b73aba678b Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Thu, 18 Jan 2024 09:02:41 +0000 Subject: [PATCH 016/213] Fix CI tests --- plugins/tiddlywiki/multiwikiserver/admin-ui/AdminLayout.tid | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/tiddlywiki/multiwikiserver/admin-ui/AdminLayout.tid b/plugins/tiddlywiki/multiwikiserver/admin-ui/AdminLayout.tid index ff1c2c19f4e..5b36335c99b 100644 --- a/plugins/tiddlywiki/multiwikiserver/admin-ui/AdminLayout.tid +++ b/plugins/tiddlywiki/multiwikiserver/admin-ui/AdminLayout.tid @@ -6,6 +6,8 @@ icon: $:/favicon.ico \import [subfilter{$:/core/config/GlobalImportFilter}]
+ +
TiddlyWiki5
{{MultiWikiServer Administration}}
<$button> From 2f09c32d2dab511ad389a6fe667b0d6cbeb0c512 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Thu, 18 Jan 2024 21:47:57 +0000 Subject: [PATCH 017/213] Fix getTiddler query --- .../modules/sql-tiddler-store.js | 24 +++++++------------ .../modules/tests-sql-tiddler-store.js | 1 + 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js index 4274c369050..7794d23fc30 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js @@ -284,22 +284,14 @@ SqlTiddlerStore.prototype.getTiddler = function(title,recipename) { SELECT field_name, field_value FROM fields WHERE tiddler_id = ( - SELECT tt.tiddler_id - FROM ( - SELECT bb.bag_id, t.tiddler_id - FROM ( - SELECT b.bag_id - FROM bags AS b - INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id - INNER JOIN recipes AS r ON rb.recipe_id = r.recipe_id - WHERE r.recipe_name = $recipe_name - ORDER BY rb.position - ) AS bb - INNER JOIN tiddlers AS t ON bb.bag_id = t.bag_id - WHERE t.title = $title - ) AS tt - ORDER BY tt.tiddler_id DESC - LIMIT 1 + SELECT t.tiddler_id + FROM bags AS b + INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id + INNER JOIN recipes AS r ON rb.recipe_id = r.recipe_id + INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id + WHERE r.recipe_name = $recipe_name + AND t.title = $title + ORDER BY rb.position DESC ) `,{ title: title, diff --git a/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js index d41613af6da..3262c4ccdfb 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js @@ -47,6 +47,7 @@ describe("SQL tiddler store", function() { expect(sqlTiddlerStore.getTiddler("Another Tiddler","recipe-rho")).toEqual({ title: "Another Tiddler", text: "I'm in alpha", tags: "one two three" }); expect(sqlTiddlerStore.getTiddler("Hello There","recipe-sigma")).toEqual({ title: "Hello There", text: "I'm in gamma", tags: "seven eight nine" }); expect(sqlTiddlerStore.getTiddler("Another Tiddler","recipe-sigma")).toEqual({ title: "Another Tiddler", text: "I'm in alpha", tags: "one two three" }); + expect(sqlTiddlerStore.getTiddler("Hello There","recipe-upsilon")).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 sqlTiddlerStore.deleteTiddler("Hello There","bag-beta"); expect(sqlTiddlerStore.getRecipeTiddlers("recipe-rho")).toEqual([ "Another Tiddler", "Hello There"]); From 82fae45656ba0877ec01f96f0c20cd543a0c565c Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Thu, 18 Jan 2024 21:48:09 +0000 Subject: [PATCH 018/213] Admin styling --- .../admin-ui/MultiWikiServer Administration.tid | 10 ++++++++++ plugins/tiddlywiki/multiwikiserver/admin-ui/Styles.tid | 7 +++++++ 2 files changed, 17 insertions(+) diff --git a/plugins/tiddlywiki/multiwikiserver/admin-ui/MultiWikiServer Administration.tid b/plugins/tiddlywiki/multiwikiserver/admin-ui/MultiWikiServer Administration.tid index 03f55e88317..c36aa86b065 100644 --- a/plugins/tiddlywiki/multiwikiserver/admin-ui/MultiWikiServer Administration.tid +++ b/plugins/tiddlywiki/multiwikiserver/admin-ui/MultiWikiServer Administration.tid @@ -8,9 +8,19 @@ title: MultiWikiServer Administration <$text text={{!!recipe-name}}/> +
    +<$list filter="[list]"> +
  1. +<$text text=<>/> +
  2. + +
+
+Higher numbered bags take priority if a tiddler with the same title is in more than one bag +

Bags

    <$list filter="[prefix[$:/state/multiwikiserver/bags/]]"> diff --git a/plugins/tiddlywiki/multiwikiserver/admin-ui/Styles.tid b/plugins/tiddlywiki/multiwikiserver/admin-ui/Styles.tid index 07ac4d1ed39..5b62f657c7b 100644 --- a/plugins/tiddlywiki/multiwikiserver/admin-ui/Styles.tid +++ b/plugins/tiddlywiki/multiwikiserver/admin-ui/Styles.tid @@ -3,3 +3,10 @@ tags: $:/tags/Stylesheet \rules only filteredtranscludeinline transcludeinline macrodef macrocallinline macrocallblock +/* +Styles specific to the full screen layout +*/ + +.mws-admin-layout { + padding: 1rem; +} From 4f37355a9fad1d559b1de89e7d88c09eea59d175 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Fri, 19 Jan 2024 10:28:04 +0000 Subject: [PATCH 019/213] Tests should use a dummy admin wiki --- .../multiwikiserver/modules/tests-sql-tiddler-store.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js index 3262c4ccdfb..f6724cf30b9 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js @@ -17,7 +17,9 @@ if($tw.node) { describe("SQL tiddler store", function() { // Create and initialise the tiddler store var SqlTiddlerStore = require("$:/plugins/tiddlywiki/multiwikiserver/sql-tiddler-store.js").SqlTiddlerStore; - const sqlTiddlerStore = new SqlTiddlerStore({}); + const sqlTiddlerStore = new SqlTiddlerStore({ + adminWiki: new $tw.Wiki() + }); sqlTiddlerStore.createTables(); // Create bags and recipes sqlTiddlerStore.createBag("bag-alpha"); From 4133e7d6d6e1f0ac582c7d604993c587abba0a3d Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Fri, 19 Jan 2024 10:52:12 +0000 Subject: [PATCH 020/213] Stream wiki generation Avoids "string too long" errors when working with big tiddlers (>100MB) --- .../multiwikiserver/modules/route-get-wiki.js | 74 ++++++++++--------- 1 file changed, 39 insertions(+), 35 deletions(-) diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-wiki.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-wiki.js index f520e53e8e1..17170d078d4 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-get-wiki.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-wiki.js @@ -19,42 +19,46 @@ exports.path = /^\/wiki\/([^\/]+)$/; exports.handler = function(request,response,state) { // Get the recipe name from the parameters var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]); - // Get the tiddlers in the recipe - var titles = $tw.sqlTiddlerStore.getRecipeTiddlers(recipe_name); - // Render the template - var template = $tw.wiki.renderTiddler("text/plain","$:/core/templates/tiddlywiki5.html",{ - variables: { - saveTiddlerFilter: ` - $:/boot/boot.css - $:/boot/boot.js - $:/boot/bootprefix.js - $:/core - $:/library/sjcl.js - $:/plugins/tiddlywiki/tiddlyweb - $:/themes/tiddlywiki/snowwhite - $:/themes/tiddlywiki/vanilla - ` + // Check request is valid + if(recipe_name) { + // Start the response + response.writeHead(200, "OK",{ + "Content-Type": "text/html" + }); + // Get the tiddlers in the recipe + var titles = $tw.sqlTiddlerStore.getRecipeTiddlers(recipe_name); + // Render the template + var template = $tw.wiki.renderTiddler("text/plain","$:/core/templates/tiddlywiki5.html",{ + variables: { + saveTiddlerFilter: ` + $:/boot/boot.css + $:/boot/boot.js + $:/boot/bootprefix.js + $:/core + $:/library/sjcl.js + $:/plugins/tiddlywiki/tiddlyweb + $:/themes/tiddlywiki/snowwhite + $:/themes/tiddlywiki/vanilla + ` + } + }); + // Splice in our tiddlers + 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"); } - }); - // Splice in our tiddlers - 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"); - } - var htmlParts = []; - htmlParts.push(template.substring(0,markerPos + marker.length)); - $tw.utils.each(titles,function(title) { - var tiddler = $tw.sqlTiddlerStore.getTiddler(title,recipe_name); - htmlParts.push(JSON.stringify(Object.assign({},tiddler,{revision: "0", bag: "bag-gamma"}))); - htmlParts.push(",") - }); - htmlParts.push(JSON.stringify({title: "$:/config/tiddlyweb/host",text: "$protocol$//$host$$pathname$/"})); - htmlParts.push(",") - htmlParts.push(template.substring(markerPos + marker.length)) - // Send response - if(htmlParts) { - state.sendResponse(200,{"Content-Type": "text/html"},htmlParts.join("\n"),"utf8"); + response.write(template.substring(0,markerPos + marker.length)); + $tw.utils.each(titles,function(title) { + var tiddler = $tw.sqlTiddlerStore.getTiddler(title,recipe_name); + response.write(JSON.stringify(Object.assign({},tiddler,{revision: "0", bag: "bag-gamma"}))); + response.write(",") + }); + response.write(JSON.stringify({title: "$:/config/tiddlyweb/host",text: "$protocol$//$host$$pathname$/"})); + response.write(",") + response.write(template.substring(markerPos + marker.length)) + // Finish response + response.end(); } else { response.writeHead(404); response.end(); From 9767e7d3b71350928cd64dad2cfeab5f57cefe7f Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Fri, 19 Jan 2024 11:03:27 +0000 Subject: [PATCH 021/213] Update entity state tiddlers on startup to read bag and recipe info --- .../multiwikiserver/modules/init.js | 1 + .../modules/sql-tiddler-store.js | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/plugins/tiddlywiki/multiwikiserver/modules/init.js b/plugins/tiddlywiki/multiwikiserver/modules/init.js index 2a9acd7cb0a..3750a6a15c3 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/init.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/init.js @@ -42,6 +42,7 @@ exports.startup = function() { databasePath: databasePath }); $tw.sqlTiddlerStore.createTables(); + $tw.sqlTiddlerStore.updateAdminWiki(); // Create bags and recipes $tw.sqlTiddlerStore.createBag("bag-alpha"); $tw.sqlTiddlerStore.createBag("bag-beta"); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js index 7794d23fc30..d24a8212c80 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js @@ -57,6 +57,26 @@ SqlTiddlerStore.prototype.saveEntityStateTiddler = function(tiddler) { this.adminWiki.addTiddler(new $tw.Tiddler(tiddler,{title: this.entityStateTiddlerPrefix + tiddler.title})); }; +SqlTiddlerStore.prototype.updateAdminWiki = function() { + // Update bags + for(const bagInfo of this.listBags()) { + this.saveEntityStateTiddler({ + title: "bags/" + bagInfo.bag_name, + "bag-name": bagInfo.bag_name, + text: "" + }); + } + // Update recipes + for(const recipeInfo of this.listRecipes()) { + this.saveEntityStateTiddler({ + title: "recipes/" + recipeInfo.recipe_name, + "recipe-name": recipeInfo.recipe_name, + text: "", + list: $tw.utils.stringifyList(this.getRecipeBags(recipeInfo.recipe_name)) + }); + } +}; + SqlTiddlerStore.prototype.createTables = function() { this.runStatements([` -- Bags have names and access control settings From 4b0df1a7ae7f99f4c83eafc2feee16b80f3a4b2e Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Fri, 19 Jan 2024 11:03:58 +0000 Subject: [PATCH 022/213] Basic support for creating bags and recipes Cannot yet specify the bags for the new recipe --- .../MultiWikiServer Administration.tid | 52 +++++++++++++++++++ .../multiwikiserver/modules/route-put-bag.js | 36 +++++++++++++ .../modules/route-put-recipe.js | 36 +++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 plugins/tiddlywiki/multiwikiserver/modules/route-put-bag.js create mode 100644 plugins/tiddlywiki/multiwikiserver/modules/route-put-recipe.js diff --git a/plugins/tiddlywiki/multiwikiserver/admin-ui/MultiWikiServer Administration.tid b/plugins/tiddlywiki/multiwikiserver/admin-ui/MultiWikiServer Administration.tid index c36aa86b065..4521d1fc655 100644 --- a/plugins/tiddlywiki/multiwikiserver/admin-ui/MultiWikiServer Administration.tid +++ b/plugins/tiddlywiki/multiwikiserver/admin-ui/MultiWikiServer Administration.tid @@ -1,5 +1,51 @@ title: MultiWikiServer Administration +\procedure createBag(name) + +\procedure completion-createBag() +\import [subfilter{$:/core/config/GlobalImportFilter}] + <$action-log msg="In completion-createBag"/> + <$action-log/> +\end completion-createBag + +<$action-sendmessage + $message="tm-http-request" + url=`/wiki/$(name)$/bags/$(name)$` + method="PUT" + oncompletion=<> +/> +\end createBag + +\procedure createBagButton(name) +<$button class=""> +<$transclude $variable="createBag" name={{$:/state/NewBagName}}/> +{{$:/core/images/new-button}} +<$text text="Create a new bag:"/><$edit-text tiddler="$:/state/NewBagName" tag="input"/> +\end createBagButton + +\procedure createRecipe(name) + +\procedure completion-createRecipe() +\import [subfilter{$:/core/config/GlobalImportFilter}] + <$action-log msg="In completion-createRecipe"/> + <$action-log/> +\end completion-createRecipe + +<$action-sendmessage + $message="tm-http-request" + url=`/wiki/$(name)$/recipes/$(name)$` + method="PUT" + oncompletion=<> +/> +\end createRecipe + +\procedure createRecipeButton(name) +<$button class=""> +<$transclude $variable="createRecipe" name={{$:/state/NewRecipeName}}/> +{{$:/core/images/new-button}} +<$text text="Create a new recipe:"/><$edit-text tiddler="$:/state/NewRecipeName" tag="input"/> +\end createRecipeButton +

    Recipes

      @@ -19,6 +65,9 @@ title: MultiWikiServer Administration
    +<> +
    +
    Higher numbered bags take priority if a tiddler with the same title is in more than one bag

    Bags

    @@ -29,4 +78,7 @@ Higher numbered bags take priority if a tiddler with the same title is in more t
+
+<> +
\ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-put-bag.js b/plugins/tiddlywiki/multiwikiserver/modules/route-put-bag.js new file mode 100644 index 00000000000..7a4ad94c58b --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-put-bag.js @@ -0,0 +1,36 @@ +/*\ +title: $:/plugins/tiddlywiki/multiwikiserver/route-put-bag.js +type: application/javascript +module-type: route + +PUT /wikis/:bag_name/bags/:bag_name + +NOTE: Urls currently include the bag name twice. This is temporary to minimise the changes to the TiddlyWeb plugin + +\*/ +(function() { + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +exports.method = "PUT"; + +exports.path = /^\/wiki\/([^\/]+)\/bags\/([^\/]+)$/; + +exports.handler = function(request,response,state) { + // Get the parameters + var bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]), + bag_name_2 = $tw.utils.decodeURIComponentSafe(state.params[1]); + if(bag_name === bag_name_2) { + $tw.sqlTiddlerStore.createBag(bag_name); + state.sendResponse(204,{ + "Content-Type": "text/plain" + }); + } else { + response.writeHead(404); + response.end(); + } +}; + +}()); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-put-recipe.js b/plugins/tiddlywiki/multiwikiserver/modules/route-put-recipe.js new file mode 100644 index 00000000000..7793bcf5b6b --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-put-recipe.js @@ -0,0 +1,36 @@ +/*\ +title: $:/plugins/tiddlywiki/multiwikiserver/route-put-recipe.js +type: application/javascript +module-type: route + +PUT /wikis/:recipe_name/recipes/:recipe_name + +NOTE: Urls currently include the recipe name twice. This is temporary to minimise the changes to the TiddlyWeb plugin + +\*/ +(function() { + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +exports.method = "PUT"; + +exports.path = /^\/wiki\/([^\/]+)\/recipes\/([^\/]+)$/; + +exports.handler = function(request,response,state) { + // Get the parameters + var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]), + recipe_name_2 = $tw.utils.decodeURIComponentSafe(state.params[1]); + if(recipe_name === recipe_name_2) { + $tw.sqlTiddlerStore.createRecipe(recipe_name); + state.sendResponse(204,{ + "Content-Type": "text/plain" + }); + } else { + response.writeHead(404); + response.end(); + } +}; + +}()); From 26ede2839b90c7f2d841a09f5d21b461c9d5eee4 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Fri, 19 Jan 2024 14:46:21 +0000 Subject: [PATCH 023/213] Add support for _canonical_uri tiddlers Currently hard wired to kick in for tiddlers over 10MB (in base64 representation for binary tiddlers) --- .../modules/route-get-tiddler.js | 49 ++++++++++++------- .../multiwikiserver/modules/route-get-wiki.js | 14 +++++- 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-tiddler.js index 7a9fcea91eb..b45884caa7a 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-get-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-tiddler.js @@ -22,25 +22,36 @@ exports.handler = function(request,response,state) { // Get the parameters var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]), recipe_name_2 = $tw.utils.decodeURIComponentSafe(state.params[1]), - title = $tw.utils.decodeURIComponentSafe(state.params[2]); - if(recipe_name === recipe_name_2) { - var tiddler = $tw.sqlTiddlerStore.getTiddler(title,recipe_name), - tiddlerFields = {}, - knownFields = [ - "bag", "created", "creator", "modified", "modifier", "permissions", "recipe", "revision", "tags", "text", "title", "type", "uri" - ]; - $tw.utils.each(tiddler,function(value,name) { - if(knownFields.indexOf(name) !== -1) { - tiddlerFields[name] = value; - } else { - tiddlerFields.fields = tiddlerFields.fields || {}; - tiddlerFields.fields[name] = value; - } - }); - tiddlerFields.revision = "0"; - tiddlerFields.bag = "bag-gamma"; - tiddlerFields.type = tiddlerFields.type || "text/vnd.tiddlywiki"; - state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify(tiddlerFields),"utf8"); + title = $tw.utils.decodeURIComponentSafe(state.params[2]), + tiddler = recipe_name === recipe_name_2 && $tw.sqlTiddlerStore.getTiddler(title,recipe_name); + if(recipe_name === recipe_name_2 && 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) { + var tiddlerFields = {}, + knownFields = [ + "bag", "created", "creator", "modified", "modifier", "permissions", "recipe", "revision", "tags", "text", "title", "type", "uri" + ]; + $tw.utils.each(tiddler,function(value,name) { + if(knownFields.indexOf(name) !== -1) { + tiddlerFields[name] = value; + } else { + tiddlerFields.fields = tiddlerFields.fields || {}; + tiddlerFields.fields[name] = value; + } + }); + tiddlerFields.revision = "0"; + tiddlerFields.bag = "bag-gamma"; + tiddlerFields.type = tiddlerFields.type || "text/vnd.tiddlywiki"; + state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify(tiddlerFields),"utf8"); + } else { + // This is not a JSON API request, we should return the raw tiddler content + var type = tiddler.type || "text/plain"; + response.writeHead(200, "OK",{ + "Content-Type": type + }); + response.write(tiddler.text || "",($tw.config.contentTypeInfo[type] ||{encoding: "utf8"}).encoding); + response.end();; + } } else { response.writeHead(404); response.end(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-wiki.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-wiki.js index 17170d078d4..14089553c41 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-get-wiki.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-wiki.js @@ -51,7 +51,19 @@ exports.handler = function(request,response,state) { response.write(template.substring(0,markerPos + marker.length)); $tw.utils.each(titles,function(title) { var tiddler = $tw.sqlTiddlerStore.getTiddler(title,recipe_name); - response.write(JSON.stringify(Object.assign({},tiddler,{revision: "0", bag: "bag-gamma"}))); + if((tiddler.text || "").length > 10 * 1024 * 1024) { + response.write(JSON.stringify(Object.assign({},tiddler,{ + revision: "0", + bag: "bag-gamma", + text: undefined, + _canonical_uri: `/wiki/${recipe_name}/recipes/${recipe_name}/tiddlers/${title}` + }))); + } else { + response.write(JSON.stringify(Object.assign({},tiddler,{ + revision: "0", + bag: "bag-gamma" + }))); + } response.write(",") }); response.write(JSON.stringify({title: "$:/config/tiddlyweb/host",text: "$protocol$//$host$$pathname$/"})); From 54432485e71f84d73d495734fe602f06d1e3d1b1 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Fri, 19 Jan 2024 19:25:58 +0000 Subject: [PATCH 024/213] Add an HTML view of bag listings --- .../multiwikiserver/modules/route-get-bag.js | 51 +++++++++++++++++++ .../multiwikiserver/modules/route-get-wiki.js | 2 +- .../modules/sql-tiddler-store.js | 19 +++++++ .../multiwikiserver/templates/get-bags.tid | 13 +++++ 4 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 plugins/tiddlywiki/multiwikiserver/modules/route-get-bag.js create mode 100644 plugins/tiddlywiki/multiwikiserver/templates/get-bags.tid diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-bag.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-bag.js new file mode 100644 index 00000000000..c1f64c3a96d --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-bag.js @@ -0,0 +1,51 @@ +/*\ +title: $:/plugins/tiddlywiki/multiwikiserver/route-get-bag.js +type: application/javascript +module-type: route + +GET /wikis/:bag_name/bags/:bag_name + +NOTE: Urls currently include the bag name twice. This is temporary to minimise the changes to the TiddlyWeb plugin + +\*/ +(function() { + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +exports.method = "GET"; + +exports.path = /^\/wiki\/([^\/]+)\/bags\/([^\/]+)$/; + +exports.handler = function(request,response,state) { + // Get the parameters + var bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]), + bag_name_2 = $tw.utils.decodeURIComponentSafe(state.params[1]), + titles = bag_name === bag_name_2 && $tw.sqlTiddlerStore.getBagTiddlers(bag_name); + if(bag_name === bag_name_2 && titles) { + // 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(titles),"utf8"); + } else { + // This is not a JSON API request, we should return the raw tiddler content + response.writeHead(200, "OK",{ + "Content-Type": "text/html" + }); + // Render the html + var html = $tw.sqlTiddlerStore.adminWiki.renderTiddler("text/html","$:/plugins/tiddlywiki/multiwikiserver/templates/get-bags",{ + variables: { + "bag-name": bag_name, + "bag-titles": JSON.stringify(titles) + } + }); + response.write(html); + response.end();; + } + } else { + response.writeHead(404); + response.end(); + } +}; + +}()); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-wiki.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-wiki.js index 14089553c41..561c6f9616e 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-get-wiki.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-wiki.js @@ -28,7 +28,7 @@ exports.handler = function(request,response,state) { // Get the tiddlers in the recipe var titles = $tw.sqlTiddlerStore.getRecipeTiddlers(recipe_name); // Render the template - var template = $tw.wiki.renderTiddler("text/plain","$:/core/templates/tiddlywiki5.html",{ + var template = $tw.sqlTiddlerStore.adminWiki.renderTiddler("text/plain","$:/core/templates/tiddlywiki5.html",{ variables: { saveTiddlerFilter: ` $:/boot/boot.css diff --git a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js index d24a8212c80..d794fd5432b 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js @@ -327,6 +327,25 @@ SqlTiddlerStore.prototype.getTiddler = function(title,recipename) { } }; +/* +Get the titles of the tiddlers in a bag. Returns an empty array for bags that do not exist +*/ +SqlTiddlerStore.prototype.getBagTiddlers = function(bagname) { + const rows = this.runStatementGetAll(` + SELECT DISTINCT title + FROM tiddlers + WHERE bag_id IN ( + SELECT bag_id + FROM bags + WHERE bag_name = $bag_name + ) + ORDER BY title ASC + `,{ + bag_name: bagname + }); + return rows.map(value => value.title); +}; + /* Get the titles of the tiddlers in a recipe. Returns an empty array for recipes that do not exist */ diff --git a/plugins/tiddlywiki/multiwikiserver/templates/get-bags.tid b/plugins/tiddlywiki/multiwikiserver/templates/get-bags.tid new file mode 100644 index 00000000000..440e54767c5 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/templates/get-bags.tid @@ -0,0 +1,13 @@ +title: $:/plugins/tiddlywiki/multiwikiserver/templates/get-bags + +! Bag <$text text={{{ []}}}/> + + From 02afbb4000336b2f6e50156f3eb06f95c47f4363 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Fri, 19 Jan 2024 19:27:54 +0000 Subject: [PATCH 025/213] Rename some of the routes more logically --- .../{route-delete-tiddler.js => route-delete-recipe-tiddler.js} | 2 +- .../{route-get-tiddler.js => route-get-recipe-tiddler.js} | 2 +- ...e-get-tiddlers-json.js => route-get-recipe-tiddlers-json.js} | 2 +- .../modules/{route-get-wiki.js => route-get-recipe.js} | 2 +- .../{route-put-tiddler.js => route-put-recipe-tiddler.js} | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename plugins/tiddlywiki/multiwikiserver/modules/{route-delete-tiddler.js => route-delete-recipe-tiddler.js} (93%) rename plugins/tiddlywiki/multiwikiserver/modules/{route-get-tiddler.js => route-get-recipe-tiddler.js} (96%) rename plugins/tiddlywiki/multiwikiserver/modules/{route-get-tiddlers-json.js => route-get-recipe-tiddlers-json.js} (94%) rename plugins/tiddlywiki/multiwikiserver/modules/{route-get-wiki.js => route-get-recipe.js} (97%) rename plugins/tiddlywiki/multiwikiserver/modules/{route-put-tiddler.js => route-put-recipe-tiddler.js} (95%) diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-delete-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/route-delete-recipe-tiddler.js similarity index 93% rename from plugins/tiddlywiki/multiwikiserver/modules/route-delete-tiddler.js rename to plugins/tiddlywiki/multiwikiserver/modules/route-delete-recipe-tiddler.js index 41967630894..b16cbf47b09 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-delete-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-delete-recipe-tiddler.js @@ -1,5 +1,5 @@ /*\ -title: $:/plugins/tiddlywiki/multiwikiserver/route-delete-tiddler.js +title: $:/plugins/tiddlywiki/multiwikiserver/route-delete-recipe-tiddler.js type: application/javascript module-type: route diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddler.js similarity index 96% rename from plugins/tiddlywiki/multiwikiserver/modules/route-get-tiddler.js rename to plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddler.js index b45884caa7a..e2c539b9f4e 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-get-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddler.js @@ -1,5 +1,5 @@ /*\ -title: $:/plugins/tiddlywiki/multiwikiserver/route-get-tiddler.js +title: $:/plugins/tiddlywiki/multiwikiserver/route-get-recipe-tiddler.js type: application/javascript module-type: route diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-tiddlers-json.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddlers-json.js similarity index 94% rename from plugins/tiddlywiki/multiwikiserver/modules/route-get-tiddlers-json.js rename to plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddlers-json.js index 281534f0970..01cc42a2a63 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-get-tiddlers-json.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddlers-json.js @@ -1,5 +1,5 @@ /*\ -title: $:/plugins/tiddlywiki/multiwikiserver/route-get-tiddlers-json.js +title: $:/plugins/tiddlywiki/multiwikiserver/route-get-recipe-tiddlers-json.js type: application/javascript module-type: route diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-wiki.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe.js similarity index 97% rename from plugins/tiddlywiki/multiwikiserver/modules/route-get-wiki.js rename to plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe.js index 561c6f9616e..60c235a1110 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-get-wiki.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe.js @@ -1,5 +1,5 @@ /*\ -title: $:/plugins/tiddlywiki/multiwikiserver/route-get-wiki.js +title: $:/plugins/tiddlywiki/multiwikiserver/route-get-recipe.js type: application/javascript module-type: route diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-put-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/route-put-recipe-tiddler.js similarity index 95% rename from plugins/tiddlywiki/multiwikiserver/modules/route-put-tiddler.js rename to plugins/tiddlywiki/multiwikiserver/modules/route-put-recipe-tiddler.js index e9d9d7ae137..31c95d7e907 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-put-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-put-recipe-tiddler.js @@ -1,5 +1,5 @@ /*\ -title: $:/plugins/tiddlywiki/multiwikiserver/route-put-tiddler.js +title: $:/plugins/tiddlywiki/multiwikiserver/route-put-recipe-tiddler.js type: application/javascript module-type: route From 5fddd3b1042ccf0355d53c4146ea921ed86dbabc Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Fri, 19 Jan 2024 19:33:58 +0000 Subject: [PATCH 026/213] Add support for retrieving tiddlers from bags --- .../modules/route-get-bag-tiddler.js | 61 +++++++++++++++++++ .../modules/route-get-recipe-tiddler.js | 2 +- .../modules/route-get-recipe-tiddlers-json.js | 2 +- .../modules/route-get-recipe.js | 2 +- .../modules/sql-tiddler-store.js | 26 +++++++- .../modules/tests-sql-tiddler-store.js | 16 ++--- 6 files changed, 97 insertions(+), 12 deletions(-) create mode 100644 plugins/tiddlywiki/multiwikiserver/modules/route-get-bag-tiddler.js diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-bag-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-bag-tiddler.js new file mode 100644 index 00000000000..e1165062b0a --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-bag-tiddler.js @@ -0,0 +1,61 @@ +/*\ +title: $:/plugins/tiddlywiki/multiwikiserver/route-get-bag-tiddler.js +type: application/javascript +module-type: route + +GET /wikis/:bag_name/bags/:bag_name/tiddler/:title + +NOTE: Urls currently include the bag name twice. This is temporary to minimise the changes to the TiddlyWeb plugin + +\*/ +(function() { + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +exports.method = "GET"; + +exports.path = /^\/wiki\/([^\/]+)\/bags\/([^\/]+)\/tiddlers\/([^\/]+)$/; + +exports.handler = function(request,response,state) { + // Get the parameters + var bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]), + bag_name_2 = $tw.utils.decodeURIComponentSafe(state.params[1]), + title = $tw.utils.decodeURIComponentSafe(state.params[2]), + tiddler = bag_name === bag_name_2 && $tw.sqlTiddlerStore.getBagTiddler(title,bag_name); + if(bag_name === bag_name_2 && 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) { + var tiddlerFields = {}, + knownFields = [ + "bag", "created", "creator", "modified", "modifier", "permissions", "recipe", "revision", "tags", "text", "title", "type", "uri" + ]; + $tw.utils.each(tiddler,function(value,name) { + if(knownFields.indexOf(name) !== -1) { + tiddlerFields[name] = value; + } else { + tiddlerFields.fields = tiddlerFields.fields || {}; + tiddlerFields.fields[name] = value; + } + }); + tiddlerFields.revision = "0"; + tiddlerFields.bag = "bag-gamma"; + tiddlerFields.type = tiddlerFields.type || "text/vnd.tiddlywiki"; + state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify(tiddlerFields),"utf8"); + } else { + // This is not a JSON API request, we should return the raw tiddler content + var type = tiddler.type || "text/plain"; + response.writeHead(200, "OK",{ + "Content-Type": type + }); + response.write(tiddler.text || "",($tw.config.contentTypeInfo[type] ||{encoding: "utf8"}).encoding); + response.end();; + } + } else { + response.writeHead(404); + response.end(); + } +}; + +}()); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddler.js index e2c539b9f4e..df04d5fb25a 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddler.js @@ -23,7 +23,7 @@ exports.handler = function(request,response,state) { var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]), recipe_name_2 = $tw.utils.decodeURIComponentSafe(state.params[1]), title = $tw.utils.decodeURIComponentSafe(state.params[2]), - tiddler = recipe_name === recipe_name_2 && $tw.sqlTiddlerStore.getTiddler(title,recipe_name); + tiddler = recipe_name === recipe_name_2 && $tw.sqlTiddlerStore.getRecipeTiddler(title,recipe_name); if(recipe_name === recipe_name_2 && 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) { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddlers-json.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddlers-json.js index 01cc42a2a63..24c5eaf8852 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddlers-json.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddlers-json.js @@ -28,7 +28,7 @@ exports.handler = function(request,response,state) { // Get a skinny version of each tiddler var tiddlers = []; $tw.utils.each(titles,function(title) { - var tiddler = $tw.sqlTiddlerStore.getTiddler(title,recipe_name); + var tiddler = $tw.sqlTiddlerStore.getRecipeTiddler(title,recipe_name); tiddlers.push(Object.assign({},tiddler,{text: undefined, revision: "0", bag: "bag-gamma"})); }); var text = JSON.stringify(tiddlers); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe.js index 60c235a1110..fb011100e24 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe.js @@ -50,7 +50,7 @@ exports.handler = function(request,response,state) { } response.write(template.substring(0,markerPos + marker.length)); $tw.utils.each(titles,function(title) { - var tiddler = $tw.sqlTiddlerStore.getTiddler(title,recipe_name); + var tiddler = $tw.sqlTiddlerStore.getRecipeTiddler(title,recipe_name); if((tiddler.text || "").length > 10 * 1024 * 1024) { response.write(JSON.stringify(Object.assign({},tiddler,{ revision: "0", diff --git a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js index d794fd5432b..3651e78e4ae 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js @@ -299,7 +299,31 @@ SqlTiddlerStore.prototype.deleteTiddler = function(title,bagname) { }); }; -SqlTiddlerStore.prototype.getTiddler = function(title,recipename) { +SqlTiddlerStore.prototype.getBagTiddler = function(title,bagname) { + const rows = this.runStatementGetAll(` + SELECT field_name, field_value + FROM fields + WHERE tiddler_id = ( + SELECT t.tiddler_id + 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 + ) + `,{ + title: title, + bag_name: bagname + }); + if(rows.length === 0) { + return null; + } else { + return rows.reduce((accumulator,value) => { + accumulator[value["field_name"]] = value.field_value; + return accumulator; + },{title: title}); + } +}; + +SqlTiddlerStore.prototype.getRecipeTiddler = function(title,recipename) { const rows = this.runStatementGetAll(` SELECT field_name, field_value FROM fields diff --git a/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js index f6724cf30b9..69cda1aab08 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js @@ -44,23 +44,23 @@ describe("SQL tiddler store", function() { // Verify what we've got expect(sqlTiddlerStore.getRecipeTiddlers("recipe-rho")).toEqual([ "Another Tiddler", "Hello There"]); expect(sqlTiddlerStore.getRecipeTiddlers("recipe-sigma")).toEqual([ "Another Tiddler", "Hello There"]); - expect(sqlTiddlerStore.getTiddler("Hello There","recipe-rho")).toEqual({ title: "Hello There", text: "I'm in beta", tags: "four five six" }); - expect(sqlTiddlerStore.getTiddler("Missing Tiddler","recipe-rho")).toEqual(null); - expect(sqlTiddlerStore.getTiddler("Another Tiddler","recipe-rho")).toEqual({ title: "Another Tiddler", text: "I'm in alpha", tags: "one two three" }); - expect(sqlTiddlerStore.getTiddler("Hello There","recipe-sigma")).toEqual({ title: "Hello There", text: "I'm in gamma", tags: "seven eight nine" }); - expect(sqlTiddlerStore.getTiddler("Another Tiddler","recipe-sigma")).toEqual({ title: "Another Tiddler", text: "I'm in alpha", tags: "one two three" }); - expect(sqlTiddlerStore.getTiddler("Hello There","recipe-upsilon")).toEqual({title: "Hello There",text: "I'm in beta",tags: "four five six"}); + expect(sqlTiddlerStore.getRecipeTiddler("Hello There","recipe-rho")).toEqual({ title: "Hello There", text: "I'm in beta", tags: "four five six" }); + expect(sqlTiddlerStore.getRecipeTiddler("Missing Tiddler","recipe-rho")).toEqual(null); + expect(sqlTiddlerStore.getRecipeTiddler("Another Tiddler","recipe-rho")).toEqual({ title: "Another Tiddler", text: "I'm in alpha", tags: "one two three" }); + expect(sqlTiddlerStore.getRecipeTiddler("Hello There","recipe-sigma")).toEqual({ title: "Hello There", text: "I'm in gamma", tags: "seven eight nine" }); + expect(sqlTiddlerStore.getRecipeTiddler("Another Tiddler","recipe-sigma")).toEqual({ title: "Another Tiddler", text: "I'm in alpha", tags: "one two three" }); + expect(sqlTiddlerStore.getRecipeTiddler("Hello There","recipe-upsilon")).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 sqlTiddlerStore.deleteTiddler("Hello There","bag-beta"); expect(sqlTiddlerStore.getRecipeTiddlers("recipe-rho")).toEqual([ "Another Tiddler", "Hello There"]); expect(sqlTiddlerStore.getRecipeTiddlers("recipe-sigma")).toEqual([ "Another Tiddler", "Hello There"]); - expect(sqlTiddlerStore.getTiddler("Hello There","recipe-beta")).toEqual(null); + expect(sqlTiddlerStore.getRecipeTiddler("Hello There","recipe-beta")).toEqual(null); sqlTiddlerStore.deleteTiddler("Another Tiddler","bag-alpha"); expect(sqlTiddlerStore.getRecipeTiddlers("recipe-rho")).toEqual([ "Hello There"]); expect(sqlTiddlerStore.getRecipeTiddlers("recipe-sigma")).toEqual([ "Hello There"]); // Save a recipe tiddler sqlTiddlerStore.saveRecipeTiddler({title: "More", text: "None"},"recipe-rho"); - expect(sqlTiddlerStore.getTiddler("More","recipe-rho")).toEqual({title: "More", text: "None"}); + expect(sqlTiddlerStore.getRecipeTiddler("More","recipe-rho")).toEqual({title: "More", text: "None"}); }); }); From 70b048f35689f3193006c8a52a0115e8fe573073 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Fri, 19 Jan 2024 19:36:36 +0000 Subject: [PATCH 027/213] Fix bag links --- .../admin-ui/MultiWikiServer Administration.tid | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/tiddlywiki/multiwikiserver/admin-ui/MultiWikiServer Administration.tid b/plugins/tiddlywiki/multiwikiserver/admin-ui/MultiWikiServer Administration.tid index 4521d1fc655..4138b130db5 100644 --- a/plugins/tiddlywiki/multiwikiserver/admin-ui/MultiWikiServer Administration.tid +++ b/plugins/tiddlywiki/multiwikiserver/admin-ui/MultiWikiServer Administration.tid @@ -51,7 +51,7 @@ title: MultiWikiServer Administration
    <$list filter="[prefix[$:/state/multiwikiserver/recipes/]]">
  • - + <$text text={{!!recipe-name}}/>
      @@ -74,7 +74,9 @@ Higher numbered bags take priority if a tiddler with the same title is in more t From 8f9ae7e4d599b248e38c5d9e41e91af090a83605 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Fri, 19 Jan 2024 19:52:57 +0000 Subject: [PATCH 028/213] Clarify method name --- plugins/tiddlywiki/multiwikiserver/modules/init.js | 6 +++--- .../multiwikiserver/modules/sql-tiddler-store.js | 4 ++-- .../multiwikiserver/modules/tests-sql-tiddler-store.js | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/plugins/tiddlywiki/multiwikiserver/modules/init.js b/plugins/tiddlywiki/multiwikiserver/modules/init.js index 3750a6a15c3..492c743f9bb 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/init.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/init.js @@ -52,9 +52,9 @@ exports.startup = function() { $tw.sqlTiddlerStore.createRecipe("recipe-tau",["bag-alpha"]); $tw.sqlTiddlerStore.createRecipe("recipe-upsilon",["bag-alpha","bag-gamma","bag-beta"]); // Save tiddlers - $tw.sqlTiddlerStore.saveTiddler({title: "$:/SiteTitle",text: "Bag Alpha"},"bag-alpha"); - $tw.sqlTiddlerStore.saveTiddler({title: "$:/SiteTitle",text: "Bag Beta"},"bag-beta"); - $tw.sqlTiddlerStore.saveTiddler({title: "$:/SiteTitle",text: "Bag Gamma"},"bag-gamma"); + $tw.sqlTiddlerStore.saveBagTiddler({title: "$:/SiteTitle",text: "Bag Alpha"},"bag-alpha"); + $tw.sqlTiddlerStore.saveBagTiddler({title: "$:/SiteTitle",text: "Bag Beta"},"bag-beta"); + $tw.sqlTiddlerStore.saveBagTiddler({title: "$:/SiteTitle",text: "Bag Gamma"},"bag-gamma"); }; })(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js index 3651e78e4ae..475e9c6f6bc 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js @@ -212,7 +212,7 @@ SqlTiddlerStore.prototype.createRecipe = function(recipename,bagnames) { }); }; -SqlTiddlerStore.prototype.saveTiddler = function(tiddlerFields,bagname) { +SqlTiddlerStore.prototype.saveBagTiddler = function(tiddlerFields,bagname) { // Update the tiddlers table this.runStatement(` INSERT OR REPLACE INTO tiddlers (bag_id, title) @@ -269,7 +269,7 @@ SqlTiddlerStore.prototype.saveRecipeTiddler = function(tiddlerFields,recipename) recipe_name: recipename }); // Save the tiddler to the topmost bag - this.saveTiddler(tiddlerFields,row.bag_name); + this.saveBagTiddler(tiddlerFields,row.bag_name); }; SqlTiddlerStore.prototype.deleteTiddler = function(title,bagname) { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js index 69cda1aab08..8a4b13f4917 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js @@ -37,10 +37,10 @@ describe("SQL tiddler store", function() { // Run tests it("should save and retrieve tiddlers", function() { // Save tiddlers - sqlTiddlerStore.saveTiddler({title: "Another Tiddler",text: "I'm in alpha",tags: "one two three"},"bag-alpha"); - sqlTiddlerStore.saveTiddler({title: "Hello There",text: "I'm in alpha as well",tags: "one two three"},"bag-alpha"); - sqlTiddlerStore.saveTiddler({title: "Hello There",text: "I'm in beta",tags: "four five six"},"bag-beta"); - sqlTiddlerStore.saveTiddler({title: "Hello There",text: "I'm in gamma",tags: "seven eight nine"},"bag-gamma"); + sqlTiddlerStore.saveBagTiddler({title: "Another Tiddler",text: "I'm in alpha",tags: "one two three"},"bag-alpha"); + sqlTiddlerStore.saveBagTiddler({title: "Hello There",text: "I'm in alpha as well",tags: "one two three"},"bag-alpha"); + sqlTiddlerStore.saveBagTiddler({title: "Hello There",text: "I'm in beta",tags: "four five six"},"bag-beta"); + sqlTiddlerStore.saveBagTiddler({title: "Hello There",text: "I'm in gamma",tags: "seven eight nine"},"bag-gamma"); // Verify what we've got expect(sqlTiddlerStore.getRecipeTiddlers("recipe-rho")).toEqual([ "Another Tiddler", "Hello There"]); expect(sqlTiddlerStore.getRecipeTiddlers("recipe-sigma")).toEqual([ "Another Tiddler", "Hello There"]); From 01d29ed11e76ea2e5073d46c30b9909a8c0e8288 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Fri, 19 Jan 2024 20:12:29 +0000 Subject: [PATCH 029/213] get bag tiddler and put recipe tiddler should return the bag name --- .../multiwikiserver/modules/route-get-bag-tiddler.js | 2 +- .../multiwikiserver/modules/route-put-recipe-tiddler.js | 6 ++---- .../tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js | 1 + 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-bag-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-bag-tiddler.js index e1165062b0a..8fe73851813 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-get-bag-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-bag-tiddler.js @@ -40,7 +40,7 @@ exports.handler = function(request,response,state) { } }); tiddlerFields.revision = "0"; - tiddlerFields.bag = "bag-gamma"; + tiddlerFields.bag = bag_name; tiddlerFields.type = tiddlerFields.type || "text/vnd.tiddlywiki"; state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify(tiddlerFields),"utf8"); } else { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-put-recipe-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/route-put-recipe-tiddler.js index 31c95d7e907..455f9f79419 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-put-recipe-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-put-recipe-tiddler.js @@ -39,11 +39,9 @@ exports.handler = function(request,response,state) { }); // Require the recipe names to match if(recipe_name === recipe_name_2) { - $tw.sqlTiddlerStore.saveRecipeTiddler(fields,recipe_name); - var recipe_bags = $tw.sqlTiddlerStore.getRecipeBags(recipe_name), - top_bag = recipe_bags[recipe_bags.length - 1]; + var bag_name = $tw.sqlTiddlerStore.saveRecipeTiddler(fields,recipe_name); response.writeHead(204, "OK",{ - Etag: "\"" + top_bag + "/" + encodeURIComponent(title) + "/" + 2222 + ":\"", + Etag: "\"" + bag_name + "/" + encodeURIComponent(title) + "/" + 2222 + ":\"", "Content-Type": "text/plain" }); response.end(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js index 475e9c6f6bc..78003a13843 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js @@ -270,6 +270,7 @@ SqlTiddlerStore.prototype.saveRecipeTiddler = function(tiddlerFields,recipename) }); // Save the tiddler to the topmost bag this.saveBagTiddler(tiddlerFields,row.bag_name); + return row.bag_name; }; SqlTiddlerStore.prototype.deleteTiddler = function(title,bagname) { From afa9ad3cdedd054b3dbda4ca69e868e1cef3b6b3 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Fri, 19 Jan 2024 20:35:47 +0000 Subject: [PATCH 030/213] Update store.getRecipeTiddler to also return the bag from which the tiddler came --- .../modules/route-get-recipe-tiddler.js | 8 ++-- .../modules/route-get-recipe-tiddlers-json.js | 4 +- .../modules/route-get-recipe.js | 8 ++-- .../modules/sql-tiddler-store.js | 48 +++++++++++-------- .../modules/tests-sql-tiddler-store.js | 12 ++--- 5 files changed, 45 insertions(+), 35 deletions(-) diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddler.js index df04d5fb25a..3f5d7963c3b 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddler.js @@ -23,15 +23,15 @@ exports.handler = function(request,response,state) { var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]), recipe_name_2 = $tw.utils.decodeURIComponentSafe(state.params[1]), title = $tw.utils.decodeURIComponentSafe(state.params[2]), - tiddler = recipe_name === recipe_name_2 && $tw.sqlTiddlerStore.getRecipeTiddler(title,recipe_name); - if(recipe_name === recipe_name_2 && tiddler) { + tiddlerInfo = recipe_name === recipe_name_2 && $tw.sqlTiddlerStore.getRecipeTiddler(title,recipe_name); + if(recipe_name === recipe_name_2 && tiddlerInfo) { // 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) { var tiddlerFields = {}, knownFields = [ "bag", "created", "creator", "modified", "modifier", "permissions", "recipe", "revision", "tags", "text", "title", "type", "uri" ]; - $tw.utils.each(tiddler,function(value,name) { + $tw.utils.each(tiddlerInfo.tiddler,function(value,name) { if(knownFields.indexOf(name) !== -1) { tiddlerFields[name] = value; } else { @@ -40,7 +40,7 @@ exports.handler = function(request,response,state) { } }); tiddlerFields.revision = "0"; - tiddlerFields.bag = "bag-gamma"; + tiddlerFields.bag = tiddlerInfo.bag_name; tiddlerFields.type = tiddlerFields.type || "text/vnd.tiddlywiki"; state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify(tiddlerFields),"utf8"); } else { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddlers-json.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddlers-json.js index 24c5eaf8852..3f6ec82ccd9 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddlers-json.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddlers-json.js @@ -28,8 +28,8 @@ exports.handler = function(request,response,state) { // Get a skinny version of each tiddler var tiddlers = []; $tw.utils.each(titles,function(title) { - var tiddler = $tw.sqlTiddlerStore.getRecipeTiddler(title,recipe_name); - tiddlers.push(Object.assign({},tiddler,{text: undefined, revision: "0", bag: "bag-gamma"})); + var tiddlerInfo = $tw.sqlTiddlerStore.getRecipeTiddler(title,recipe_name); + tiddlers.push(Object.assign({},tiddlerInfo.tiddler,{text: undefined, revision: "0", bag: "bag-gamma"})); }); var text = JSON.stringify(tiddlers); state.sendResponse(200,{"Content-Type": "application/json"},text,"utf8"); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe.js index fb011100e24..d272aee5ac2 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe.js @@ -50,16 +50,16 @@ exports.handler = function(request,response,state) { } response.write(template.substring(0,markerPos + marker.length)); $tw.utils.each(titles,function(title) { - var tiddler = $tw.sqlTiddlerStore.getRecipeTiddler(title,recipe_name); - if((tiddler.text || "").length > 10 * 1024 * 1024) { - response.write(JSON.stringify(Object.assign({},tiddler,{ + var tiddlerInfo = $tw.sqlTiddlerStore.getRecipeTiddler(title,recipe_name); + if((tiddlerInfo.tiddler.text || "").length > 10 * 1024 * 1024) { + response.write(JSON.stringify(Object.assign({},tiddlerInfo.tiddler,{ revision: "0", bag: "bag-gamma", text: undefined, _canonical_uri: `/wiki/${recipe_name}/recipes/${recipe_name}/tiddlers/${title}` }))); } else { - response.write(JSON.stringify(Object.assign({},tiddler,{ + response.write(JSON.stringify(Object.assign({},tiddlerInfo.tiddler,{ revision: "0", bag: "bag-gamma" }))); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js index 78003a13843..af80cd4861b 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js @@ -324,32 +324,42 @@ SqlTiddlerStore.prototype.getBagTiddler = function(title,bagname) { } }; +/* +Returns {bag_name:, tiddler: {fields}} +*/ SqlTiddlerStore.prototype.getRecipeTiddler = function(title,recipename) { - const rows = this.runStatementGetAll(` - SELECT field_name, field_value - FROM fields - WHERE tiddler_id = ( - SELECT t.tiddler_id - FROM bags AS b - INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id - INNER JOIN recipes AS r ON rb.recipe_id = r.recipe_id - INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id - WHERE r.recipe_name = $recipe_name - AND t.title = $title - ORDER BY rb.position DESC - ) + const rowTiddlerId = this.runStatementGet(` + SELECT t.tiddler_id, b.bag_name + FROM bags AS b + INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id + INNER JOIN recipes AS r ON rb.recipe_id = r.recipe_id + INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id + WHERE r.recipe_name = $recipe_name + AND t.title = $title + ORDER BY rb.position DESC `,{ title: title, recipe_name: recipename }); - if(rows.length === 0) { + if(!rowTiddlerId) { return null; - } else { - return rows.reduce((accumulator,value) => { - accumulator[value["field_name"]] = value.field_value; - return accumulator; - },{title: title}); } + // Get the fields + const rows = this.runStatementGetAll(` + SELECT field_name, field_value + FROM fields + WHERE tiddler_id = $tiddler_id + `,{ + tiddler_id: rowTiddlerId.tiddler_id, + recipe_name: recipename + }); + return { + bag_name: rowTiddlerId.bag_name, + tiddler: rows.reduce((accumulator,value) => { + accumulator[value["field_name"]] = value.field_value; + return accumulator; + },{title: title}) + }; }; /* diff --git a/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js index 8a4b13f4917..9618763e058 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js @@ -44,12 +44,12 @@ describe("SQL tiddler store", function() { // Verify what we've got expect(sqlTiddlerStore.getRecipeTiddlers("recipe-rho")).toEqual([ "Another Tiddler", "Hello There"]); expect(sqlTiddlerStore.getRecipeTiddlers("recipe-sigma")).toEqual([ "Another Tiddler", "Hello There"]); - expect(sqlTiddlerStore.getRecipeTiddler("Hello There","recipe-rho")).toEqual({ title: "Hello There", text: "I'm in beta", tags: "four five six" }); + expect(sqlTiddlerStore.getRecipeTiddler("Hello There","recipe-rho").tiddler).toEqual({ title: "Hello There", text: "I'm in beta", tags: "four five six" }); expect(sqlTiddlerStore.getRecipeTiddler("Missing Tiddler","recipe-rho")).toEqual(null); - expect(sqlTiddlerStore.getRecipeTiddler("Another Tiddler","recipe-rho")).toEqual({ title: "Another Tiddler", text: "I'm in alpha", tags: "one two three" }); - expect(sqlTiddlerStore.getRecipeTiddler("Hello There","recipe-sigma")).toEqual({ title: "Hello There", text: "I'm in gamma", tags: "seven eight nine" }); - expect(sqlTiddlerStore.getRecipeTiddler("Another Tiddler","recipe-sigma")).toEqual({ title: "Another Tiddler", text: "I'm in alpha", tags: "one two three" }); - expect(sqlTiddlerStore.getRecipeTiddler("Hello There","recipe-upsilon")).toEqual({title: "Hello There",text: "I'm in beta",tags: "four five six"}); + expect(sqlTiddlerStore.getRecipeTiddler("Another Tiddler","recipe-rho").tiddler).toEqual({ title: "Another Tiddler", text: "I'm in alpha", tags: "one two three" }); + expect(sqlTiddlerStore.getRecipeTiddler("Hello There","recipe-sigma").tiddler).toEqual({ title: "Hello There", text: "I'm in gamma", tags: "seven eight nine" }); + expect(sqlTiddlerStore.getRecipeTiddler("Another Tiddler","recipe-sigma").tiddler).toEqual({ title: "Another Tiddler", text: "I'm in alpha", tags: "one two three" }); + expect(sqlTiddlerStore.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 sqlTiddlerStore.deleteTiddler("Hello There","bag-beta"); expect(sqlTiddlerStore.getRecipeTiddlers("recipe-rho")).toEqual([ "Another Tiddler", "Hello There"]); @@ -60,7 +60,7 @@ describe("SQL tiddler store", function() { expect(sqlTiddlerStore.getRecipeTiddlers("recipe-sigma")).toEqual([ "Hello There"]); // Save a recipe tiddler sqlTiddlerStore.saveRecipeTiddler({title: "More", text: "None"},"recipe-rho"); - expect(sqlTiddlerStore.getRecipeTiddler("More","recipe-rho")).toEqual({title: "More", text: "None"}); + expect(sqlTiddlerStore.getRecipeTiddler("More","recipe-rho").tiddler).toEqual({title: "More", text: "None"}); }); }); From e9f83ca735e6c82525ff84c0831fc3b6ed470781 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Fri, 19 Jan 2024 22:03:07 +0000 Subject: [PATCH 031/213] Add missing LIMIT 1 --- plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js index af80cd4861b..3a118b622d3 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js @@ -337,6 +337,7 @@ SqlTiddlerStore.prototype.getRecipeTiddler = function(title,recipename) { WHERE r.recipe_name = $recipe_name AND t.title = $title ORDER BY rb.position DESC + LIMIT 1 `,{ title: title, recipe_name: recipename From 59aed49e98d51aa1857aa8eaec787cf22613fde9 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Sat, 20 Jan 2024 20:22:46 +0000 Subject: [PATCH 032/213] Make getRecipeTiddlers return the bagname as well --- .../modules/route-get-recipe-tiddlers-json.js | 8 +++---- .../modules/route-get-recipe.js | 10 ++++---- .../modules/sql-tiddler-store.js | 22 ++++++++--------- .../modules/tests-sql-tiddler-store.js | 24 ++++++++++++++----- 4 files changed, 37 insertions(+), 27 deletions(-) diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddlers-json.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddlers-json.js index 3f6ec82ccd9..4e69ee87b01 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddlers-json.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddlers-json.js @@ -24,12 +24,12 @@ exports.handler = function(request,response,state) { recipe_name_2 = $tw.utils.decodeURIComponentSafe(state.params[1]); if(recipe_name === recipe_name_2) { // Get the tiddlers in the recipe - var titles = $tw.sqlTiddlerStore.getRecipeTiddlers(recipe_name); + var recipeTiddlers = $tw.sqlTiddlerStore.getRecipeTiddlers(recipe_name); // Get a skinny version of each tiddler var tiddlers = []; - $tw.utils.each(titles,function(title) { - var tiddlerInfo = $tw.sqlTiddlerStore.getRecipeTiddler(title,recipe_name); - tiddlers.push(Object.assign({},tiddlerInfo.tiddler,{text: undefined, revision: "0", bag: "bag-gamma"})); + $tw.utils.each(recipeTiddlers,function(recipeTiddlerInfo) { + var tiddlerInfo = $tw.sqlTiddlerStore.getRecipeTiddler(recipeTiddlerInfo.title,recipe_name); + tiddlers.push(Object.assign({},tiddlerInfo.tiddler,{text: undefined, revision: "0", bag: recipeTiddlerInfo.bag_name})); }); var text = JSON.stringify(tiddlers); state.sendResponse(200,{"Content-Type": "application/json"},text,"utf8"); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe.js index d272aee5ac2..7126b8470d0 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe.js @@ -26,7 +26,7 @@ exports.handler = function(request,response,state) { "Content-Type": "text/html" }); // Get the tiddlers in the recipe - var titles = $tw.sqlTiddlerStore.getRecipeTiddlers(recipe_name); + var recipeTiddlers = $tw.sqlTiddlerStore.getRecipeTiddlers(recipe_name); // Render the template var template = $tw.sqlTiddlerStore.adminWiki.renderTiddler("text/plain","$:/core/templates/tiddlywiki5.html",{ variables: { @@ -49,19 +49,19 @@ exports.handler = function(request,response,state) { throw new Error("Cannot find tiddler store in template"); } response.write(template.substring(0,markerPos + marker.length)); - $tw.utils.each(titles,function(title) { - var tiddlerInfo = $tw.sqlTiddlerStore.getRecipeTiddler(title,recipe_name); + $tw.utils.each(recipeTiddlers,function(recipeTiddlerInfo) { + var tiddlerInfo = $tw.sqlTiddlerStore.getRecipeTiddler(recipeTiddlerInfo.title,recipe_name); if((tiddlerInfo.tiddler.text || "").length > 10 * 1024 * 1024) { response.write(JSON.stringify(Object.assign({},tiddlerInfo.tiddler,{ revision: "0", - bag: "bag-gamma", + bag: recipeTiddlerInfo.bag_name, text: undefined, _canonical_uri: `/wiki/${recipe_name}/recipes/${recipe_name}/tiddlers/${title}` }))); } else { response.write(JSON.stringify(Object.assign({},tiddlerInfo.tiddler,{ revision: "0", - bag: "bag-gamma" + bag: recipeTiddlerInfo.bag_name }))); } response.write(",") diff --git a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js index 3a118b622d3..327f037f9a2 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js @@ -387,22 +387,20 @@ Get the titles of the tiddlers in a recipe. Returns an empty array for recipes t */ SqlTiddlerStore.prototype.getRecipeTiddlers = function(recipename) { const rows = this.runStatementGetAll(` - SELECT DISTINCT title - FROM tiddlers - WHERE bag_id IN ( - SELECT bag_id - FROM recipe_bags - WHERE recipe_id = ( - SELECT recipe_id - FROM recipes - WHERE recipe_name = $recipe_name - ) + SELECT title, bag_name + FROM ( + SELECT t.title, 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 + INNER JOIN recipes AS r ON rb.recipe_id = r.recipe_id + INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id + WHERE r.recipe_name = $recipe_name + GROUP BY title ) - ORDER BY title ASC `,{ recipe_name: recipename }); - return rows.map(value => value.title); + return rows; }; /* diff --git a/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js index 9618763e058..b0f9a20e7b7 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js @@ -42,8 +42,14 @@ describe("SQL tiddler store", function() { sqlTiddlerStore.saveBagTiddler({title: "Hello There",text: "I'm in beta",tags: "four five six"},"bag-beta"); sqlTiddlerStore.saveBagTiddler({title: "Hello There",text: "I'm in gamma",tags: "seven eight nine"},"bag-gamma"); // Verify what we've got - expect(sqlTiddlerStore.getRecipeTiddlers("recipe-rho")).toEqual([ "Another Tiddler", "Hello There"]); - expect(sqlTiddlerStore.getRecipeTiddlers("recipe-sigma")).toEqual([ "Another Tiddler", "Hello There"]); + expect(sqlTiddlerStore.getRecipeTiddlers("recipe-rho")).toEqual([ + { title: 'Another Tiddler', bag_name: 'bag-alpha' }, + { title: 'Hello There', bag_name: 'bag-beta' } + ]); + expect(sqlTiddlerStore.getRecipeTiddlers("recipe-sigma")).toEqual([ + { title: 'Another Tiddler', bag_name: 'bag-alpha' }, + { title: 'Hello There', bag_name: 'bag-gamma' } + ]); expect(sqlTiddlerStore.getRecipeTiddler("Hello There","recipe-rho").tiddler).toEqual({ title: "Hello There", text: "I'm in beta", tags: "four five six" }); expect(sqlTiddlerStore.getRecipeTiddler("Missing Tiddler","recipe-rho")).toEqual(null); expect(sqlTiddlerStore.getRecipeTiddler("Another Tiddler","recipe-rho").tiddler).toEqual({ title: "Another Tiddler", text: "I'm in alpha", tags: "one two three" }); @@ -52,12 +58,18 @@ describe("SQL tiddler store", function() { expect(sqlTiddlerStore.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 sqlTiddlerStore.deleteTiddler("Hello There","bag-beta"); - expect(sqlTiddlerStore.getRecipeTiddlers("recipe-rho")).toEqual([ "Another Tiddler", "Hello There"]); - expect(sqlTiddlerStore.getRecipeTiddlers("recipe-sigma")).toEqual([ "Another Tiddler", "Hello There"]); + expect(sqlTiddlerStore.getRecipeTiddlers("recipe-rho")).toEqual([ + { title: 'Another Tiddler', bag_name: 'bag-alpha' }, + { title: 'Hello There', bag_name: 'bag-alpha' } + ]); + expect(sqlTiddlerStore.getRecipeTiddlers("recipe-sigma")).toEqual([ + { title: 'Another Tiddler', bag_name: 'bag-alpha' }, + { title: 'Hello There', bag_name: 'bag-gamma' } + ]); expect(sqlTiddlerStore.getRecipeTiddler("Hello There","recipe-beta")).toEqual(null); sqlTiddlerStore.deleteTiddler("Another Tiddler","bag-alpha"); - expect(sqlTiddlerStore.getRecipeTiddlers("recipe-rho")).toEqual([ "Hello There"]); - expect(sqlTiddlerStore.getRecipeTiddlers("recipe-sigma")).toEqual([ "Hello There"]); + expect(sqlTiddlerStore.getRecipeTiddlers("recipe-rho")).toEqual([ { title: 'Hello There', bag_name: 'bag-alpha' } ]); + expect(sqlTiddlerStore.getRecipeTiddlers("recipe-sigma")).toEqual([ { title: 'Hello There', bag_name: 'bag-gamma' } ]); // Save a recipe tiddler sqlTiddlerStore.saveRecipeTiddler({title: "More", text: "None"},"recipe-rho"); expect(sqlTiddlerStore.getRecipeTiddler("More","recipe-rho").tiddler).toEqual({title: "More", text: "None"}); From d832bbcc708f3b069eaff57886dad5a32309c3e9 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Sat, 20 Jan 2024 21:48:33 +0000 Subject: [PATCH 033/213] Order the results of getRecipeTiddlers --- .../tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js index 327f037f9a2..84b688e2bc4 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js @@ -395,7 +395,8 @@ SqlTiddlerStore.prototype.getRecipeTiddlers = function(recipename) { INNER JOIN recipes AS r ON rb.recipe_id = r.recipe_id INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id WHERE r.recipe_name = $recipe_name - GROUP BY title + GROUP BY t.title + ORDER BY t.title ) `,{ recipe_name: recipename From 11ecaff7db2f619b9106f2ee86f8395ce1324e4a Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Sat, 20 Jan 2024 21:48:40 +0000 Subject: [PATCH 034/213] Fix typo --- .../multiwikiserver/modules/route-get-recipe-tiddler.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddler.js index 3f5d7963c3b..e3815fdf516 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddler.js @@ -24,7 +24,7 @@ exports.handler = function(request,response,state) { recipe_name_2 = $tw.utils.decodeURIComponentSafe(state.params[1]), title = $tw.utils.decodeURIComponentSafe(state.params[2]), tiddlerInfo = recipe_name === recipe_name_2 && $tw.sqlTiddlerStore.getRecipeTiddler(title,recipe_name); - if(recipe_name === recipe_name_2 && tiddlerInfo) { + if(recipe_name === recipe_name_2 && 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) { var tiddlerFields = {}, @@ -45,11 +45,11 @@ exports.handler = function(request,response,state) { state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify(tiddlerFields),"utf8"); } else { // This is not a JSON API request, we should return the raw tiddler content - var type = tiddler.type || "text/plain"; + var type = tiddlerInfo.tiddler.type || "text/plain"; response.writeHead(200, "OK",{ "Content-Type": type }); - response.write(tiddler.text || "",($tw.config.contentTypeInfo[type] ||{encoding: "utf8"}).encoding); + response.write(tiddlerInfo.tiddler.text || "",($tw.config.contentTypeInfo[type] ||{encoding: "utf8"}).encoding); response.end();; } } else { From f7914db0196bc749ff598c881b91471f10d82c81 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Sat, 20 Jan 2024 21:50:12 +0000 Subject: [PATCH 035/213] Add bag and recipe favicons to dashboard --- .../MultiWikiServer Administration.tid | 74 ++++++++++--------- .../multiwikiserver/admin-ui/Styles.tid | 5 ++ 2 files changed, 43 insertions(+), 36 deletions(-) diff --git a/plugins/tiddlywiki/multiwikiserver/admin-ui/MultiWikiServer Administration.tid b/plugins/tiddlywiki/multiwikiserver/admin-ui/MultiWikiServer Administration.tid index 4138b130db5..ca3eafe66cc 100644 --- a/plugins/tiddlywiki/multiwikiserver/admin-ui/MultiWikiServer Administration.tid +++ b/plugins/tiddlywiki/multiwikiserver/admin-ui/MultiWikiServer Administration.tid @@ -47,40 +47,42 @@ title: MultiWikiServer Administration \end createRecipeButton
      -

      Recipes

      - -
      -<> +

      Recipes

      + +
      + <> +
      +
      + Higher numbered bags take priority if a tiddler with the same title is in more than one bag +
      +

      Bags

      + +
      + <> +
      -
      -Higher numbered bags take priority if a tiddler with the same title is in more than one bag -
      -

      Bags

      - -
      -<> -
      -
      \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/admin-ui/Styles.tid b/plugins/tiddlywiki/multiwikiserver/admin-ui/Styles.tid index 5b62f657c7b..70ea17ed0ed 100644 --- a/plugins/tiddlywiki/multiwikiserver/admin-ui/Styles.tid +++ b/plugins/tiddlywiki/multiwikiserver/admin-ui/Styles.tid @@ -10,3 +10,8 @@ Styles specific to the full screen layout .mws-admin-layout { padding: 1rem; } + +.mws-favicon { + max-width: 2em; + max-height: 2em; +} \ No newline at end of file From 4f9ba11489da03ab11a7ac2b5f720b526bcf64d8 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Sun, 21 Jan 2024 18:17:23 +0000 Subject: [PATCH 036/213] Update to newest better-sqlite3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f2393bffe8e..86ed80063c0 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,6 @@ "lint": "eslint ." }, "dependencies": { - "better-sqlite3": "^9.2.2" + "better-sqlite3": "^9.3.0" } } From dc8692044c6079734d9b8e107fb5a765a51a50b0 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Sun, 21 Jan 2024 18:18:29 +0000 Subject: [PATCH 037/213] Use SQLite's AUTOINCREMENT to give us tiddler version identifiers This commit fixes sync within hosted wikis --- .../modules/route-get-bag-tiddler.js | 12 ++--- .../modules/route-get-recipe-tiddler.js | 2 +- .../modules/route-get-recipe-tiddlers-json.js | 2 +- .../modules/route-get-recipe.js | 4 +- .../modules/route-put-recipe-tiddler.js | 4 +- .../modules/sql-tiddler-store.js | 45 +++++++++++++------ .../modules/tests-sql-tiddler-store.js | 2 +- 7 files changed, 45 insertions(+), 26 deletions(-) diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-bag-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-bag-tiddler.js index 8fe73851813..e3ce8ef51f0 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-get-bag-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-bag-tiddler.js @@ -23,15 +23,15 @@ exports.handler = function(request,response,state) { var bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]), bag_name_2 = $tw.utils.decodeURIComponentSafe(state.params[1]), title = $tw.utils.decodeURIComponentSafe(state.params[2]), - tiddler = bag_name === bag_name_2 && $tw.sqlTiddlerStore.getBagTiddler(title,bag_name); - if(bag_name === bag_name_2 && tiddler) { + result = bag_name === bag_name_2 && $tw.sqlTiddlerStore.getBagTiddler(title,bag_name); + if(bag_name === bag_name_2 && result) { // 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) { var tiddlerFields = {}, knownFields = [ "bag", "created", "creator", "modified", "modifier", "permissions", "recipe", "revision", "tags", "text", "title", "type", "uri" ]; - $tw.utils.each(tiddler,function(value,name) { + $tw.utils.each(result.tiddler,function(value,name) { if(knownFields.indexOf(name) !== -1) { tiddlerFields[name] = value; } else { @@ -39,17 +39,17 @@ exports.handler = function(request,response,state) { tiddlerFields.fields[name] = value; } }); - tiddlerFields.revision = "0"; + tiddlerFields.revision = "" + result.tiddler_id; tiddlerFields.bag = bag_name; tiddlerFields.type = tiddlerFields.type || "text/vnd.tiddlywiki"; state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify(tiddlerFields),"utf8"); } else { // This is not a JSON API request, we should return the raw tiddler content - var type = tiddler.type || "text/plain"; + var type = result.tiddler.type || "text/plain"; response.writeHead(200, "OK",{ "Content-Type": type }); - response.write(tiddler.text || "",($tw.config.contentTypeInfo[type] ||{encoding: "utf8"}).encoding); + response.write(result.tiddler.text || "",($tw.config.contentTypeInfo[type] ||{encoding: "utf8"}).encoding); response.end();; } } else { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddler.js index e3815fdf516..b34075759ed 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddler.js @@ -39,7 +39,7 @@ exports.handler = function(request,response,state) { tiddlerFields.fields[name] = value; } }); - tiddlerFields.revision = "0"; + tiddlerFields.revision = "" + tiddlerInfo.tiddler_id; tiddlerFields.bag = tiddlerInfo.bag_name; tiddlerFields.type = tiddlerFields.type || "text/vnd.tiddlywiki"; state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify(tiddlerFields),"utf8"); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddlers-json.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddlers-json.js index 4e69ee87b01..f8d81173b93 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddlers-json.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddlers-json.js @@ -29,7 +29,7 @@ exports.handler = function(request,response,state) { var tiddlers = []; $tw.utils.each(recipeTiddlers,function(recipeTiddlerInfo) { var tiddlerInfo = $tw.sqlTiddlerStore.getRecipeTiddler(recipeTiddlerInfo.title,recipe_name); - tiddlers.push(Object.assign({},tiddlerInfo.tiddler,{text: undefined, revision: "0", bag: recipeTiddlerInfo.bag_name})); + tiddlers.push(Object.assign({},tiddlerInfo.tiddler,{text: undefined, revision: "" + tiddlerInfo.tiddler_id, bag: recipeTiddlerInfo.bag_name})); }); var text = JSON.stringify(tiddlers); state.sendResponse(200,{"Content-Type": "application/json"},text,"utf8"); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe.js index 7126b8470d0..d77cbf5f77a 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe.js @@ -53,14 +53,14 @@ exports.handler = function(request,response,state) { var tiddlerInfo = $tw.sqlTiddlerStore.getRecipeTiddler(recipeTiddlerInfo.title,recipe_name); if((tiddlerInfo.tiddler.text || "").length > 10 * 1024 * 1024) { response.write(JSON.stringify(Object.assign({},tiddlerInfo.tiddler,{ - revision: "0", + revision: "" + tiddlerInfo.tiddler_id, bag: recipeTiddlerInfo.bag_name, text: undefined, _canonical_uri: `/wiki/${recipe_name}/recipes/${recipe_name}/tiddlers/${title}` }))); } else { response.write(JSON.stringify(Object.assign({},tiddlerInfo.tiddler,{ - revision: "0", + revision: "" + tiddlerInfo.tiddler_id, bag: recipeTiddlerInfo.bag_name }))); } diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-put-recipe-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/route-put-recipe-tiddler.js index 455f9f79419..c5ca2dd52ed 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-put-recipe-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-put-recipe-tiddler.js @@ -39,9 +39,9 @@ exports.handler = function(request,response,state) { }); // Require the recipe names to match if(recipe_name === recipe_name_2) { - var bag_name = $tw.sqlTiddlerStore.saveRecipeTiddler(fields,recipe_name); + var result = $tw.sqlTiddlerStore.saveRecipeTiddler(fields,recipe_name); response.writeHead(204, "OK",{ - Etag: "\"" + bag_name + "/" + encodeURIComponent(title) + "/" + 2222 + ":\"", + Etag: "\"" + result.bag_name + "/" + encodeURIComponent(title) + "/" + result.tiddler_id + ":\"", "Content-Type": "text/plain" }); response.end(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js index 84b688e2bc4..e63951c2fa8 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js @@ -32,7 +32,7 @@ SqlTiddlerStore.prototype.close = function() { SqlTiddlerStore.prototype.runStatement = function(sql,params) { params = params || {}; const statement = this.db.prepare(sql); - statement.run(params); + return statement.run(params); }; SqlTiddlerStore.prototype.runStatementGet = function(sql,params) { @@ -81,14 +81,14 @@ SqlTiddlerStore.prototype.createTables = function() { this.runStatements([` -- Bags have names and access control settings CREATE TABLE IF NOT EXISTS bags ( - bag_id INTEGER PRIMARY KEY, + bag_id INTEGER PRIMARY KEY AUTOINCREMENT, bag_name TEXT UNIQUE, accesscontrol TEXT ) `,` -- Recipes have names... CREATE TABLE IF NOT EXISTS recipes ( - recipe_id INTEGER PRIMARY KEY, + recipe_id INTEGER PRIMARY KEY AUTOINCREMENT, recipe_name TEXT UNIQUE ) `,` @@ -104,7 +104,7 @@ SqlTiddlerStore.prototype.createTables = function() { `,` -- Tiddlers are contained in bags and have titles CREATE TABLE IF NOT EXISTS tiddlers ( - tiddler_id INTEGER PRIMARY KEY, + tiddler_id INTEGER PRIMARY KEY AUTOINCREMENT, bag_id INTEGER, title TEXT, FOREIGN KEY (bag_id) REFERENCES bags(bag_id) ON UPDATE CASCADE ON DELETE CASCADE, @@ -212,9 +212,12 @@ SqlTiddlerStore.prototype.createRecipe = function(recipename,bagnames) { }); }; +/* +Returns {tiddler_id:} +*/ SqlTiddlerStore.prototype.saveBagTiddler = function(tiddlerFields,bagname) { // Update the tiddlers table - this.runStatement(` + var info = this.runStatement(` INSERT OR REPLACE INTO tiddlers (bag_id, title) VALUES ( (SELECT bag_id FROM bags WHERE bag_name = $bag_name), @@ -246,8 +249,14 @@ SqlTiddlerStore.prototype.saveBagTiddler = function(tiddlerFields,bagname) { bag_name: bagname, field_values: JSON.stringify(Object.assign({},tiddlerFields,{title: undefined})) }); + return { + tiddler_id: info.lastInsertRowid + } }; +/* +Returns {tiddler_id:,bag_name:} +*/ SqlTiddlerStore.prototype.saveRecipeTiddler = function(tiddlerFields,recipename) { // Find the topmost bag in the recipe var row = this.runStatementGet(` @@ -269,8 +278,11 @@ SqlTiddlerStore.prototype.saveRecipeTiddler = function(tiddlerFields,recipename) recipe_name: recipename }); // Save the tiddler to the topmost bag - this.saveBagTiddler(tiddlerFields,row.bag_name); - return row.bag_name; + var info = this.saveBagTiddler(tiddlerFields,row.bag_name); + return { + tiddler_id: info.tiddler_id, + bag_name: row.bag_name + }; }; SqlTiddlerStore.prototype.deleteTiddler = function(title,bagname) { @@ -300,9 +312,12 @@ SqlTiddlerStore.prototype.deleteTiddler = function(title,bagname) { }); }; +/* +returns {tiddler_id:,tiddler:} +*/ SqlTiddlerStore.prototype.getBagTiddler = function(title,bagname) { const rows = this.runStatementGetAll(` - SELECT field_name, field_value + SELECT field_name, field_value, tiddler_id FROM fields WHERE tiddler_id = ( SELECT t.tiddler_id @@ -317,15 +332,18 @@ SqlTiddlerStore.prototype.getBagTiddler = function(title,bagname) { if(rows.length === 0) { return null; } else { - return rows.reduce((accumulator,value) => { - accumulator[value["field_name"]] = value.field_value; - return accumulator; - },{title: title}); + return { + tiddler_id: rows[0].tiddler_id, + tiddler: rows.reduce((accumulator,value) => { + accumulator[value["field_name"]] = value.field_value; + return accumulator; + },{title: title}) + }; } }; /* -Returns {bag_name:, tiddler: {fields}} +Returns {bag_name:, tiddler: {fields}, tiddler_id:} */ SqlTiddlerStore.prototype.getRecipeTiddler = function(title,recipename) { const rowTiddlerId = this.runStatementGet(` @@ -356,6 +374,7 @@ SqlTiddlerStore.prototype.getRecipeTiddler = function(title,recipename) { }); return { bag_name: rowTiddlerId.bag_name, + tiddler_id: rowTiddlerId.tiddler_id, tiddler: rows.reduce((accumulator,value) => { accumulator[value["field_name"]] = value.field_value; return accumulator; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js index b0f9a20e7b7..7523ac0927f 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js @@ -71,7 +71,7 @@ describe("SQL tiddler store", function() { expect(sqlTiddlerStore.getRecipeTiddlers("recipe-rho")).toEqual([ { title: 'Hello There', bag_name: 'bag-alpha' } ]); expect(sqlTiddlerStore.getRecipeTiddlers("recipe-sigma")).toEqual([ { title: 'Hello There', bag_name: 'bag-gamma' } ]); // Save a recipe tiddler - sqlTiddlerStore.saveRecipeTiddler({title: "More", text: "None"},"recipe-rho"); + expect(sqlTiddlerStore.saveRecipeTiddler({title: "More", text: "None"},"recipe-rho")).toEqual({tiddler_id: 5, bag_name: 'bag-beta'}); expect(sqlTiddlerStore.getRecipeTiddler("More","recipe-rho").tiddler).toEqual({title: "More", text: "None"}); }); From da5b3163584091559cbd45a5510cc30a7cc373f6 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Mon, 22 Jan 2024 22:08:55 +0000 Subject: [PATCH 038/213] Split SqlTiddlerStore into SqlTiddlerStore and SqlTiddlerDatabase The motivation is to encapsulate knowledge of the SQL queries --- .../multiwikiserver/modules/init.js | 1 - .../modules/sql-tiddler-database.js | 412 ++++++++++++++++++ .../modules/sql-tiddler-store.js | 352 ++------------- .../modules/tests-sql-tiddler-database.js | 82 ++++ .../modules/tests-sql-tiddler-store.js | 82 ---- 5 files changed, 521 insertions(+), 408 deletions(-) create mode 100644 plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-database.js create mode 100644 plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-database.js delete mode 100644 plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js diff --git a/plugins/tiddlywiki/multiwikiserver/modules/init.js b/plugins/tiddlywiki/multiwikiserver/modules/init.js index 492c743f9bb..1fe40007bdf 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/init.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/init.js @@ -41,7 +41,6 @@ exports.startup = function() { $tw.sqlTiddlerStore = new SqlTiddlerStore({ databasePath: databasePath }); - $tw.sqlTiddlerStore.createTables(); $tw.sqlTiddlerStore.updateAdminWiki(); // Create bags and recipes $tw.sqlTiddlerStore.createBag("bag-alpha"); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-database.js b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-database.js new file mode 100644 index 00000000000..2a658cceded --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-database.js @@ -0,0 +1,412 @@ +/*\ +title: $:/plugins/tiddlywiki/multiwikiserver/sql-tiddler-database.js +type: application/javascript +module-type: library + +Low level SQL functions to store and retrieve tiddlers in a SQLite database. + +This class is intended to encapsulate all the SQL queries used to access the database. + +\*/ + +(function() { + +/* +Create a tiddler store. Options include: + +databasePath - path to the database file (can be ":memory:" to get a temporary database) +*/ +function SqlTiddlerDatabase(options) { + options = options || {}; + // Create the database + var databasePath = options.databasePath || ":memory:"; + this.db = new $tw.sqlite3.Database(databasePath,{verbose: undefined && console.log}); +} + +SqlTiddlerDatabase.prototype.close = function() { + this.db.close(); + this.db = undefined; +}; + +SqlTiddlerDatabase.prototype.runStatement = function(sql,params) { + params = params || {}; + const statement = this.db.prepare(sql); + return statement.run(params); +}; + +SqlTiddlerDatabase.prototype.runStatementGet = function(sql,params) { + params = params || {}; + const statement = this.db.prepare(sql); + return statement.get(params); +}; + +SqlTiddlerDatabase.prototype.runStatementGetAll = function(sql,params) { + params = params || {}; + const statement = this.db.prepare(sql); + return statement.all(params); +}; + +SqlTiddlerDatabase.prototype.runStatements = function(sqlArray) { + for(const sql of sqlArray) { + this.runStatement(sql); + } +}; + +SqlTiddlerDatabase.prototype.createTables = function() { + this.runStatements([` + -- Bags have names and access control settings + CREATE TABLE IF NOT EXISTS bags ( + bag_id INTEGER PRIMARY KEY AUTOINCREMENT, + bag_name TEXT UNIQUE, + accesscontrol TEXT + ) + `,` + -- Recipes have names... + CREATE TABLE IF NOT EXISTS recipes ( + recipe_id INTEGER PRIMARY KEY AUTOINCREMENT, + recipe_name TEXT UNIQUE + ) + `,` + -- ...and recipes also have an ordered list of bags + CREATE TABLE IF NOT EXISTS recipe_bags ( + recipe_id INTEGER, + bag_id INTEGER, + position INTEGER, + FOREIGN KEY (recipe_id) REFERENCES recipes(recipe_id) ON UPDATE CASCADE ON DELETE CASCADE, + 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, + bag_id INTEGER, + title TEXT, + 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, + field_name TEXT, + field_value TEXT, + FOREIGN KEY (tiddler_id) REFERENCES tiddlers(tiddler_id) ON UPDATE CASCADE ON DELETE CASCADE, + UNIQUE (tiddler_id, field_name) + ) + `]); +}; + +SqlTiddlerDatabase.prototype.logTables = function() { + var self = this; + function sqlLogTable(table) { + console.log(`TABLE ${table}:`); + let statement = self.db.prepare(`select * from ${table}`); + for(const row of statement.all()) { + console.log(row); + } + } + const tables = ["recipes","bags","recipe_bags","tiddlers","fields"]; + for(const table of tables) { + sqlLogTable(table); + } +}; + +SqlTiddlerDatabase.prototype.listBags = function() { + const rows = this.runStatementGetAll(` + SELECT bag_name, accesscontrol + FROM bags + ORDER BY bag_name + `); + return rows; +}; + +SqlTiddlerDatabase.prototype.createBag = function(bagname) { + // Run the queries + this.runStatement(` + INSERT OR IGNORE INTO bags (bag_name, accesscontrol) + VALUES ($bag_name, '') + `,{ + bag_name: bagname + }); + this.runStatement(` + UPDATE bags + SET accesscontrol = $accesscontrol + WHERE bag_name = $bag_name + `,{ + bag_name: bagname, + accesscontrol: "[some access control stuff]" + }); +}; + +SqlTiddlerDatabase.prototype.listRecipes = function() { + const rows = this.runStatementGetAll(` + SELECT recipe_name + FROM recipes + ORDER BY recipe_name + `); + return rows; +}; + +SqlTiddlerDatabase.prototype.createRecipe = function(recipename,bagnames) { + // Run the queries + this.runStatement(` + -- Create the entry in the recipes table if required + INSERT OR IGNORE INTO recipes (recipe_name) + VALUES ($recipe_name) + `,{ + recipe_name: recipename + }); + this.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: recipename + }); + this.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: recipename, + bag_names: JSON.stringify(bagnames) + }); +}; + +/* +Returns {tiddler_id:} +*/ +SqlTiddlerDatabase.prototype.saveBagTiddler = function(tiddlerFields,bagname) { + // Update the tiddlers table + var info = this.runStatement(` + INSERT OR REPLACE INTO tiddlers (bag_id, title) + VALUES ( + (SELECT bag_id FROM bags WHERE bag_name = $bag_name), + $title + ) + `,{ + title: tiddlerFields.title, + bag_name: bagname + }); + // Update the fields table + this.runStatement(` + INSERT OR REPLACE INTO fields (tiddler_id, field_name, field_value) + SELECT + t.tiddler_id, + json_each.key AS field_name, + json_each.value AS field_value + FROM ( + SELECT tiddler_id + FROM tiddlers + WHERE bag_id = ( + SELECT bag_id + FROM bags + WHERE bag_name = $bag_name + ) AND title = $title + ) AS t + JOIN json_each($field_values) AS json_each + `,{ + title: tiddlerFields.title, + bag_name: bagname, + field_values: JSON.stringify(Object.assign({},tiddlerFields,{title: undefined})) + }); + return { + tiddler_id: info.lastInsertRowid + } +}; + +/* +Returns {tiddler_id:,bag_name:} +*/ +SqlTiddlerDatabase.prototype.saveRecipeTiddler = function(tiddlerFields,recipename) { + // Find the topmost bag in the recipe + var row = this.runStatementGet(` + SELECT b.bag_name + FROM bags AS b + JOIN ( + SELECT rb.bag_id + FROM recipe_bags AS rb + WHERE rb.recipe_id = ( + SELECT recipe_id + FROM recipes + WHERE recipe_name = $recipe_name + ) + ORDER BY rb.position DESC + LIMIT 1 + ) AS selected_bag + ON b.bag_id = selected_bag.bag_id + `,{ + recipe_name: recipename + }); + // Save the tiddler to the topmost bag + var info = this.saveBagTiddler(tiddlerFields,row.bag_name); + return { + tiddler_id: info.tiddler_id, + bag_name: row.bag_name + }; +}; + +SqlTiddlerDatabase.prototype.deleteTiddler = function(title,bagname) { + // Run the queries + this.runStatement(` + DELETE FROM fields + WHERE tiddler_id IN ( + SELECT t.tiddler_id + FROM tiddlers AS t + 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: bagname + }); + this.runStatement(` + DELETE FROM tiddlers + WHERE bag_id = ( + SELECT bag_id + FROM bags + WHERE bag_name = $bag_name + ) AND title = $title + `,{ + title: title, + bag_name: bagname + }); +}; + +/* +returns {tiddler_id:,tiddler:} +*/ +SqlTiddlerDatabase.prototype.getBagTiddler = function(title,bagname) { + const rows = this.runStatementGetAll(` + SELECT field_name, field_value, tiddler_id + FROM fields + WHERE tiddler_id = ( + SELECT t.tiddler_id + 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 + ) + `,{ + title: title, + bag_name: bagname + }); + if(rows.length === 0) { + return null; + } else { + return { + tiddler_id: rows[0].tiddler_id, + tiddler: rows.reduce((accumulator,value) => { + accumulator[value["field_name"]] = value.field_value; + return accumulator; + },{title: title}) + }; + } +}; + +/* +Returns {bag_name:, tiddler: {fields}, tiddler_id:} +*/ +SqlTiddlerDatabase.prototype.getRecipeTiddler = function(title,recipename) { + const rowTiddlerId = this.runStatementGet(` + SELECT t.tiddler_id, b.bag_name + FROM bags AS b + INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id + INNER JOIN recipes AS r ON rb.recipe_id = r.recipe_id + INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id + WHERE r.recipe_name = $recipe_name + AND t.title = $title + ORDER BY rb.position DESC + LIMIT 1 + `,{ + title: title, + recipe_name: recipename + }); + if(!rowTiddlerId) { + return null; + } + // Get the fields + const rows = this.runStatementGetAll(` + SELECT field_name, field_value + FROM fields + WHERE tiddler_id = $tiddler_id + `,{ + tiddler_id: rowTiddlerId.tiddler_id, + recipe_name: recipename + }); + return { + bag_name: rowTiddlerId.bag_name, + tiddler_id: rowTiddlerId.tiddler_id, + tiddler: rows.reduce((accumulator,value) => { + accumulator[value["field_name"]] = value.field_value; + return accumulator; + },{title: title}) + }; +}; + +/* +Get the titles of the tiddlers in a bag. Returns an empty array for bags that do not exist +*/ +SqlTiddlerDatabase.prototype.getBagTiddlers = function(bagname) { + const rows = this.runStatementGetAll(` + SELECT DISTINCT title + FROM tiddlers + WHERE bag_id IN ( + SELECT bag_id + FROM bags + WHERE bag_name = $bag_name + ) + ORDER BY title ASC + `,{ + bag_name: bagname + }); + return rows.map(value => value.title); +}; + +/* +Get the titles of the tiddlers in a recipe as {title:,bag_name:}. Returns an empty array for recipes that do not exist +*/ +SqlTiddlerDatabase.prototype.getRecipeTiddlers = function(recipename) { + const rows = this.runStatementGetAll(` + SELECT title, bag_name + FROM ( + SELECT t.title, 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 + INNER JOIN recipes AS r ON rb.recipe_id = r.recipe_id + INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id + WHERE r.recipe_name = $recipe_name + GROUP BY t.title + ORDER BY t.title + ) + `,{ + recipe_name: recipename + }); + return rows; +}; + +/* +Get the names of the bags in a recipe. Returns an empty array for recipes that do not exist +*/ +SqlTiddlerDatabase.prototype.getRecipeBags = function(recipename) { + const rows = this.runStatementGetAll(` + SELECT bags.bag_name + FROM bags + JOIN ( + SELECT rb.bag_id + FROM recipe_bags AS rb + JOIN recipes AS r ON rb.recipe_id = r.recipe_id + WHERE r.recipe_name = $recipe_name + ORDER BY rb.position + ) AS bag_priority ON bags.bag_id = bag_priority.bag_id + `,{ + recipe_name: recipename + }); + return rows.map(value => value.bag_name); +}; + +exports.SqlTiddlerDatabase = SqlTiddlerDatabase; + +})(); \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js index e63951c2fa8..79ca9c40db2 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js @@ -3,7 +3,11 @@ title: $:/plugins/tiddlywiki/multiwikiserver/sql-tiddler-store.js type: application/javascript module-type: library -Functions to perform basic tiddler operations with a sqlite3 database +Higher level functions to perform basic tiddler operations with a sqlite3 database. + +This class is largely a wrapper for the sql-tiddler-database.js class, adding the following functionality: + +* Synchronising bag and recipe names to the admin wiki \*/ @@ -20,37 +24,17 @@ function SqlTiddlerStore(options) { this.adminWiki = options.adminWiki || $tw.wiki; this.entityStateTiddlerPrefix = "$:/state/multiwikiserver/"; // Create the database - var databasePath = options.databasePath || ":memory:"; - this.db = new $tw.sqlite3.Database(databasePath,{verbose: undefined && console.log}); + this.databasePath = options.databasePath || ":memory:"; + var SqlTiddlerDatabase = require("$:/plugins/tiddlywiki/multiwikiserver/sql-tiddler-database.js").SqlTiddlerDatabase; + this.sqlTiddlerDatabase = new SqlTiddlerDatabase({ + databasePath: this.databasePath + }); + this.sqlTiddlerDatabase.createTables(); } SqlTiddlerStore.prototype.close = function() { - this.db.close(); - this.db = undefined; -}; - -SqlTiddlerStore.prototype.runStatement = function(sql,params) { - params = params || {}; - const statement = this.db.prepare(sql); - return statement.run(params); -}; - -SqlTiddlerStore.prototype.runStatementGet = function(sql,params) { - params = params || {}; - const statement = this.db.prepare(sql); - return statement.get(params); -}; - -SqlTiddlerStore.prototype.runStatementGetAll = function(sql,params) { - params = params || {}; - const statement = this.db.prepare(sql); - return statement.all(params); -}; - -SqlTiddlerStore.prototype.runStatements = function(sqlArray) { - for(const sql of sqlArray) { - this.runStatement(sql); - } + this.sqlTiddlerDatabase.close(); + this.sqlTiddlerDatabase = undefined; }; SqlTiddlerStore.prototype.saveEntityStateTiddler = function(tiddler) { @@ -77,91 +61,16 @@ SqlTiddlerStore.prototype.updateAdminWiki = function() { } }; -SqlTiddlerStore.prototype.createTables = function() { - this.runStatements([` - -- Bags have names and access control settings - CREATE TABLE IF NOT EXISTS bags ( - bag_id INTEGER PRIMARY KEY AUTOINCREMENT, - bag_name TEXT UNIQUE, - accesscontrol TEXT - ) - `,` - -- Recipes have names... - CREATE TABLE IF NOT EXISTS recipes ( - recipe_id INTEGER PRIMARY KEY AUTOINCREMENT, - recipe_name TEXT UNIQUE - ) - `,` - -- ...and recipes also have an ordered list of bags - CREATE TABLE IF NOT EXISTS recipe_bags ( - recipe_id INTEGER, - bag_id INTEGER, - position INTEGER, - FOREIGN KEY (recipe_id) REFERENCES recipes(recipe_id) ON UPDATE CASCADE ON DELETE CASCADE, - 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, - bag_id INTEGER, - title TEXT, - 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, - field_name TEXT, - field_value TEXT, - FOREIGN KEY (tiddler_id) REFERENCES tiddlers(tiddler_id) ON UPDATE CASCADE ON DELETE CASCADE, - UNIQUE (tiddler_id, field_name) - ) - `]); -}; - SqlTiddlerStore.prototype.logTables = function() { - var self = this; - function sqlLogTable(table) { - console.log(`TABLE ${table}:`); - let statement = self.db.prepare(`select * from ${table}`); - for(const row of statement.all()) { - console.log(row); - } - } - const tables = ["recipes","bags","recipe_bags","tiddlers","fields"]; - for(const table of tables) { - sqlLogTable(table); - } + this.sqlTiddlerDatabase.logTables(); }; SqlTiddlerStore.prototype.listBags = function() { - const rows = this.runStatementGetAll(` - SELECT bag_name, accesscontrol - FROM bags - ORDER BY bag_name - `); - return rows; + return this.sqlTiddlerDatabase.listBags(); }; SqlTiddlerStore.prototype.createBag = function(bagname) { - // Run the queries - this.runStatement(` - INSERT OR IGNORE INTO bags (bag_name, accesscontrol) - VALUES ($bag_name, '') - `,{ - bag_name: bagname - }); - this.runStatement(` - UPDATE bags - SET accesscontrol = $accesscontrol - WHERE bag_name = $bag_name - `,{ - bag_name: bagname, - accesscontrol: "[some access control stuff]" - }); + this.sqlTiddlerDatabase.createBag(bagname); this.saveEntityStateTiddler({ title: "bags/" + bagname, "bag-name": bagname, @@ -170,40 +79,11 @@ SqlTiddlerStore.prototype.createBag = function(bagname) { }; SqlTiddlerStore.prototype.listRecipes = function() { - const rows = this.runStatementGetAll(` - SELECT recipe_name - FROM recipes - ORDER BY recipe_name - `); - return rows; + return this.sqlTiddlerDatabase.listRecipes(); }; SqlTiddlerStore.prototype.createRecipe = function(recipename,bagnames) { - // Run the queries - this.runStatement(` - -- Create the entry in the recipes table if required - INSERT OR IGNORE INTO recipes (recipe_name) - VALUES ($recipe_name) - `,{ - recipe_name: recipename - }); - this.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: recipename - }); - this.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: recipename, - bag_names: JSON.stringify(bagnames) - }); + this.sqlTiddlerDatabase.createRecipe(recipename,bagnames); this.saveEntityStateTiddler({ title: "recipes/" + recipename, "recipe-name": recipename, @@ -216,231 +96,53 @@ SqlTiddlerStore.prototype.createRecipe = function(recipename,bagnames) { Returns {tiddler_id:} */ SqlTiddlerStore.prototype.saveBagTiddler = function(tiddlerFields,bagname) { - // Update the tiddlers table - var info = this.runStatement(` - INSERT OR REPLACE INTO tiddlers (bag_id, title) - VALUES ( - (SELECT bag_id FROM bags WHERE bag_name = $bag_name), - $title - ) - `,{ - title: tiddlerFields.title, - bag_name: bagname - }); - // Update the fields table - this.runStatement(` - INSERT OR REPLACE INTO fields (tiddler_id, field_name, field_value) - SELECT - t.tiddler_id, - json_each.key AS field_name, - json_each.value AS field_value - FROM ( - SELECT tiddler_id - FROM tiddlers - WHERE bag_id = ( - SELECT bag_id - FROM bags - WHERE bag_name = $bag_name - ) AND title = $title - ) AS t - JOIN json_each($field_values) AS json_each - `,{ - title: tiddlerFields.title, - bag_name: bagname, - field_values: JSON.stringify(Object.assign({},tiddlerFields,{title: undefined})) - }); - return { - tiddler_id: info.lastInsertRowid - } + return this.sqlTiddlerDatabase.saveBagTiddler(tiddlerFields,bagname); }; /* Returns {tiddler_id:,bag_name:} */ SqlTiddlerStore.prototype.saveRecipeTiddler = function(tiddlerFields,recipename) { - // Find the topmost bag in the recipe - var row = this.runStatementGet(` - SELECT b.bag_name - FROM bags AS b - JOIN ( - SELECT rb.bag_id - FROM recipe_bags AS rb - WHERE rb.recipe_id = ( - SELECT recipe_id - FROM recipes - WHERE recipe_name = $recipe_name - ) - ORDER BY rb.position DESC - LIMIT 1 - ) AS selected_bag - ON b.bag_id = selected_bag.bag_id - `,{ - recipe_name: recipename - }); - // Save the tiddler to the topmost bag - var info = this.saveBagTiddler(tiddlerFields,row.bag_name); - return { - tiddler_id: info.tiddler_id, - bag_name: row.bag_name - }; + return this.sqlTiddlerDatabase.saveRecipeTiddler(tiddlerFields,recipename); }; SqlTiddlerStore.prototype.deleteTiddler = function(title,bagname) { - // Run the queries - this.runStatement(` - DELETE FROM fields - WHERE tiddler_id IN ( - SELECT t.tiddler_id - FROM tiddlers AS t - 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: bagname - }); - this.runStatement(` - DELETE FROM tiddlers - WHERE bag_id = ( - SELECT bag_id - FROM bags - WHERE bag_name = $bag_name - ) AND title = $title - `,{ - title: title, - bag_name: bagname - }); + this.sqlTiddlerDatabase.deleteTiddler(title,bagname); }; /* returns {tiddler_id:,tiddler:} */ SqlTiddlerStore.prototype.getBagTiddler = function(title,bagname) { - const rows = this.runStatementGetAll(` - SELECT field_name, field_value, tiddler_id - FROM fields - WHERE tiddler_id = ( - SELECT t.tiddler_id - 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 - ) - `,{ - title: title, - bag_name: bagname - }); - if(rows.length === 0) { - return null; - } else { - return { - tiddler_id: rows[0].tiddler_id, - tiddler: rows.reduce((accumulator,value) => { - accumulator[value["field_name"]] = value.field_value; - return accumulator; - },{title: title}) - }; - } + return this.sqlTiddlerDatabase.getBagTiddler(title,bagname); }; /* Returns {bag_name:, tiddler: {fields}, tiddler_id:} */ SqlTiddlerStore.prototype.getRecipeTiddler = function(title,recipename) { - const rowTiddlerId = this.runStatementGet(` - SELECT t.tiddler_id, b.bag_name - FROM bags AS b - INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id - INNER JOIN recipes AS r ON rb.recipe_id = r.recipe_id - INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id - WHERE r.recipe_name = $recipe_name - AND t.title = $title - ORDER BY rb.position DESC - LIMIT 1 - `,{ - title: title, - recipe_name: recipename - }); - if(!rowTiddlerId) { - return null; - } - // Get the fields - const rows = this.runStatementGetAll(` - SELECT field_name, field_value - FROM fields - WHERE tiddler_id = $tiddler_id - `,{ - tiddler_id: rowTiddlerId.tiddler_id, - recipe_name: recipename - }); - return { - bag_name: rowTiddlerId.bag_name, - tiddler_id: rowTiddlerId.tiddler_id, - tiddler: rows.reduce((accumulator,value) => { - accumulator[value["field_name"]] = value.field_value; - return accumulator; - },{title: title}) - }; + return this.sqlTiddlerDatabase.getRecipeTiddler(title,recipename); }; /* Get the titles of the tiddlers in a bag. Returns an empty array for bags that do not exist */ SqlTiddlerStore.prototype.getBagTiddlers = function(bagname) { - const rows = this.runStatementGetAll(` - SELECT DISTINCT title - FROM tiddlers - WHERE bag_id IN ( - SELECT bag_id - FROM bags - WHERE bag_name = $bag_name - ) - ORDER BY title ASC - `,{ - bag_name: bagname - }); - return rows.map(value => value.title); + return this.sqlTiddlerDatabase.getBagTiddlers(bagname); }; /* -Get the titles of the tiddlers in a recipe. Returns an empty array for recipes that do not exist +Get the titles of the tiddlers in a recipe as {title:,bag_name:}. Returns an empty array for recipes that do not exist */ SqlTiddlerStore.prototype.getRecipeTiddlers = function(recipename) { - const rows = this.runStatementGetAll(` - SELECT title, bag_name - FROM ( - SELECT t.title, 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 - INNER JOIN recipes AS r ON rb.recipe_id = r.recipe_id - INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id - WHERE r.recipe_name = $recipe_name - GROUP BY t.title - ORDER BY t.title - ) - `,{ - recipe_name: recipename - }); - return rows; + return this.sqlTiddlerDatabase.getRecipeTiddlers(recipename); }; /* Get the names of the bags in a recipe. Returns an empty array for recipes that do not exist */ SqlTiddlerStore.prototype.getRecipeBags = function(recipename) { - const rows = this.runStatementGetAll(` - SELECT bags.bag_name - FROM bags - JOIN ( - SELECT rb.bag_id - FROM recipe_bags AS rb - JOIN recipes AS r ON rb.recipe_id = r.recipe_id - WHERE r.recipe_name = $recipe_name - ORDER BY rb.position - ) AS bag_priority ON bags.bag_id = bag_priority.bag_id - `,{ - recipe_name: recipename - }); - return rows.map(value => value.bag_name); + return this.sqlTiddlerDatabase.getRecipeBags(recipename); }; exports.SqlTiddlerStore = SqlTiddlerStore; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-database.js b/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-database.js new file mode 100644 index 00000000000..04f679421e6 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-database.js @@ -0,0 +1,82 @@ +/*\ +title: tests-sql-tiddler-database.js +type: application/javascript +tags: [[$:/tags/test-spec]] + +Tests the SQL tiddler database layer + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +if($tw.node) { + +describe("SQL tiddler store", function() { + // Create and initialise the tiddler store + var SqlTiddlerDatabase = require("$:/plugins/tiddlywiki/multiwikiserver/sql-tiddler-database.js").SqlTiddlerDatabase; + const sqlTiddlerDatabase = new SqlTiddlerDatabase({ + adminWiki: new $tw.Wiki() + }); + sqlTiddlerDatabase.createTables(); + // Create bags and recipes + sqlTiddlerDatabase.createBag("bag-alpha"); + sqlTiddlerDatabase.createBag("bag-beta"); + sqlTiddlerDatabase.createBag("bag-gamma"); + sqlTiddlerDatabase.createRecipe("recipe-rho",["bag-alpha","bag-beta"]); + sqlTiddlerDatabase.createRecipe("recipe-sigma",["bag-alpha","bag-gamma"]); + sqlTiddlerDatabase.createRecipe("recipe-tau",["bag-alpha"]); + sqlTiddlerDatabase.createRecipe("recipe-upsilon",["bag-alpha","bag-gamma","bag-beta"]); + // Tear down + afterAll(function() { + // Close the database + sqlTiddlerDatabase.close(); + }); + // Run tests + it("should save and retrieve tiddlers", function() { + // Save tiddlers + sqlTiddlerDatabase.saveBagTiddler({title: "Another Tiddler",text: "I'm in alpha",tags: "one two three"},"bag-alpha"); + sqlTiddlerDatabase.saveBagTiddler({title: "Hello There",text: "I'm in alpha as well",tags: "one two three"},"bag-alpha"); + sqlTiddlerDatabase.saveBagTiddler({title: "Hello There",text: "I'm in beta",tags: "four five six"},"bag-beta"); + sqlTiddlerDatabase.saveBagTiddler({title: "Hello There",text: "I'm in gamma",tags: "seven eight nine"},"bag-gamma"); + // Verify what we've got + expect(sqlTiddlerDatabase.getRecipeTiddlers("recipe-rho")).toEqual([ + { title: 'Another Tiddler', bag_name: 'bag-alpha' }, + { title: 'Hello There', bag_name: 'bag-beta' } + ]); + expect(sqlTiddlerDatabase.getRecipeTiddlers("recipe-sigma")).toEqual([ + { title: 'Another Tiddler', bag_name: 'bag-alpha' }, + { title: 'Hello There', bag_name: 'bag-gamma' } + ]); + expect(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"}); + // Delete a tiddlers to ensure the underlying tiddler in the recipe shows through + sqlTiddlerDatabase.deleteTiddler("Hello There","bag-beta"); + expect(sqlTiddlerDatabase.getRecipeTiddlers("recipe-rho")).toEqual([ + { title: 'Another Tiddler', bag_name: 'bag-alpha' }, + { title: 'Hello There', bag_name: 'bag-alpha' } + ]); + expect(sqlTiddlerDatabase.getRecipeTiddlers("recipe-sigma")).toEqual([ + { title: 'Another Tiddler', bag_name: 'bag-alpha' }, + { title: 'Hello There', bag_name: 'bag-gamma' } + ]); + expect(sqlTiddlerDatabase.getRecipeTiddler("Hello There","recipe-beta")).toEqual(null); + sqlTiddlerDatabase.deleteTiddler("Another Tiddler","bag-alpha"); + expect(sqlTiddlerDatabase.getRecipeTiddlers("recipe-rho")).toEqual([ { title: 'Hello There', bag_name: 'bag-alpha' } ]); + expect(sqlTiddlerDatabase.getRecipeTiddlers("recipe-sigma")).toEqual([ { title: 'Hello There', bag_name: 'bag-gamma' } ]); + // Save a recipe tiddler + expect(sqlTiddlerDatabase.saveRecipeTiddler({title: "More", text: "None"},"recipe-rho")).toEqual({tiddler_id: 5, bag_name: 'bag-beta'}); + expect(sqlTiddlerDatabase.getRecipeTiddler("More","recipe-rho").tiddler).toEqual({title: "More", text: "None"}); + + }); +}); + +} + +})(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js deleted file mode 100644 index 7523ac0927f..00000000000 --- a/plugins/tiddlywiki/multiwikiserver/modules/tests-sql-tiddler-store.js +++ /dev/null @@ -1,82 +0,0 @@ -/*\ -title: tests-sql-tiddler-store.js -type: application/javascript -tags: [[$:/tags/test-spec]] - -Tests the SQL tiddler store - -\*/ -(function(){ - -/*jslint node: true, browser: true */ -/*global $tw: false */ -"use strict"; - -if($tw.node) { - -describe("SQL tiddler store", function() { - // Create and initialise the tiddler store - var SqlTiddlerStore = require("$:/plugins/tiddlywiki/multiwikiserver/sql-tiddler-store.js").SqlTiddlerStore; - const sqlTiddlerStore = new SqlTiddlerStore({ - adminWiki: new $tw.Wiki() - }); - sqlTiddlerStore.createTables(); - // Create bags and recipes - sqlTiddlerStore.createBag("bag-alpha"); - sqlTiddlerStore.createBag("bag-beta"); - sqlTiddlerStore.createBag("bag-gamma"); - sqlTiddlerStore.createRecipe("recipe-rho",["bag-alpha","bag-beta"]); - sqlTiddlerStore.createRecipe("recipe-sigma",["bag-alpha","bag-gamma"]); - sqlTiddlerStore.createRecipe("recipe-tau",["bag-alpha"]); - sqlTiddlerStore.createRecipe("recipe-upsilon",["bag-alpha","bag-gamma","bag-beta"]); - // Tear down - afterAll(function() { - // Close the database - sqlTiddlerStore.close(); - }); - // Run tests - it("should save and retrieve tiddlers", function() { - // Save tiddlers - sqlTiddlerStore.saveBagTiddler({title: "Another Tiddler",text: "I'm in alpha",tags: "one two three"},"bag-alpha"); - sqlTiddlerStore.saveBagTiddler({title: "Hello There",text: "I'm in alpha as well",tags: "one two three"},"bag-alpha"); - sqlTiddlerStore.saveBagTiddler({title: "Hello There",text: "I'm in beta",tags: "four five six"},"bag-beta"); - sqlTiddlerStore.saveBagTiddler({title: "Hello There",text: "I'm in gamma",tags: "seven eight nine"},"bag-gamma"); - // Verify what we've got - expect(sqlTiddlerStore.getRecipeTiddlers("recipe-rho")).toEqual([ - { title: 'Another Tiddler', bag_name: 'bag-alpha' }, - { title: 'Hello There', bag_name: 'bag-beta' } - ]); - expect(sqlTiddlerStore.getRecipeTiddlers("recipe-sigma")).toEqual([ - { title: 'Another Tiddler', bag_name: 'bag-alpha' }, - { title: 'Hello There', bag_name: 'bag-gamma' } - ]); - expect(sqlTiddlerStore.getRecipeTiddler("Hello There","recipe-rho").tiddler).toEqual({ title: "Hello There", text: "I'm in beta", tags: "four five six" }); - expect(sqlTiddlerStore.getRecipeTiddler("Missing Tiddler","recipe-rho")).toEqual(null); - expect(sqlTiddlerStore.getRecipeTiddler("Another Tiddler","recipe-rho").tiddler).toEqual({ title: "Another Tiddler", text: "I'm in alpha", tags: "one two three" }); - expect(sqlTiddlerStore.getRecipeTiddler("Hello There","recipe-sigma").tiddler).toEqual({ title: "Hello There", text: "I'm in gamma", tags: "seven eight nine" }); - expect(sqlTiddlerStore.getRecipeTiddler("Another Tiddler","recipe-sigma").tiddler).toEqual({ title: "Another Tiddler", text: "I'm in alpha", tags: "one two three" }); - expect(sqlTiddlerStore.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 - sqlTiddlerStore.deleteTiddler("Hello There","bag-beta"); - expect(sqlTiddlerStore.getRecipeTiddlers("recipe-rho")).toEqual([ - { title: 'Another Tiddler', bag_name: 'bag-alpha' }, - { title: 'Hello There', bag_name: 'bag-alpha' } - ]); - expect(sqlTiddlerStore.getRecipeTiddlers("recipe-sigma")).toEqual([ - { title: 'Another Tiddler', bag_name: 'bag-alpha' }, - { title: 'Hello There', bag_name: 'bag-gamma' } - ]); - expect(sqlTiddlerStore.getRecipeTiddler("Hello There","recipe-beta")).toEqual(null); - sqlTiddlerStore.deleteTiddler("Another Tiddler","bag-alpha"); - expect(sqlTiddlerStore.getRecipeTiddlers("recipe-rho")).toEqual([ { title: 'Hello There', bag_name: 'bag-alpha' } ]); - expect(sqlTiddlerStore.getRecipeTiddlers("recipe-sigma")).toEqual([ { title: 'Hello There', bag_name: 'bag-gamma' } ]); - // Save a recipe tiddler - expect(sqlTiddlerStore.saveRecipeTiddler({title: "More", text: "None"},"recipe-rho")).toEqual({tiddler_id: 5, bag_name: 'bag-beta'}); - expect(sqlTiddlerStore.getRecipeTiddler("More","recipe-rho").tiddler).toEqual({title: "More", text: "None"}); - - }); -}); - -} - -})(); From e343eccdc3167968143dbf9fc20e2a927c316b10 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 23 Jan 2024 10:51:12 +0000 Subject: [PATCH 039/213] Refactor _canonical_uri handling out of route handlers --- .../modules/route-get-bag-tiddler.js | 2 - .../modules/route-get-recipe-tiddler.js | 2 - .../modules/route-get-recipe-tiddlers-json.js | 2 +- .../modules/route-get-recipe.js | 15 +------ .../modules/sql-tiddler-store.js | 45 ++++++++++++++++++- 5 files changed, 45 insertions(+), 21 deletions(-) diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-bag-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-bag-tiddler.js index e3ce8ef51f0..ac9c49a138d 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-get-bag-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-bag-tiddler.js @@ -39,8 +39,6 @@ exports.handler = function(request,response,state) { tiddlerFields.fields[name] = value; } }); - tiddlerFields.revision = "" + result.tiddler_id; - tiddlerFields.bag = bag_name; tiddlerFields.type = tiddlerFields.type || "text/vnd.tiddlywiki"; state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify(tiddlerFields),"utf8"); } else { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddler.js index b34075759ed..68b5c4891ac 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddler.js @@ -39,8 +39,6 @@ exports.handler = function(request,response,state) { tiddlerFields.fields[name] = value; } }); - tiddlerFields.revision = "" + tiddlerInfo.tiddler_id; - tiddlerFields.bag = tiddlerInfo.bag_name; tiddlerFields.type = tiddlerFields.type || "text/vnd.tiddlywiki"; state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify(tiddlerFields),"utf8"); } else { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddlers-json.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddlers-json.js index f8d81173b93..f90d9d111af 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddlers-json.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe-tiddlers-json.js @@ -29,7 +29,7 @@ exports.handler = function(request,response,state) { var tiddlers = []; $tw.utils.each(recipeTiddlers,function(recipeTiddlerInfo) { var tiddlerInfo = $tw.sqlTiddlerStore.getRecipeTiddler(recipeTiddlerInfo.title,recipe_name); - tiddlers.push(Object.assign({},tiddlerInfo.tiddler,{text: undefined, revision: "" + tiddlerInfo.tiddler_id, bag: recipeTiddlerInfo.bag_name})); + tiddlers.push(Object.assign({},tiddlerInfo.tiddler,{text: undefined})); }); var text = JSON.stringify(tiddlers); state.sendResponse(200,{"Content-Type": "application/json"},text,"utf8"); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe.js b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe.js index d77cbf5f77a..1857ed1d088 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/route-get-recipe.js @@ -50,20 +50,7 @@ exports.handler = function(request,response,state) { } response.write(template.substring(0,markerPos + marker.length)); $tw.utils.each(recipeTiddlers,function(recipeTiddlerInfo) { - var tiddlerInfo = $tw.sqlTiddlerStore.getRecipeTiddler(recipeTiddlerInfo.title,recipe_name); - if((tiddlerInfo.tiddler.text || "").length > 10 * 1024 * 1024) { - response.write(JSON.stringify(Object.assign({},tiddlerInfo.tiddler,{ - revision: "" + tiddlerInfo.tiddler_id, - bag: recipeTiddlerInfo.bag_name, - text: undefined, - _canonical_uri: `/wiki/${recipe_name}/recipes/${recipe_name}/tiddlers/${title}` - }))); - } else { - response.write(JSON.stringify(Object.assign({},tiddlerInfo.tiddler,{ - revision: "" + tiddlerInfo.tiddler_id, - bag: recipeTiddlerInfo.bag_name - }))); - } + response.write(JSON.stringify($tw.sqlTiddlerStore.getRecipeTiddler(recipeTiddlerInfo.title,recipe_name).tiddler)); response.write(",") }); response.write(JSON.stringify({title: "$:/config/tiddlyweb/host",text: "$protocol$//$host$$pathname$/"})); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js index 79ca9c40db2..98b83ba69fb 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js @@ -8,6 +8,7 @@ Higher level functions to perform basic tiddler operations with a sqlite3 databa This class is largely a wrapper for the sql-tiddler-database.js class, adding the following functionality: * Synchronising bag and recipe names to the admin wiki +* Handling _canonical_uri tiddlers \*/ @@ -61,6 +62,30 @@ SqlTiddlerStore.prototype.updateAdminWiki = function() { } }; +/* +Given tiddler fields, tiddler_id and a bagname, return the tiddler fields after the following process: +- If the text field is over a threshold, modify the tiddler to use _canonical_uri +- 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,recipe_name) { + if((tiddlerFields.text || "").length > 10 * 1024 * 1024) { + return Object.assign({},tiddlerFields,{ + revision: "" + tiddler_id, + bag: bag_name, + text: undefined, + _canonical_uri: recipe_name + ? `/wiki/${recipe_name}/recipes/${recipe_name}/tiddlers/${title}` + : `/wiki/${bag_name}/bags/${bag_name}/tiddlers/${title}` + }); + } else { + return Object.assign({},tiddlerFields,{ + revision: "" + tiddler_id, + bag: bag_name + }); + } +}; + SqlTiddlerStore.prototype.logTables = function() { this.sqlTiddlerDatabase.logTables(); }; @@ -114,14 +139,30 @@ SqlTiddlerStore.prototype.deleteTiddler = function(title,bagname) { returns {tiddler_id:,tiddler:} */ SqlTiddlerStore.prototype.getBagTiddler = function(title,bagname) { - return this.sqlTiddlerDatabase.getBagTiddler(title,bagname); + var tiddlerInfo = this.sqlTiddlerDatabase.getBagTiddler(title,bagname); + if(tiddlerInfo) { + return Object.assign( + {}, + tiddlerInfo, + { + tiddler: this.processOutgoingTiddler(tiddlerInfo.tiddler,tiddlerInfo.tiddler_id,bagname,null) + }); + } else { + return null; + } }; /* Returns {bag_name:, tiddler: {fields}, tiddler_id:} */ SqlTiddlerStore.prototype.getRecipeTiddler = function(title,recipename) { - return this.sqlTiddlerDatabase.getRecipeTiddler(title,recipename); + var tiddlerInfo = this.sqlTiddlerDatabase.getRecipeTiddler(title,recipename); + return Object.assign( + {}, + tiddlerInfo, + { + tiddler: this.processOutgoingTiddler(tiddlerInfo.tiddler,tiddlerInfo.tiddler_id,tiddlerInfo.bag_name,recipename) + }); }; /* From c1312100aa1ea655b2d8118a2cf68ea6332584b2 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 23 Jan 2024 12:52:40 +0000 Subject: [PATCH 040/213] Admin UI styling --- .../multiwikiserver/admin-ui/AdminLayout.tid | 19 ++-- .../MultiWikiServer Administration.tid | 65 ++++++++++---- .../multiwikiserver/admin-ui/Styles.tid | 90 ++++++++++++++++++- .../modules/sql-tiddler-store.js | 8 +- 4 files changed, 151 insertions(+), 31 deletions(-) diff --git a/plugins/tiddlywiki/multiwikiserver/admin-ui/AdminLayout.tid b/plugins/tiddlywiki/multiwikiserver/admin-ui/AdminLayout.tid index 5b36335c99b..7e101559932 100644 --- a/plugins/tiddlywiki/multiwikiserver/admin-ui/AdminLayout.tid +++ b/plugins/tiddlywiki/multiwikiserver/admin-ui/AdminLayout.tid @@ -5,14 +5,15 @@ description: Admin Layout icon: $:/favicon.ico \import [subfilter{$:/core/config/GlobalImportFilter}] +
      - -
      TiddlyWiki5
      -{{MultiWikiServer Administration}} -
      -<$button> -<$action-setfield $tiddler="$:/layout" text="$:/core/ui/PageTemplate"/> -Switch to TiddlyWiki default user interface - -
      + +
      TiddlyWiki5
      + {{MultiWikiServer Administration}} +
      + <$button> + <$action-setfield $tiddler="$:/layout" text="$:/core/ui/PageTemplate"/> + Switch to TiddlyWiki default user interface + +
      diff --git a/plugins/tiddlywiki/multiwikiserver/admin-ui/MultiWikiServer Administration.tid b/plugins/tiddlywiki/multiwikiserver/admin-ui/MultiWikiServer Administration.tid index ca3eafe66cc..9352ffa7462 100644 --- a/plugins/tiddlywiki/multiwikiserver/admin-ui/MultiWikiServer Administration.tid +++ b/plugins/tiddlywiki/multiwikiserver/admin-ui/MultiWikiServer Administration.tid @@ -46,22 +46,54 @@ title: MultiWikiServer Administration <$text text="Create a new recipe:"/><$edit-text tiddler="$:/state/NewRecipeName" tag="input"/> \end createRecipeButton + +\procedure bagPill(element-tag:"span",is-topmost:"no") +\whitespace trim +<$genesis $type=<> class={{{ mws-bag-pill [match[yes]then[mws-bag-pill-topmost]] +[join[ ]] }}}> + <$image source=`/wiki/${ [{!!bag-name}encodeuricomponent[]] }$/bags/${ [{!!bag-name}encodeuricomponent[]] }$/tiddlers/%24%3A%2Ffavicon.ico` class="mws-favicon-small"/> + + <$text text={{!!bag-name}}/> + + +\end + + +\procedure wikiCard() +\whitespace trim + +
      + +
      +
      +
      + <$text text={{!!recipe-name}}/> +
      +
      +
        + <$list filter="[list]" counter="counter"> + <$transclude $variable="bagPill" is-topmost={{{ [match[yes]] }}} element-tag="li"/> + +
      +
      +
      + DDDDDD +
      +
      + Additional Details +
      +
      +
      +\end +
      -

      Recipes

      -
        +

        Wikis

        +

        + These are the wikis available on this server. Click on a wiki to visit it in a new browser tab. +

        + @@ -72,13 +104,10 @@ title: MultiWikiServer Administration Higher numbered bags take priority if a tiddler with the same title is in more than one bag

      Bags

      -
        + diff --git a/plugins/tiddlywiki/multiwikiserver/admin-ui/Styles.tid b/plugins/tiddlywiki/multiwikiserver/admin-ui/Styles.tid index 70ea17ed0ed..11a5bab096a 100644 --- a/plugins/tiddlywiki/multiwikiserver/admin-ui/Styles.tid +++ b/plugins/tiddlywiki/multiwikiserver/admin-ui/Styles.tid @@ -11,7 +11,93 @@ Styles specific to the full screen layout padding: 1rem; } + +.mws-wiki-card { + display: flex; + margin: 1em 0; + width: 100%; + text-decoration: none; + color: <>; + background: <>; + border-radius: 0.28571429rem; + box-shadow: 0 1px 3px 0 #d4d4d5, 0 0 0 1px #d4d4d5; + padding: 0.5em 0.5em 0.5em 1em; +} + + +.mws-wiki-card:hover { + background: <>; + color: <>; +} + +.mws-wiki-card-image { + display: flex; + align-items: center; +} + +.mws-wiki-card-content { + padding-left: 1em; +} + +.mws-wiki-card-header { + font-size: 1.3em; + font-weight: bold; + margin: 0 0 0.25em 0; +} + +.mws-wiki-card-meta { + color: <>; +} + +.mws-wiki-card-description { + +} + +.mws-wiki-card-extra { + +} + +.mws-vertical-list { + list-style: none; + padding: 0; +} + +.mws-horizontal-list { + list-style: none; + padding: 0; +} + +.mws-horizontal-list > li { + display: inline-block; +} + +.mws-bag-pill { + background: <>; + color: <>; + margin-right: 0.5em; + border-radius: 0.25em; + padding: 0 0.25em; +} + +.mws-bag-pill-topmost { + background: <>; +} + +.mws-bag-pill .mws-bag-pill-label { + margin-left: 0.5em; +} + +.mws-favicon.tc-image-error, .mws-favicon-small.tc-image-error { + visibility: hidden; +} + .mws-favicon { - max-width: 2em; - max-height: 2em; + max-width: 4em; + max-height: 4em; +} + +.mws-favicon-small { + vertical-align: text-bottom; + max-width: 1em; + max-height: 1em; } \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js index 98b83ba69fb..11aa8284840 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js @@ -57,7 +57,9 @@ SqlTiddlerStore.prototype.updateAdminWiki = function() { title: "recipes/" + recipeInfo.recipe_name, "recipe-name": recipeInfo.recipe_name, text: "", - list: $tw.utils.stringifyList(this.getRecipeBags(recipeInfo.recipe_name)) + list: $tw.utils.stringifyList(this.getRecipeBags(recipeInfo.recipe_name).map(bag_name => { + return this.entityStateTiddlerPrefix + "bags/" + bag_name; + })) }); } }; @@ -113,7 +115,9 @@ SqlTiddlerStore.prototype.createRecipe = function(recipename,bagnames) { title: "recipes/" + recipename, "recipe-name": recipename, text: "", - list: $tw.utils.stringifyList(bagnames) + list: $tw.utils.stringifyList(bagnames.map(bag_name => { + return this.entityStateTiddlerPrefix + "bags/" + bag_name; + })) }); }; From 239ace0c074d54302144d63ae5d6cfc4a28a1605 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 23 Jan 2024 12:53:06 +0000 Subject: [PATCH 041/213] Avoid clients of sqlTiddlerStore having to call updateAdminWIki() explicitly --- plugins/tiddlywiki/multiwikiserver/modules/init.js | 1 - plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/tiddlywiki/multiwikiserver/modules/init.js b/plugins/tiddlywiki/multiwikiserver/modules/init.js index 1fe40007bdf..4e747336fc9 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/init.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/init.js @@ -41,7 +41,6 @@ exports.startup = function() { $tw.sqlTiddlerStore = new SqlTiddlerStore({ databasePath: databasePath }); - $tw.sqlTiddlerStore.updateAdminWiki(); // Create bags and recipes $tw.sqlTiddlerStore.createBag("bag-alpha"); $tw.sqlTiddlerStore.createBag("bag-beta"); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js index 11aa8284840..22c207dc63e 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/sql-tiddler-store.js @@ -31,6 +31,7 @@ function SqlTiddlerStore(options) { databasePath: this.databasePath }); this.sqlTiddlerDatabase.createTables(); + this.updateAdminWiki(); } SqlTiddlerStore.prototype.close = function() { From 138c7f2665830ce08a1778c54bbcb74a1a8f2a9e Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 23 Jan 2024 14:29:08 +0000 Subject: [PATCH 042/213] Don't use the filesystem plugin Otherwise changes to _multiwikiserver/ tiddlers are saved back to the file system Perhaps it would work better to configure the wiki to sync state tiddlers to the browser. --- editions/multiwikiserver/tiddlywiki.info | 1 - 1 file changed, 1 deletion(-) diff --git a/editions/multiwikiserver/tiddlywiki.info b/editions/multiwikiserver/tiddlywiki.info index 95538232953..513a5208184 100644 --- a/editions/multiwikiserver/tiddlywiki.info +++ b/editions/multiwikiserver/tiddlywiki.info @@ -2,7 +2,6 @@ "description": "Multiple wiki client-server edition", "plugins": [ "tiddlywiki/tiddlyweb", - "tiddlywiki/filesystem", "tiddlywiki/highlight", "tiddlywiki/multiwikiserver" ], From f6d647894486defb7ab856da0ce1901fc0ed8750 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 23 Jan 2024 14:29:50 +0000 Subject: [PATCH 043/213] Add support for recipe descriptions --- .../MultiWikiServer Administration.tid | 35 ++++++++++--------- .../multiwikiserver/admin-ui/Styles.tid | 19 ++++++---- .../multiwikiserver/modules/init.js | 8 ++--- .../modules/route-put-recipe-tiddler.js | 12 ++++--- .../modules/sql-tiddler-database.js | 22 ++++++++---- .../modules/sql-tiddler-store.js | 28 +++++++++------ 6 files changed, 76 insertions(+), 48 deletions(-) diff --git a/plugins/tiddlywiki/multiwikiserver/admin-ui/MultiWikiServer Administration.tid b/plugins/tiddlywiki/multiwikiserver/admin-ui/MultiWikiServer Administration.tid index 9352ffa7462..46a603907d8 100644 --- a/plugins/tiddlywiki/multiwikiserver/admin-ui/MultiWikiServer Administration.tid +++ b/plugins/tiddlywiki/multiwikiserver/admin-ui/MultiWikiServer Administration.tid @@ -50,10 +50,12 @@ title: MultiWikiServer Administration \procedure bagPill(element-tag:"span",is-topmost:"no") \whitespace trim <$genesis $type=<> class={{{ mws-bag-pill [match[yes]then[mws-bag-pill-topmost]] +[join[ ]] }}}> - <$image source=`/wiki/${ [{!!bag-name}encodeuricomponent[]] }$/bags/${ [{!!bag-name}encodeuricomponent[]] }$/tiddlers/%24%3A%2Ffavicon.ico` class="mws-favicon-small"/> - - <$text text={{!!bag-name}}/> - + + <$image source=`/wiki/${ [{!!bag-name}encodeuricomponent[]] }$/bags/${ [{!!bag-name}encodeuricomponent[]] }$/tiddlers/%24%3A%2Ffavicon.ico` class="mws-favicon-small"/> + + <$text text={{!!bag-name}}/> + + \end @@ -62,24 +64,25 @@ title: MultiWikiServer Administration \whitespace trim
        - + <$image source=`/wiki/${ [{!!recipe-name}encodeuricomponent[]] }$/recipes/${ [{!!recipe-name}encodeuricomponent[]] }$/tiddlers/%24%3A%2Ffavicon.ico` class="mws-favicon"/>
        <$text text={{!!recipe-name}}/>
        -
          - <$list filter="[list]" counter="counter"> - <$transclude $variable="bagPill" is-topmost={{{ [match[yes]] }}} element-tag="li"/> - -
        + <%if [list] %> +
          + <$list filter="[list]" counter="counter"> + <$transclude $variable="bagPill" is-topmost={{{ [match[yes]] }}} element-tag="li"/> + +
        + <%else%> + (no bags defined) + <%endif%>
        - DDDDDD -
        -
        - Additional Details + <$transclude $tiddler=<> $mode="inline"/>
        @@ -91,7 +94,7 @@ title: MultiWikiServer Administration These are the wikis available on this server. Click on a wiki to visit it in a new browser tab.

          - <$list filter="[prefix[$:/state/multiwikiserver/recipes/]]"> + <$list filter="[prefix[_multiwikiserver/recipes/]]">
        • <>
        • @@ -105,7 +108,7 @@ title: MultiWikiServer Administration

Bags