mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2025-01-05 23:10:28 +00:00
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
This commit is contained in:
parent
299781bdba
commit
a980390870
@ -14,8 +14,7 @@
|
||||
"tiddlywiki/dynannotate",
|
||||
"tiddlywiki/codemirror",
|
||||
"tiddlywiki/menubar",
|
||||
"tiddlywiki/jszip",
|
||||
"tiddlywiki/multiwikiserver"
|
||||
"tiddlywiki/jszip"
|
||||
],
|
||||
"themes": [
|
||||
"tiddlywiki/vanilla",
|
||||
|
@ -7,7 +7,10 @@
|
||||
"tiddlywiki/evernote",
|
||||
"tiddlywiki/internals",
|
||||
"tiddlywiki/menubar",
|
||||
"tiddlywiki/qrcode"
|
||||
"tiddlywiki/qrcode",
|
||||
"tiddlywiki/tiddlyweb",
|
||||
"tiddlywiki/filesystem",
|
||||
"tiddlywiki/multiwikiserver"
|
||||
],
|
||||
"themes": [
|
||||
"tiddlywiki/vanilla",
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
}());
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
}());
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
}());
|
@ -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();
|
||||
|
||||
};
|
||||
|
||||
}());
|
@ -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) {
|
@ -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();
|
||||
|
||||
};
|
||||
|
||||
}());
|
@ -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;
|
||||
|
||||
})();
|
@ -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"});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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<substitutions.length; t++) {
|
||||
var s = substitutions[t];
|
||||
|
Loading…
Reference in New Issue
Block a user