2024-01-02 14:39:14 +00:00
|
|
|
/*\
|
|
|
|
title: $:/plugins/tiddlywiki/multiwikiserver/sql-tiddler-store.js
|
|
|
|
type: application/javascript
|
|
|
|
module-type: library
|
|
|
|
|
2024-01-22 22:08:55 +00:00
|
|
|
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:
|
|
|
|
|
2024-01-24 22:24:24 +00:00
|
|
|
* Validating requests (eg bag and recipe name constraints)
|
2024-01-22 22:08:55 +00:00
|
|
|
* Synchronising bag and recipe names to the admin wiki
|
2024-01-23 10:51:12 +00:00
|
|
|
* Handling _canonical_uri tiddlers
|
2024-01-02 14:39:14 +00:00
|
|
|
|
|
|
|
\*/
|
|
|
|
|
|
|
|
(function() {
|
|
|
|
|
2024-01-05 10:58:07 +00:00
|
|
|
/*
|
|
|
|
Create a tiddler store. Options include:
|
|
|
|
|
|
|
|
databasePath - path to the database file (can be ":memory:" to get a temporary database)
|
2024-01-17 22:41:41 +00:00
|
|
|
adminWiki - reference to $tw.Wiki object into which entity state tiddlers should be saved
|
2024-01-05 10:58:07 +00:00
|
|
|
*/
|
2024-01-02 14:39:14 +00:00
|
|
|
function SqlTiddlerStore(options) {
|
2024-01-05 10:58:07 +00:00
|
|
|
options = options || {};
|
2024-01-17 22:41:41 +00:00
|
|
|
this.adminWiki = options.adminWiki || $tw.wiki;
|
2024-01-24 10:56:23 +00:00
|
|
|
this.entityStateTiddlerPrefix = "$:/state/MultiWikiServer/";
|
2024-01-17 22:41:41 +00:00
|
|
|
// Create the database
|
2024-01-22 22:08:55 +00:00
|
|
|
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();
|
2024-01-23 12:53:06 +00:00
|
|
|
this.updateAdminWiki();
|
2024-01-02 14:39:14 +00:00
|
|
|
}
|
|
|
|
|
2024-01-24 22:24:24 +00:00
|
|
|
/*
|
|
|
|
Returns null if a bag/recipe name is valid, or a string error message if not
|
|
|
|
*/
|
|
|
|
SqlTiddlerStore.prototype.validateItemName = function(name) {
|
|
|
|
if(typeof name !== "string") {
|
|
|
|
return "Not a valid string";
|
|
|
|
}
|
|
|
|
if(name.length > 256) {
|
|
|
|
return "Too long";
|
|
|
|
}
|
|
|
|
if(!(/^[^\s\u00A0\x00-\x1F\x7F~`!@#$%^&*()+={}\[\];:\'\"<>.,\/\\\?]+$/g.test(name))) {
|
|
|
|
return "Invalid character(s)";
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
Returns null if the argument is an array of valid bag/recipe names, or a string error message if not
|
|
|
|
*/
|
|
|
|
SqlTiddlerStore.prototype.validateItemNames = function(names) {
|
|
|
|
if(!$tw.utils.isArray(names)) {
|
|
|
|
return "Not a valid array";
|
|
|
|
}
|
|
|
|
var errors = [];
|
|
|
|
for(const name of names) {
|
|
|
|
const result = this.validateItemName(name);
|
|
|
|
if(result) {
|
|
|
|
errors.push(result);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if(errors.length === 0) {
|
|
|
|
return null;
|
|
|
|
} else {
|
|
|
|
return errors.join("\n");
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2024-01-02 14:39:14 +00:00
|
|
|
SqlTiddlerStore.prototype.close = function() {
|
2024-01-22 22:08:55 +00:00
|
|
|
this.sqlTiddlerDatabase.close();
|
|
|
|
this.sqlTiddlerDatabase = undefined;
|
2024-01-02 14:39:14 +00:00
|
|
|
};
|
|
|
|
|
2024-01-17 22:41:41 +00:00
|
|
|
SqlTiddlerStore.prototype.saveEntityStateTiddler = function(tiddler) {
|
|
|
|
this.adminWiki.addTiddler(new $tw.Tiddler(tiddler,{title: this.entityStateTiddlerPrefix + tiddler.title}));
|
|
|
|
};
|
|
|
|
|
2024-01-19 11:03:27 +00:00
|
|
|
SqlTiddlerStore.prototype.updateAdminWiki = function() {
|
|
|
|
// Update bags
|
|
|
|
for(const bagInfo of this.listBags()) {
|
|
|
|
this.saveEntityStateTiddler({
|
|
|
|
title: "bags/" + bagInfo.bag_name,
|
|
|
|
"bag-name": bagInfo.bag_name,
|
2024-01-24 22:24:24 +00:00
|
|
|
text: bagInfo.description
|
2024-01-19 11:03:27 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
// Update recipes
|
|
|
|
for(const recipeInfo of this.listRecipes()) {
|
|
|
|
this.saveEntityStateTiddler({
|
|
|
|
title: "recipes/" + recipeInfo.recipe_name,
|
|
|
|
"recipe-name": recipeInfo.recipe_name,
|
2024-01-23 14:29:50 +00:00
|
|
|
text: recipeInfo.description,
|
2024-01-23 12:52:40 +00:00
|
|
|
list: $tw.utils.stringifyList(this.getRecipeBags(recipeInfo.recipe_name).map(bag_name => {
|
|
|
|
return this.entityStateTiddlerPrefix + "bags/" + bag_name;
|
|
|
|
}))
|
2024-01-19 11:03:27 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2024-01-23 10:51:12 +00:00
|
|
|
/*
|
|
|
|
Given tiddler fields, tiddler_id and a bagname, return the tiddler fields after the following process:
|
|
|
|
- Apply the tiddler_id as the revision field
|
|
|
|
- Apply the bag_name as the bag field
|
|
|
|
*/
|
2024-01-26 15:48:39 +00:00
|
|
|
SqlTiddlerStore.prototype.processOutgoingTiddler = function(tiddlerFields,tiddler_id,bag_name) {
|
|
|
|
return Object.assign({},tiddlerFields,{
|
|
|
|
revision: "" + tiddler_id,
|
|
|
|
bag: bag_name
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
Given tiddler fields and a bagname or a recipename, if the text field is over a threshold, modify
|
|
|
|
the tiddler to use _canonical_uri, otherwise return the tiddler unmodified
|
|
|
|
*/
|
|
|
|
SqlTiddlerStore.prototype.processCanonicalUriTiddler = function(tiddlerFields,bag_name,recipe_name) {
|
2024-01-23 10:51:12 +00:00
|
|
|
if((tiddlerFields.text || "").length > 10 * 1024 * 1024) {
|
|
|
|
return Object.assign({},tiddlerFields,{
|
|
|
|
text: undefined,
|
|
|
|
_canonical_uri: recipe_name
|
2024-01-26 15:01:07 +00:00
|
|
|
? `/wiki/${recipe_name}/recipes/${recipe_name}/tiddlers/${tiddlerFields.title}`
|
|
|
|
: `/wiki/${bag_name}/bags/${bag_name}/tiddlers/${tiddlerFields.title}`
|
2024-01-23 10:51:12 +00:00
|
|
|
});
|
|
|
|
} else {
|
2024-01-26 15:48:39 +00:00
|
|
|
return tiddlerFields;
|
2024-01-23 10:51:12 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2024-01-23 16:53:12 +00:00
|
|
|
|
|
|
|
SqlTiddlerStore.prototype.saveTiddlersFromPath = function(tiddler_files_path,bag_name) {
|
|
|
|
// Clear out the bag
|
|
|
|
this.deleteAllTiddlersInBag(bag_name);
|
|
|
|
// Get the tiddlers
|
|
|
|
var path = require("path");
|
|
|
|
var tiddlersFromPath = $tw.loadTiddlersFromPath(path.resolve($tw.boot.corePath,$tw.config.editionsPath,tiddler_files_path));
|
|
|
|
// Save the tiddlers
|
|
|
|
for(const tiddlersFromFile of tiddlersFromPath) {
|
|
|
|
for(const tiddler of tiddlersFromFile.tiddlers) {
|
|
|
|
this.saveBagTiddler(tiddler,bag_name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2024-01-02 14:39:14 +00:00
|
|
|
SqlTiddlerStore.prototype.logTables = function() {
|
2024-01-22 22:08:55 +00:00
|
|
|
this.sqlTiddlerDatabase.logTables();
|
2024-01-02 14:39:14 +00:00
|
|
|
};
|
|
|
|
|
2024-01-17 22:41:41 +00:00
|
|
|
SqlTiddlerStore.prototype.listBags = function() {
|
2024-01-22 22:08:55 +00:00
|
|
|
return this.sqlTiddlerDatabase.listBags();
|
2024-01-17 22:41:41 +00:00
|
|
|
};
|
|
|
|
|
2024-01-24 22:24:24 +00:00
|
|
|
SqlTiddlerStore.prototype.createBag = function(bagname,description) {
|
|
|
|
console.log(`create bag method for ${bagname} with ${description}`)
|
|
|
|
console.log(`validation results are ${this.validateItemName(bagname)}`)
|
|
|
|
const validationBagName = this.validateItemName(bagname);
|
|
|
|
if(validationBagName) {
|
|
|
|
return {message: validationBagName};
|
|
|
|
}
|
|
|
|
this.sqlTiddlerDatabase.createBag(bagname,description);
|
2024-01-17 22:41:41 +00:00
|
|
|
this.saveEntityStateTiddler({
|
|
|
|
title: "bags/" + bagname,
|
|
|
|
"bag-name": bagname,
|
2024-01-24 22:24:24 +00:00
|
|
|
text: description
|
2024-01-17 22:41:41 +00:00
|
|
|
});
|
2024-01-24 22:24:24 +00:00
|
|
|
return null;
|
2024-01-17 22:41:41 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
SqlTiddlerStore.prototype.listRecipes = function() {
|
2024-01-22 22:08:55 +00:00
|
|
|
return this.sqlTiddlerDatabase.listRecipes();
|
2024-01-02 14:39:14 +00:00
|
|
|
};
|
|
|
|
|
2024-01-24 22:24:24 +00:00
|
|
|
/*
|
|
|
|
Returns null on success, or {message:} on error
|
|
|
|
*/
|
2024-01-23 14:29:50 +00:00
|
|
|
SqlTiddlerStore.prototype.createRecipe = function(recipename,bagnames,description) {
|
2024-01-24 22:24:24 +00:00
|
|
|
console.log(`create recipe method for ${recipename} with ${JSON.stringify(bagnames)}`)
|
|
|
|
console.log(`validation results are ${this.validateItemName(recipename)} and ${this.validateItemNames(bagnames)}`)
|
2024-01-23 14:29:50 +00:00
|
|
|
bagnames = bagnames || [];
|
|
|
|
description = description || "";
|
2024-01-24 22:24:24 +00:00
|
|
|
const validationRecipeName = this.validateItemName(recipename);
|
|
|
|
if(validationRecipeName) {
|
|
|
|
return {message: validationRecipeName};
|
|
|
|
}
|
|
|
|
const validationBagNames = this.validateItemNames(bagnames);
|
|
|
|
if(validationBagNames) {
|
|
|
|
return {message: validationBagNames};
|
|
|
|
}
|
2024-01-26 14:42:43 +00:00
|
|
|
if(bagnames.length === 0) {
|
|
|
|
return {message: "Recipes must contain at least one bag"};
|
|
|
|
}
|
2024-01-23 14:29:50 +00:00
|
|
|
this.sqlTiddlerDatabase.createRecipe(recipename,bagnames,description);
|
2024-01-17 22:41:41 +00:00
|
|
|
this.saveEntityStateTiddler({
|
|
|
|
title: "recipes/" + recipename,
|
|
|
|
"recipe-name": recipename,
|
2024-01-23 14:29:50 +00:00
|
|
|
text: description,
|
2024-01-23 12:52:40 +00:00
|
|
|
list: $tw.utils.stringifyList(bagnames.map(bag_name => {
|
|
|
|
return this.entityStateTiddlerPrefix + "bags/" + bag_name;
|
|
|
|
}))
|
2024-01-17 22:41:41 +00:00
|
|
|
});
|
2024-01-24 22:24:24 +00:00
|
|
|
return null;
|
2024-01-02 14:39:14 +00:00
|
|
|
};
|
|
|
|
|
2024-01-21 18:18:29 +00:00
|
|
|
/*
|
|
|
|
Returns {tiddler_id:}
|
|
|
|
*/
|
2024-01-19 19:52:57 +00:00
|
|
|
SqlTiddlerStore.prototype.saveBagTiddler = function(tiddlerFields,bagname) {
|
2024-01-22 22:08:55 +00:00
|
|
|
return this.sqlTiddlerDatabase.saveBagTiddler(tiddlerFields,bagname);
|
2024-01-02 14:39:14 +00:00
|
|
|
};
|
|
|
|
|
2024-01-21 18:18:29 +00:00
|
|
|
/*
|
|
|
|
Returns {tiddler_id:,bag_name:}
|
|
|
|
*/
|
2024-01-03 16:27:13 +00:00
|
|
|
SqlTiddlerStore.prototype.saveRecipeTiddler = function(tiddlerFields,recipename) {
|
2024-01-22 22:08:55 +00:00
|
|
|
return this.sqlTiddlerDatabase.saveRecipeTiddler(tiddlerFields,recipename);
|
2024-01-03 16:27:13 +00:00
|
|
|
};
|
|
|
|
|
2024-01-02 14:39:14 +00:00
|
|
|
SqlTiddlerStore.prototype.deleteTiddler = function(title,bagname) {
|
2024-01-22 22:08:55 +00:00
|
|
|
this.sqlTiddlerDatabase.deleteTiddler(title,bagname);
|
2024-01-02 14:39:14 +00:00
|
|
|
};
|
|
|
|
|
2024-01-21 18:18:29 +00:00
|
|
|
/*
|
|
|
|
returns {tiddler_id:,tiddler:}
|
|
|
|
*/
|
2024-01-19 19:33:58 +00:00
|
|
|
SqlTiddlerStore.prototype.getBagTiddler = function(title,bagname) {
|
2024-01-23 10:51:12 +00:00
|
|
|
var tiddlerInfo = this.sqlTiddlerDatabase.getBagTiddler(title,bagname);
|
|
|
|
if(tiddlerInfo) {
|
|
|
|
return Object.assign(
|
|
|
|
{},
|
|
|
|
tiddlerInfo,
|
|
|
|
{
|
2024-01-26 15:48:39 +00:00
|
|
|
tiddler: this.processOutgoingTiddler(tiddlerInfo.tiddler,tiddlerInfo.tiddler_id,bagname)
|
2024-01-23 10:51:12 +00:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
2024-01-19 19:33:58 +00:00
|
|
|
};
|
|
|
|
|
2024-01-19 20:35:47 +00:00
|
|
|
/*
|
2024-01-21 18:18:29 +00:00
|
|
|
Returns {bag_name:, tiddler: {fields}, tiddler_id:}
|
2024-01-19 20:35:47 +00:00
|
|
|
*/
|
2024-01-19 19:33:58 +00:00
|
|
|
SqlTiddlerStore.prototype.getRecipeTiddler = function(title,recipename) {
|
2024-01-23 10:51:12 +00:00
|
|
|
var tiddlerInfo = this.sqlTiddlerDatabase.getRecipeTiddler(title,recipename);
|
2024-01-23 14:29:50 +00:00
|
|
|
if(tiddlerInfo) {
|
|
|
|
return Object.assign(
|
|
|
|
{},
|
|
|
|
tiddlerInfo,
|
|
|
|
{
|
2024-01-26 15:48:39 +00:00
|
|
|
tiddler: this.processOutgoingTiddler(tiddlerInfo.tiddler,tiddlerInfo.tiddler_id,tiddlerInfo.bag_name)
|
2024-01-23 14:29:50 +00:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
2024-01-02 14:39:14 +00:00
|
|
|
};
|
|
|
|
|
2024-01-19 19:25:58 +00:00
|
|
|
/*
|
|
|
|
Get the titles of the tiddlers in a bag. Returns an empty array for bags that do not exist
|
|
|
|
*/
|
|
|
|
SqlTiddlerStore.prototype.getBagTiddlers = function(bagname) {
|
2024-01-22 22:08:55 +00:00
|
|
|
return this.sqlTiddlerDatabase.getBagTiddlers(bagname);
|
2024-01-19 19:25:58 +00:00
|
|
|
};
|
|
|
|
|
2024-01-02 14:39:14 +00:00
|
|
|
/*
|
2024-01-22 22:08:55 +00:00
|
|
|
Get the titles of the tiddlers in a recipe as {title:,bag_name:}. Returns an empty array for recipes that do not exist
|
2024-01-02 14:39:14 +00:00
|
|
|
*/
|
|
|
|
SqlTiddlerStore.prototype.getRecipeTiddlers = function(recipename) {
|
2024-01-22 22:08:55 +00:00
|
|
|
return this.sqlTiddlerDatabase.getRecipeTiddlers(recipename);
|
2024-01-02 14:39:14 +00:00
|
|
|
};
|
|
|
|
|
2024-01-23 16:53:12 +00:00
|
|
|
SqlTiddlerStore.prototype.deleteAllTiddlersInBag = function(bagname) {
|
|
|
|
return this.sqlTiddlerDatabase.deleteAllTiddlersInBag(bagname);
|
|
|
|
};
|
|
|
|
|
2024-01-03 16:27:13 +00:00
|
|
|
/*
|
|
|
|
Get the names of the bags in a recipe. Returns an empty array for recipes that do not exist
|
|
|
|
*/
|
|
|
|
SqlTiddlerStore.prototype.getRecipeBags = function(recipename) {
|
2024-01-22 22:08:55 +00:00
|
|
|
return this.sqlTiddlerDatabase.getRecipeBags(recipename);
|
2024-01-03 16:27:13 +00:00
|
|
|
};
|
|
|
|
|
2024-01-02 14:39:14 +00:00
|
|
|
exports.SqlTiddlerStore = SqlTiddlerStore;
|
|
|
|
|
|
|
|
})();
|