1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2024-11-10 11:59:58 +00:00
TiddlyWiki5/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js

363 lines
11 KiB
JavaScript
Raw Normal View History

2024-01-02 14:39:14 +00:00
/*\
title: $:/plugins/tiddlywiki/multiwikiserver/store/sql-tiddler-store.js
2024-01-02 14:39:14 +00:00
type: application/javascript
module-type: library
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:
* Validating requests (eg bag and recipe name constraints)
* Synchronising bag and recipe names to the admin wiki
* Handling large tiddlers as attachments
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
attachmentStore - reference to associated attachment store
MWS: Add support for node-sqlite-wasm alongside better-sqlite3 (#7996) * Switch from better-sqlite3 to node-sqlite3-wasm Seems to be slower, but might make cloud deployments easier by not having any binary dependencies * More logging * Temporarily use a memory database We will make this configurable * Revert "More logging" * Resume loading demo tiddlers * Cache prepared statements Gives a 20% reduction in startup time on my machine * Some more logging * Update package-lock * More logging * Route regexps should allow for proxies that automatically decode URLs Astonishingly, Azure does this * Go back to a file-based database * Less logging * Update package-lock.json * Simplify startup by not loading the docs edition * Tiddler database layer should mark statements as having been removed * Re-introduce better-sqlite3 * Make the SQLite provider be switchable * Support switchable SQL engines I am not intending to make this a long term feature. We will choose one engine and stick with it until we choose to change to another. * Adjust dependency versions * Setting up default engine * Make transaction handling compatible with node-sqlite3-wasm https://github.com/tndrle/node-sqlite3-wasm doesn't have transaction support so I've tried to implement it using SQL statements directly. @hoelzro do you think this is right? Should we be rolling back the transaction in the finally clause? It would be nice to have tests in this area... I looked at better-sqlite3's implementation - https://github.com/WiseLibs/better-sqlite3/blob/master/lib/methods/transaction.js * Default to better-sqlite3 for compatibility after merging
2024-02-22 11:57:41 +00:00
engine - wasm | better
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 || {};
this.attachmentStore = options.attachmentStore;
2024-01-17 22:41:41 +00:00
this.adminWiki = options.adminWiki || $tw.wiki;
this.entityStateTiddlerPrefix = "$:/state/MultiWikiServer/";
2024-01-17 22:41:41 +00:00
// Create the database
this.databasePath = options.databasePath || ":memory:";
var SqlTiddlerDatabase = require("$:/plugins/tiddlywiki/multiwikiserver/store/sql-tiddler-database.js").SqlTiddlerDatabase;
this.sqlTiddlerDatabase = new SqlTiddlerDatabase({
MWS: Add support for node-sqlite-wasm alongside better-sqlite3 (#7996) * Switch from better-sqlite3 to node-sqlite3-wasm Seems to be slower, but might make cloud deployments easier by not having any binary dependencies * More logging * Temporarily use a memory database We will make this configurable * Revert "More logging" * Resume loading demo tiddlers * Cache prepared statements Gives a 20% reduction in startup time on my machine * Some more logging * Update package-lock * More logging * Route regexps should allow for proxies that automatically decode URLs Astonishingly, Azure does this * Go back to a file-based database * Less logging * Update package-lock.json * Simplify startup by not loading the docs edition * Tiddler database layer should mark statements as having been removed * Re-introduce better-sqlite3 * Make the SQLite provider be switchable * Support switchable SQL engines I am not intending to make this a long term feature. We will choose one engine and stick with it until we choose to change to another. * Adjust dependency versions * Setting up default engine * Make transaction handling compatible with node-sqlite3-wasm https://github.com/tndrle/node-sqlite3-wasm doesn't have transaction support so I've tried to implement it using SQL statements directly. @hoelzro do you think this is right? Should we be rolling back the transaction in the finally clause? It would be nice to have tests in this area... I looked at better-sqlite3's implementation - https://github.com/WiseLibs/better-sqlite3/blob/master/lib/methods/transaction.js * Default to better-sqlite3 for compatibility after merging
2024-02-22 11:57:41 +00:00
databasePath: this.databasePath,
engine: options.engine
});
this.sqlTiddlerDatabase.createTables();
this.updateAdminWiki();
2024-01-02 14:39:14 +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";
}
// Removed ~ from this list temporarily
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() {
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}));
};
SqlTiddlerStore.prototype.updateAdminWiki = function() {
var self = this;
return this.sqlTiddlerDatabase.transaction(function() {
// Update bags
for(const bagInfo of self.listBags()) {
self.saveEntityStateTiddler({
title: "bags/" + bagInfo.bag_name,
"bag-name": bagInfo.bag_name,
text: bagInfo.description
});
}
// Update recipes
for(const recipeInfo of self.listRecipes()) {
self.saveEntityStateTiddler({
title: "recipes/" + recipeInfo.recipe_name,
"recipe-name": recipeInfo.recipe_name,
text: recipeInfo.description,
list: $tw.utils.stringifyList(self.getRecipeBags(recipeInfo.recipe_name).map(bag_name => {
return self.entityStateTiddlerPrefix + "bags/" + bag_name;
}))
});
}
});
};
/*
2024-03-17 14:54:06 +00:00
Given tiddler fields, tiddler_id and a bag_name, return the tiddler fields after the following process:
- Apply the tiddler_id as the revision field
- Apply the bag_name as the bag field
*/
SqlTiddlerStore.prototype.processOutgoingTiddler = function(tiddlerFields,tiddler_id,bag_name,attachment_blob) {
const fields = Object.assign({},tiddlerFields,{
2024-01-26 15:48:39 +00:00
revision: "" + tiddler_id,
bag: bag_name
});
if(attachment_blob !== null) {
delete fields.text;
fields._canonical_uri = `/wiki/${encodeURIComponent(bag_name)}/bags/${encodeURIComponent(bag_name)}/tiddlers/${encodeURIComponent(tiddlerFields.title)}/blob`;
}
return fields;
2024-01-26 15:48:39 +00:00
};
/*
*/
SqlTiddlerStore.prototype.processIncomingTiddler = function(tiddlerFields) {
let attachmentSizeLimit = $tw.utils.parseNumber(this.adminWiki.getTiddlerText("$:/config/MultiWikiServer/AttachmentSizeLimit"));
if(attachmentSizeLimit < 100 * 1024) {
attachmentSizeLimit = 100 * 1024;
}
if(tiddlerFields.text && tiddlerFields.text.length > attachmentSizeLimit) {
const attachment_blob = this.attachmentStore.saveAttachment({
text: tiddlerFields.text,
type: tiddlerFields.type,
reference: tiddlerFields.title
});
return {
tiddlerFields: Object.assign({},tiddlerFields,{text: undefined}),
attachment_blob: attachment_blob
};
} else {
return {
tiddlerFields: tiddlerFields,
attachment_blob: null
};
}
};
2024-01-23 16:53:12 +00:00
SqlTiddlerStore.prototype.saveTiddlersFromPath = function(tiddler_files_path,bag_name) {
var self = this;
this.sqlTiddlerDatabase.transaction(function() {
// Clear out the bag
self.deleteAllTiddlersInBag(bag_name);
// Get the tiddlers
var path = require("path");
var tiddlersFromPath = $tw.loadTiddlersFromPath(path.resolve($tw.boot.corePath,$tw.config.editionsPath,tiddler_files_path));
// Save the tiddlers
for(const tiddlersFromFile of tiddlersFromPath) {
for(const tiddler of tiddlersFromFile.tiddlers) {
self.saveBagTiddler(tiddler,bag_name,null);
}
2024-01-23 16:53:12 +00:00
}
});
2024-01-23 16:53:12 +00:00
};
2024-01-17 22:41:41 +00:00
SqlTiddlerStore.prototype.listBags = function() {
return this.sqlTiddlerDatabase.listBags();
2024-01-17 22:41:41 +00:00
};
2024-03-17 14:54:06 +00:00
SqlTiddlerStore.prototype.createBag = function(bag_name,description) {
var self = this;
return this.sqlTiddlerDatabase.transaction(function() {
2024-03-17 14:54:06 +00:00
const validationBagName = self.validateItemName(bag_name);
if(validationBagName) {
return {message: validationBagName};
}
2024-03-17 14:54:06 +00:00
self.sqlTiddlerDatabase.createBag(bag_name,description);
self.saveEntityStateTiddler({
2024-03-17 14:54:06 +00:00
title: "bags/" + bag_name,
"bag-name": bag_name,
text: description
});
return null;
2024-01-17 22:41:41 +00:00
});
};
SqlTiddlerStore.prototype.listRecipes = function() {
return this.sqlTiddlerDatabase.listRecipes();
2024-01-02 14:39:14 +00:00
};
/*
Returns null on success, or {message:} on error
*/
2024-03-17 14:54:06 +00:00
SqlTiddlerStore.prototype.createRecipe = function(recipe_name,bag_names,description) {
bag_names = bag_names || [];
2024-01-23 14:29:50 +00:00
description = description || "";
2024-03-17 14:54:06 +00:00
const validationRecipeName = this.validateItemName(recipe_name);
if(validationRecipeName) {
return {message: validationRecipeName};
}
2024-03-17 14:54:06 +00:00
const validationBagNames = this.validateItemNames(bag_names);
if(validationBagNames) {
return {message: validationBagNames};
}
2024-03-17 14:54:06 +00:00
if(bag_names.length === 0) {
return {message: "Recipes must contain at least one bag"};
}
var self = this;
return this.sqlTiddlerDatabase.transaction(function() {
2024-03-17 14:54:06 +00:00
self.sqlTiddlerDatabase.createRecipe(recipe_name,bag_names,description);
self.saveEntityStateTiddler({
2024-03-17 14:54:06 +00:00
title: "recipes/" + recipe_name,
"recipe-name": recipe_name,
text: description,
2024-03-17 14:54:06 +00:00
list: $tw.utils.stringifyList(bag_names.map(bag_name => {
return self.entityStateTiddlerPrefix + "bags/" + bag_name;
}))
});
return null;
2024-01-17 22:41:41 +00:00
});
2024-01-02 14:39:14 +00:00
};
/*
Returns {tiddler_id:}
*/
2024-03-17 14:54:06 +00:00
SqlTiddlerStore.prototype.saveBagTiddler = function(incomingTiddlerFields,bag_name) {
const {tiddlerFields, attachment_blob} = this.processIncomingTiddler(incomingTiddlerFields);
2024-03-17 14:54:06 +00:00
return this.sqlTiddlerDatabase.saveBagTiddler(tiddlerFields,bag_name,attachment_blob);
};
/*
Create a tiddler in a bag adopting the specified file as the attachment. The attachment file must be on the same disk as the attachment store
Options include:
filepath - filepath to the attachment file
hash - string hash of the attachment file
type - content type of file as uploaded
Returns {tiddler_id:}
*/
2024-03-17 14:54:06 +00:00
SqlTiddlerStore.prototype.saveBagTiddlerWithAttachment = function(incomingTiddlerFields,bag_name,options) {
const attachment_blob = this.attachmentStore.adoptAttachment(options.filepath,options.type,options.hash);
if(attachment_blob) {
2024-03-17 14:54:06 +00:00
return this.sqlTiddlerDatabase.saveBagTiddler(incomingTiddlerFields,bag_name,attachment_blob);
} else {
return null;
}
2024-01-02 14:39:14 +00:00
};
/*
Returns {tiddler_id:,bag_name:}
*/
2024-03-17 14:54:06 +00:00
SqlTiddlerStore.prototype.saveRecipeTiddler = function(incomingTiddlerFields,recipe_name) {
const {tiddlerFields, attachment_blob} = this.processIncomingTiddler(incomingTiddlerFields);
2024-03-17 14:54:06 +00:00
return this.sqlTiddlerDatabase.saveRecipeTiddler(tiddlerFields,recipe_name,attachment_blob);
};
2024-03-17 14:54:06 +00:00
SqlTiddlerStore.prototype.deleteTiddler = function(title,bag_name) {
this.sqlTiddlerDatabase.deleteTiddler(title,bag_name);
2024-01-02 14:39:14 +00:00
};
/*
returns {tiddler_id:,tiddler:}
*/
2024-03-17 14:54:06 +00:00
SqlTiddlerStore.prototype.getBagTiddler = function(title,bag_name) {
var tiddlerInfo = this.sqlTiddlerDatabase.getBagTiddler(title,bag_name);
if(tiddlerInfo) {
return Object.assign(
{},
tiddlerInfo,
{
2024-03-17 14:54:06 +00:00
tiddler: this.processOutgoingTiddler(tiddlerInfo.tiddler,tiddlerInfo.tiddler_id,bag_name,tiddlerInfo.attachment_blob)
});
} else {
return null;
}
};
/*
Get an attachment ready to stream. Returns null if there is an error or:
stream: stream of file
type: type of file
*/
2024-03-17 14:54:06 +00:00
SqlTiddlerStore.prototype.getBagTiddlerStream = function(title,bag_name) {
const tiddlerInfo = this.sqlTiddlerDatabase.getBagTiddler(title,bag_name);
if(tiddlerInfo) {
if(tiddlerInfo.attachment_blob) {
return this.attachmentStore.getAttachmentStream(tiddlerInfo.attachment_blob);
} else {
const { Readable } = require('stream');
const stream = new Readable();
stream._read = function() {
// Push data
const type = tiddlerInfo.tiddler.type || "text/plain";
stream.push(tiddlerInfo.tiddler.text || "",($tw.config.contentTypeInfo[type] ||{encoding: "utf8"}).encoding);
// Push null to indicate the end of the stream
stream.push(null);
};
return {
stream: stream,
type: tiddlerInfo.tiddler.type || "text/plain"
}
}
} else {
return null;
}
};
/*
Returns {bag_name:, tiddler: {fields}, tiddler_id:}
*/
2024-03-17 14:54:06 +00:00
SqlTiddlerStore.prototype.getRecipeTiddler = function(title,recipe_name) {
var tiddlerInfo = this.sqlTiddlerDatabase.getRecipeTiddler(title,recipe_name);
2024-01-23 14:29:50 +00:00
if(tiddlerInfo) {
return Object.assign(
{},
tiddlerInfo,
{
tiddler: this.processOutgoingTiddler(tiddlerInfo.tiddler,tiddlerInfo.tiddler_id,tiddlerInfo.bag_name,tiddlerInfo.attachment_blob)
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
*/
2024-03-17 14:54:06 +00:00
SqlTiddlerStore.prototype.getBagTiddlers = function(bag_name) {
return this.sqlTiddlerDatabase.getBagTiddlers(bag_name);
2024-01-19 19:25:58 +00:00
};
2024-01-02 14:39:14 +00:00
/*
Get the titles of the tiddlers in a recipe as {title:,bag_name:}. Returns null for recipes that do not exist
2024-01-02 14:39:14 +00:00
*/
2024-03-17 14:54:06 +00:00
SqlTiddlerStore.prototype.getRecipeTiddlers = function(recipe_name) {
return this.sqlTiddlerDatabase.getRecipeTiddlers(recipe_name);
2024-01-02 14:39:14 +00:00
};
2024-03-17 14:54:06 +00:00
SqlTiddlerStore.prototype.deleteAllTiddlersInBag = function(bag_name) {
var self = this;
return this.sqlTiddlerDatabase.transaction(function() {
2024-03-17 14:54:06 +00:00
return self.sqlTiddlerDatabase.deleteAllTiddlersInBag(bag_name);
});
2024-01-23 16:53:12 +00:00
};
/*
Get the names of the bags in a recipe. Returns an empty array for recipes that do not exist
*/
2024-03-17 14:54:06 +00:00
SqlTiddlerStore.prototype.getRecipeBags = function(recipe_name) {
return this.sqlTiddlerDatabase.getRecipeBags(recipe_name);
};
2024-01-02 14:39:14 +00:00
exports.SqlTiddlerStore = SqlTiddlerStore;
})();