diff --git a/.gitignore b/.gitignore index ce0198ede61..fdd9e08143f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ tmp/ output/ node_modules/ -store/ +/editions/*/store /test-results/ /playwright-report/ /playwright/.cache/ diff --git a/boot/boot.js b/boot/boot.js index ab403aa5ae8..b220852c8d8 100644 --- a/boot/boot.js +++ b/boot/boot.js @@ -23,7 +23,7 @@ $tw.utils = $tw.utils || Object.create(null); /////////////////////////// Standard node.js libraries -var fs, path, vm; +var fs, path, /** @type {import("vm")} */ vm; if($tw.node) { fs = require("fs"); path = require("path"); @@ -212,7 +212,9 @@ $tw.utils.error = function(err) { var errHeading = ( $tw.language == undefined ? "Internal JavaScript Error" : $tw.language.getString("InternalJavaScriptError/Title") ), promptMsg = ( $tw.language == undefined ? "Well, this is embarrassing. It is recommended that you restart TiddlyWiki by refreshing your browser" : $tw.language.getString("InternalJavaScriptError/Hint") ); // Log the error to the console + console.error($tw.node ? "\x1b[1;31m" + err + "\x1b[0m" : err); + console.log(new Error().stack); if($tw.browser && !$tw.node) { // Display an error message to the user var dm = $tw.utils.domMaker, @@ -621,6 +623,7 @@ $tw.utils.evalGlobal = function(code,context,filename,sandbox,allowGlobals) { // Add the code prologue and epilogue code = [ "(function(" + contextNames.join(",") + ") {", + !$tw.browser ? " require('source-map-support').install();" : "", " (function(){" + code + "\n;})();\n", (!$tw.browser && sandbox && !allowGlobals) ? globalCheck : "", "\nreturn exports;\n", @@ -873,6 +876,8 @@ $tw.modules.execute = function(moduleName,moduleRoot) { setTimeout: setTimeout, clearTimeout: clearTimeout, Buffer: $tw.browser ? undefined : Buffer, + URL: URL, + URLSearchParams: URLSearchParams, $tw: $tw, require: function(title) { return $tw.modules.execute(title, name); @@ -928,7 +933,7 @@ $tw.modules.execute = function(moduleName,moduleRoot) { moduleInfo.definition(moduleInfo,moduleInfo.exports,sandbox.require); } else if(typeof moduleInfo.definition === "string") { // String moduleInfo.exports = _exports; - $tw.utils.evalSandboxed(moduleInfo.definition,sandbox,tiddler.fields.title); + $tw.utils.evalSandboxed(moduleInfo.definition,sandbox,tiddler.fields.filepath || tiddler.fields.title); if(sandbox.module.exports) { moduleInfo.exports = sandbox.module.exports; //more codemirror workaround } @@ -2117,6 +2122,9 @@ $tw.loadPluginFolder = function(filepath,excludeRegExp) { var tiddlers = pluginFiles[f].tiddlers; for(var t=0; t=0.8.2" } }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", @@ -79,18 +109,18 @@ } }, "node_modules/@eslint/core": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz", - "integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz", + "integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", + "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -111,27 +141,27 @@ } }, "node_modules/@eslint/js": { - "version": "9.14.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.14.0.tgz", - "integrity": "sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==", + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.12.0.tgz", + "integrity": "sha512-eohesHH8WFRUprDNyEREgqP6beG6htMeUYeCpkEgBCieCMme5r9zFWjzAJp//9S+Kub4rqE+jXe9Cp1a7IYIIA==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", - "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", + "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.2.tgz", - "integrity": "sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz", + "integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==", "dev": true, "dependencies": { "levn": "^0.4.1" @@ -162,19 +192,6 @@ "node": ">=18.18.0" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -189,9 +206,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", "dev": true, "engines": { "node": ">=18.18" @@ -201,6 +218,82 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@playwright/test": { "version": "1.48.2", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.2.tgz", @@ -216,18 +309,340 @@ "node": ">=18" } }, + "node_modules/@prisma/client": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.2.1.tgz", + "integrity": "sha512-msKY2iRLISN8t5X0Tj7hU0UWet1u0KuxSPHWuf3IRkB4J95mCvGpyQBfQ6ufcmvKNOMQSq90O2iUmJEN2e5fiA==", + "hasInstallScript": true, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.2.1.tgz", + "integrity": "sha512-0KItvt39CmQxWkEw6oW+RQMD6RZ43SJWgEUnzxN8VC9ixMysa7MzZCZf22LCK5DSooiLNf8vM3LHZm/I/Ni7bQ==" + }, + "node_modules/@prisma/engines": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.2.1.tgz", + "integrity": "sha512-lTBNLJBCxVT9iP5I7Mn6GlwqAxTpS5qMERrhebkUhtXpGVkBNd/jHnNJBZQW4kGDCKaQg/r2vlJYkzOHnAb7ZQ==", + "hasInstallScript": true, + "dependencies": { + "@prisma/debug": "6.2.1", + "@prisma/engines-version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69", + "@prisma/fetch-engine": "6.2.1", + "@prisma/get-platform": "6.2.1" + } + }, + "node_modules/@prisma/engines-version": { + "version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69.tgz", + "integrity": "sha512-7tw1qs/9GWSX6qbZs4He09TOTg1ff3gYsB3ubaVNN0Pp1zLm9NC5C5MZShtkz7TyQjx7blhpknB7HwEhlG+PrQ==" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.2.1.tgz", + "integrity": "sha512-OO7O9d6Mrx2F9i+Gu1LW+DGXXyUFkP7OE5aj9iBfA/2jjDXEJjqa9X0ZmM9NZNo8Uo7ql6zKm6yjDcbAcRrw1A==", + "dependencies": { + "@prisma/debug": "6.2.1", + "@prisma/engines-version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69", + "@prisma/get-platform": "6.2.1" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.2.1.tgz", + "integrity": "sha512-zp53yvroPl5m5/gXYLz7tGCNG33bhG+JYCm74ohxOq1pPnrL47VQYFfF3RbTZ7TzGWCrR3EtoiYMywUBw7UK6Q==", + "dependencies": { + "@prisma/debug": "6.2.1" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/node": { + "version": "22.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", + "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.1.tgz", + "integrity": "sha512-tJzcVyvvb9h/PB96g30MpxACd9IrunT7GF9wfA9/0TJ1LxGOJx1TdPzSbBBnNED7K9Ka8ybJsnEpiXPktolTLg==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.19.1", + "@typescript-eslint/type-utils": "8.19.1", + "@typescript-eslint/utils": "8.19.1", + "@typescript-eslint/visitor-keys": "8.19.1", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.19.1.tgz", + "integrity": "sha512-67gbfv8rAwawjYx3fYArwldTQKoYfezNUT4D5ioWetr/xCrxXxvleo3uuiFuKfejipvq+og7mjz3b0G2bVyUCw==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.19.1", + "@typescript-eslint/types": "8.19.1", + "@typescript-eslint/typescript-estree": "8.19.1", + "@typescript-eslint/visitor-keys": "8.19.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.19.1.tgz", + "integrity": "sha512-60L9KIuN/xgmsINzonOcMDSB8p82h95hoBfSBtXuO4jlR1R9L1xSkmVZKgCPVfavDlXihh4ARNjXhh1gGnLC7Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.19.1", + "@typescript-eslint/visitor-keys": "8.19.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.19.1.tgz", + "integrity": "sha512-Rp7k9lhDKBMRJB/nM9Ksp1zs4796wVNyihG9/TU9R6KCJDNkQbc2EOKjrBtLYh3396ZdpXLtr/MkaSEmNMtykw==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "8.19.1", + "@typescript-eslint/utils": "8.19.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.19.1.tgz", + "integrity": "sha512-JBVHMLj7B1K1v1051ZaMMgLW4Q/jre5qGK0Ew6UgXz1Rqh+/xPzV1aW581OM00X6iOfyr1be+QyW8LOUf19BbA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.1.tgz", + "integrity": "sha512-jk/TZwSMJlxlNnqhy0Eod1PNEvCkpY6MXOXE/WLlblZ6ibb32i2We4uByoKPv1d0OD2xebDv4hbs3fm11SMw8Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.19.1", + "@typescript-eslint/visitor-keys": "8.19.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.19.1.tgz", + "integrity": "sha512-IxG5gLO0Ne+KaUc8iW1A+XuKLd63o4wlbI1Zp692n1xojCl/THvgIKXJXBZixTh5dd5+yTJ/VXH7GJaaw21qXA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.19.1", + "@typescript-eslint/types": "8.19.1", + "@typescript-eslint/typescript-estree": "8.19.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.19.1.tgz", + "integrity": "sha512-fzmjU8CHK853V/avYZAvuVut3ZTfwN5YtMaoi+X9Y9MA9keaWNHC3zEQ9zvyX/7Hj+5JkNyK1l7TOR2hevHB6Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.19.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -349,6 +764,18 @@ "concat-map": "0.0.1" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -372,6 +799,11 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -402,6 +834,21 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -427,9 +874,9 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "dependencies": { "path-key": "^3.1.0", @@ -441,9 +888,9 @@ } }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, "dependencies": { "ms": "^2.1.3" @@ -493,6 +940,15 @@ "node": ">=8" } }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -514,21 +970,21 @@ } }, "node_modules/eslint": { - "version": "9.14.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.14.0.tgz", - "integrity": "sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g==", + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.12.0.tgz", + "integrity": "sha512-UVIOlTEWxwIopRL1wgSQYdnVDcEvs2wyaO6DGo5mXqe3r16IoCNWkR29iHhyaP4cICWjbgbmFUGAhh0GJRuGZw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", + "@eslint-community/regexpp": "^4.11.0", "@eslint/config-array": "^0.18.0", - "@eslint/core": "^0.7.0", + "@eslint/core": "^0.6.0", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.14.0", + "@eslint/js": "9.12.0", "@eslint/plugin-kit": "^0.2.0", - "@humanfs/node": "^0.16.6", + "@humanfs/node": "^0.16.5", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.0", + "@humanwhocodes/retry": "^0.3.1", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -536,9 +992,9 @@ "cross-spawn": "^7.0.2", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.1.0", + "eslint-visitor-keys": "^4.1.0", + "espree": "^10.2.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -668,12 +1124,56 @@ "node": ">=6" } }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -686,6 +1186,15 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fastq": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", + "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -703,6 +1212,18 @@ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -786,6 +1307,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -879,12 +1412,103 @@ "node": ">=0.10.0" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -958,6 +1582,28 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -1112,6 +1758,24 @@ "node": ">=8" } }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/playwright": { "version": "1.48.2", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.2.tgz", @@ -1176,6 +1840,63 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prisma": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.2.1.tgz", + "integrity": "sha512-hhyM0H13pQleQ+br4CkzGizS5I0oInoeTw3JfLw1BRZduBSQxPILlJLwi+46wZzj9Je7ndyQEMGw/n5cN2fknA==", + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "6.2.1" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, + "node_modules/prisma/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/pump": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", @@ -1194,6 +1915,26 @@ "node": ">=6" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -1216,6 +1957,12 @@ "node": ">=0.10.0" } }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -1238,6 +1985,39 @@ "node": ">=4" } }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1332,6 +2112,53 @@ "simple-concat": "^1.0.0" } }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -1396,6 +2223,30 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz", + "integrity": "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==", + "dev": true, + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -1419,6 +2270,46 @@ "node": ">= 0.8.0" } }, + "node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.19.1.tgz", + "integrity": "sha512-LKPUQpdEMVOeKluHi8md7rwLcoXHhwvWp3x+sJkMuq3gGm9yaYJtPo8sRZSblMFJ5pcOGCAak/scKf1mvZDlQw==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.19.1", + "@typescript-eslint/parser": "8.19.1", + "@typescript-eslint/utils": "8.19.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", diff --git a/package.json b/package.json index 5ae2be339d7..09992ff9b84 100644 --- a/package.json +++ b/package.json @@ -24,10 +24,13 @@ "wiki" ], "devDependencies": { + "@eslint/js": "^9.12.0", "@playwright/test": "^1.47.2", + "@types/jest": "^29.5.14", "eslint": "^9.12.0", - "@eslint/js": "^9.12.0", - "playwright": "^1.47.2" + "playwright": "^1.47.2", + "typescript": "^5.7.2", + "typescript-eslint": "^8.19.1" }, "license": "BSD", "engines": { @@ -40,10 +43,22 @@ "mws-add-user": "node ./tiddlywiki.js ./editions/multiwikiserver --build load-mws-demo-data --mws-listen --build mws-add-user --quit", "test": "npm run build:test-edition && npm run test:multiwikiserver-edition", "lint:fix": "eslint . --fix", - "lint": "eslint ." + "lint": "eslint .", + "prisma:generate": "(cd ./plugins/tiddlywiki/multiwikiserver && prisma generate)", + "mws": "NODE_DEV_PATH=yes node ./tiddlywiki.js ./editions/multiwikiserver", + "mws:tsc": "(cd ./plugins/tiddlywiki/multiwikiserver && tsc -p tsconfig.build.json)", + "mws:jsc": "(cd ./plugins/tiddlywiki/multiwikiserver && tsc -p jsconfig.json --noEmit)", + "mws:eslint": "(cd ./plugins/tiddlywiki/multiwikiserver && npx eslint)", + "mws:clean": "rm -rf ./editions/multiwikiserver/store", + "mws:create": "npm run mws:clean && npm run mws -- --mws-create-db --mws-load-plugin-bags --build load-mws-demo-data", + "arlen": "npm run mws:tsc && npm run mws:eslint && npm run mws -- --mws-listen --mws-ctrl-c" }, "dependencies": { + "@prisma/client": "^6.2.1", + "@types/node": "^22.10.5", "better-sqlite3": "^11.5.0", - "node-sqlite3-wasm": "^0.8.25" + "node-sqlite3-wasm": "^0.8.25", + "prisma": "^6.2.1", + "source-map-support": "^0.5.21" } } diff --git a/plugins/tiddlywiki/multiwikiserver/auth/authentication.js b/plugins/tiddlywiki/multiwikiserver/auth/authentication.js index 0658c93beb0..1ad589c0cb2 100644 --- a/plugins/tiddlywiki/multiwikiserver/auth/authentication.js +++ b/plugins/tiddlywiki/multiwikiserver/auth/authentication.js @@ -14,11 +14,16 @@ Handles authentication related operations "use strict"; var crypto = require("crypto"); - +/** + * + * @param {import("../src/store/sql-tiddler-database").SqlTiddlerDatabase} database + * @returns + */ function Authenticator(database) { if(!(this instanceof Authenticator)) { return new Authenticator(database); } + /** @type {import("../src/store/sql-tiddler-database").SqlTiddlerDatabase} */ this.sqlTiddlerDatabase = database; } @@ -31,10 +36,10 @@ Authenticator.prototype.hashPassword = function(password) { return crypto.createHash("sha256").update(password).digest("hex"); }; -Authenticator.prototype.createSession = function(userId) { +Authenticator.prototype.createSession = async function(userId) { var sessionId = crypto.randomBytes(16).toString("hex"); // Store the session in your database or in-memory store - this.sqlTiddlerDatabase.createUserSession(userId, sessionId); + await this.sqlTiddlerDatabase.createUserSession(userId, sessionId); return sessionId; }; diff --git a/plugins/tiddlywiki/multiwikiserver/eslint.config.js b/plugins/tiddlywiki/multiwikiserver/eslint.config.js new file mode 100644 index 00000000000..9ac47f7b694 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/eslint.config.js @@ -0,0 +1,435 @@ +//@ts-check +const globals = require("globals"); +const jsLint = require("@eslint/js"); +const tsLint = require("typescript-eslint"); +const utils_1 = require("@typescript-eslint/utils"); +const tsutils = require("ts-api-utils"); +const ts = require("typescript"); + +module.exports = tsLint.config( + { + ignores: [ + // Ignore "third party" code whose style we will not change. + "boot/sjcl.js", + "core/modules/utils/base64-utf8/base64-utf8.module.js", + "core/modules/utils/base64-utf8/base64-utf8.module.min.js", + "core/modules/utils/diff-match-patch/diff_match_patch.js", + "core/modules/utils/diff-match-patch/diff_match_patch_uncompressed.js", + "core/modules/utils/dom/csscolorparser.js", + "plugins/tiddlywiki/*/files/", + "modules/store/attachments.js", + "modules/store/sql-tiddler-database.js", + "modules/store/sql-tiddler-store.js", + "modules/router.js", + "modules/server.js", + ] + }, + jsLint.configs.recommended, + tsLint.configs.base, + {plugins: {"custom-rules": {rules: {"always-await": AlwaysAwaitRule()}}}}, + { + languageOptions: { + globals: { + ...globals.browser, + ...globals.commonjs, + ...globals.node, + // $tw: "writable", // temporary + }, + + parserOptions: { + project: "./tsconfig.json", + }, + ecmaVersion: 8, + sourceType: "commonjs", + }, + + rules: { + "array-bracket-newline": "off", + "array-bracket-spacing": "off", + "array-callback-return": "off", + "array-element-newline": "off", + // "arrow-parens": ["error", "as-needed"], + + "arrow-spacing": ["error", { + after: true, + before: true, + }], + + "block-scoped-var": "off", + "block-spacing": "off", + "brace-style": "off", + "callback-return": "off", + camelcase: "off", + "capitalized-comments": "off", + + "comma-dangle": "off", + "comma-spacing": "off", + "comma-style": "off", + complexity: "off", + "computed-property-spacing": "off", + "consistent-return": "off", + "consistent-this": "off", + curly: "off", + "default-case": "off", + "default-case-last": "error", + "default-param-last": "error", + "dot-location": "off", + "dot-notation": "off", + "eol-last": "off", + eqeqeq: "off", + "func-call-spacing": "off", + "func-name-matching": "off", + "func-names": "off", + "func-style": "off", + "function-call-argument-newline": "off", + "function-paren-newline": "off", + "generator-star-spacing": "error", + "global-require": "off", + "grouped-accessor-pairs": "error", + "guard-for-in": "off", + "handle-callback-err": "off", + "id-blacklist": "error", + "id-denylist": "error", + "id-length": "off", + "id-match": "error", + "implicit-arrow-linebreak": "error", + // "indent": "warn", + // "indent": ["warn", "tab", { + // "outerIIFEBody": 0 , + // "SwitchCase": 1, + // }], + "indent-legacy": "off", + "init-declarations": "off", + "jsx-quotes": "error", + "key-spacing": "off", + + // "keyword-spacing": ["error", { + // before: true, + // after: false, + + // overrides: { + // case: { + // after: true, + // }, + + // do: { + // after: true, + // }, + + // else: { + // after: true, + // }, + + // return: { + // after: true, + // }, + + // throw: { + // after: true, + // }, + + // try: { + // after: true, + // }, + // }, + // }], + + "line-comment-position": "off", + "linebreak-style": "off", + "lines-around-comment": "off", + "lines-around-directive": "off", + "lines-between-class-members": "error", + "max-classes-per-file": "error", + "max-depth": "off", + "max-len": "off", + "max-lines": "off", + "max-lines-per-function": "off", + "max-nested-callbacks": "error", + "max-params": "off", + "max-statements": "off", + "max-statements-per-line": "off", + "multiline-comment-style": "off", + "multiline-ternary": "off", + "new-parens": "off", + "newline-after-var": "off", + "newline-before-return": "off", + "newline-per-chained-call": "off", + "no-alert": "off", + "no-array-constructor": "off", + // "no-await-in-loop": "error", + "no-bitwise": "off", + "no-buffer-constructor": "off", + "no-caller": "error", + "no-catch-shadow": "off", + "no-confusing-arrow": "error", + "no-console": "off", + + "no-constant-condition": ["error", { + checkLoops: false, + }], + + "no-constructor-return": "error", + "no-continue": "off", + "no-div-regex": "off", + "no-duplicate-imports": "error", + "no-else-return": "off", + "no-empty-function": "off", + "no-eq-null": "off", + "no-eval": "off", + "no-extend-native": "off", + "no-extra-bind": "off", + "no-extra-label": "off", + "no-extra-parens": "off", + "no-floating-decimal": "off", + + "no-implicit-coercion": ["error", { + boolean: false, + number: false, + string: false, + }], + + "no-implicit-globals": "off", + "no-implied-eval": "error", + "no-inline-comments": "off", + "no-invalid-this": "off", + "no-iterator": "error", + "no-label-var": "off", + "no-labels": "off", + "no-lone-blocks": "off", + "no-lonely-if": "off", + "no-loop-func": "off", + "no-loss-of-precision": "error", + "no-magic-numbers": "off", + "no-mixed-operators": "off", + "no-mixed-requires": "off", + "no-multi-assign": "off", + "no-multi-spaces": "off", + "no-multi-str": "error", + "no-multiple-empty-lines": "off", + "no-native-reassign": "off", + "no-negated-condition": "off", + "no-negated-in-lhs": "error", + "no-nested-ternary": "off", + "no-new": "off", + "no-new-func": "off", + "no-new-object": "off", + "no-new-require": "error", + "no-new-wrappers": "error", + "no-octal-escape": "error", + "no-param-reassign": "off", + "no-path-concat": "error", + "no-plusplus": "off", + "no-process-env": "off", + "no-process-exit": "off", + "no-promise-executor-return": "error", + "no-proto": "off", + "no-restricted-exports": "error", + "no-restricted-globals": "error", + "no-restricted-imports": "error", + "no-restricted-modules": "error", + "no-restricted-properties": "error", + "no-restricted-syntax": "error", + "no-return-assign": "off", + // "no-return-await": "error", + "no-script-url": "off", + "no-self-compare": "off", + "no-sequences": "off", + "no-shadow": "off", + "no-spaced-func": "off", + "no-sync": "off", + "no-tabs": "off", + "no-template-curly-in-string": "error", + "no-ternary": "off", + "no-throw-literal": "off", + "no-trailing-spaces": "off", + "no-undef-init": "off", + "no-undefined": "off", + "no-underscore-dangle": "off", + "no-unmodified-loop-condition": "off", + "no-unneeded-ternary": "off", + "no-unreachable-loop": "error", + "no-unused-expressions": "off", + "no-use-before-define": "off", + "no-useless-backreference": "error", + "no-useless-call": "off", + "no-useless-computed-key": "error", + "no-useless-concat": "off", + "no-useless-constructor": "error", + "no-useless-rename": "error", + "no-useless-return": "off", + "no-var": "off", + "no-void": "off", + "no-warning-comments": "off", + "no-whitespace-before-property": "error", + "nonblock-statement-body-position": ["error", "any"], + "object-curly-newline": "off", + "object-curly-spacing": "off", + "object-property-newline": "off", + "object-shorthand": "off", + "one-var": "off", + "one-var-declaration-per-line": "off", + "operator-assignment": "off", + "operator-linebreak": "off", + "padded-blocks": "off", + "padding-line-between-statements": "error", + "prefer-arrow-callback": "off", + "prefer-const": "off", + "prefer-destructuring": "off", + "prefer-exponentiation-operator": "off", + "prefer-named-capture-group": "off", + "prefer-numeric-literals": "error", + "prefer-object-spread": "off", + "prefer-promise-reject-errors": "error", + "prefer-reflect": "off", + "prefer-regex-literals": "off", + "prefer-rest-params": "off", + "prefer-spread": "off", + "prefer-template": "off", + "quote-props": "off", + + // quotes: ["error", "double", { + // avoidEscape: true, + // }], + + radix: "off", + // "require-atomic-updates": "error", + + "require-jsdoc": "off", + "require-unicode-regexp": "off", + "rest-spread-spacing": "error", + semi: "off", + "semi-spacing": "off", + "semi-style": "off", + "sort-imports": "error", + "sort-keys": "off", + "sort-vars": "off", + "space-before-blocks": "off", + "space-before-function-paren": "off", + "space-in-parens": "off", + "space-infix-ops": "off", + "space-unary-ops": "off", + "spaced-comment": "off", + strict: "off", + "switch-colon-spacing": "off", + "symbol-description": "error", + "template-curly-spacing": "error", + "template-tag-spacing": "error", + "unicode-bom": ["error", "never"], + "valid-jsdoc": "off", + + "valid-typeof": ["error", { + requireStringLiterals: false, + }], + + "vars-on-top": "off", + "wrap-iife": "off", + "wrap-regex": "off", + "yield-star-spacing": "error", + yoda: "off", + + // temporary rules + "no-useless-escape": "off", + "no-unused-vars": "off", + "no-empty": "off", + "no-extra-semi": "off", + "no-redeclare": "off", + "no-control-regex": "off", + "no-mixed-spaces-and-tabs": "off", + "no-extra-boolean-cast": "off", + "no-prototype-builtins": "off", + "no-undef": "off", + "no-unreachable": "off", + "no-self-assign": "off", + + "no-return-await": "off", + "no-await-in-loop": "off", + "class-methods-use-this": "off", + "@typescript-eslint/no-floating-promises": "error", + "@typescript-eslint/no-misused-promises": "error", + "@typescript-eslint/promise-function-async": "error", + "custom-rules/always-await": "error", + "arrow-body-style": "off", + "require-await": "off", + }, + } +); +function AlwaysAwaitRule() { + + return { + meta: { + type: 'suggestion', + messages: { + expression: 'Expected non-Promise value or awaited Promise in an expression.', + assignment: 'Add await operator.', + declaration: 'Add await operator.', + statement: 'Add await operator.', + }, + fixable: 'code', + hasSuggestions: true, + }, + create(context) { + const services = utils_1.ESLintUtils.getParserServices(context); + const checker = services.program.getTypeChecker(); + const checks = new Set(["AwaitExpression", "VoidExpression"]) + return { + ":expression"(node) { + return; + if(checks.has(node.type)) return; + let parent = node; while(parent.expression && (parent = parent.parent)) if(checks.has(parent.type)) return; + console.log(node); + if(node.type === "VariableDeclarator") return; + if(node.parent.type === "VariableDeclarator" && node.parent.id === node) return; + if(node.parent.type === "AssignmentExpression" && node.parent.left === node) return; + if(isSometimesThenable(checker, node)) + context.report({node: node, messageId: 'expression', }); + + }, + AssignmentExpression: checkAll, + VariableDeclarator: checkAll, + ExpressionStatement: checkAll, + CallExpression: checkAll, + ReturnStatement: checkAll, + }; + + function addAwait(fixer, expression, node) { + // in keeping with the other rules, void signals that the await is being ignored + if(expression.type === utils_1.AST_NODE_TYPES.UnaryExpression && expression.operator === 'void') + return; + return fixer.insertTextBefore(expression, 'await '); + } + + /** + * @param {import("estree").Node} node + */ + function checkAll(node) { + if(node.type === "AssignmentExpression" && isSometimesThenable(checker, node.right)) + report((fixer) => addAwait(fixer, node.right, node)); + else if(node.type === "VariableDeclarator" && isSometimesThenable(checker, node.init)) + report((fixer) => addAwait(fixer, node.init, node)); + else if(node.type === "ReturnStatement" + && node.argument && node.argument.type === "CallExpression" + && isSometimesThenable(checker, node.argument)) + report((fixer) => addAwait(fixer, node.argument, node)); + else if(node.type === "ExpressionStatement" && isSometimesThenable(checker, node.expression)) + report((fixer) => addAwait(fixer, node.expression, node)); + + function report(fix) {return context.report({node: node, messageId: 'statement', fix, });} + } + + function isAlwaysThenable(checker, node) { + const tsNode = services.esTreeNodeToTSNodeMap.get(node); + const type = checker.getTypeAtLocation(tsNode); + if(!tsutils.isThenableType(checker, tsNode, checker.getApparentType(type))) return false; + return true; + } + function isSometimesThenable(checker, node) { + const tsNode = services.esTreeNodeToTSNodeMap.get(node); + const type = checker.getTypeAtLocation(tsNode); + for(const subType of tsutils.unionTypeParts(checker.getApparentType(type))) { + if(tsutils.isThenableType(checker, tsNode, subType)) return true; + } + return false; + } + }, + }; +} \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/jsconfig.json b/plugins/tiddlywiki/multiwikiserver/jsconfig.json new file mode 100644 index 00000000000..f4de5372595 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/jsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2015", + "module": "commonjs", + "moduleResolution": "node", + "paths": { + "$:/plugins/tiddlywiki/multiwikiserver/auth/*": [ + "./auth/*" + ], + "$:/plugins/tiddlywiki/multiwikiserver/*": [ + "./src/*", + "./modules/*", + ], + }, + "checkJs": true, + "allowJs": true, + "strictNullChecks": true, + "strict": true, + "noImplicitAny": false, + }, + "include": [ + "./modules/**/*.js", + "./src/globals.d.ts" + ], + +} \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-permission.js b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-permission.js index fe742956612..b6e47e8881b 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-permission.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-permission.js @@ -23,21 +23,21 @@ var Command = function(params,commander,callback) { this.callback = callback; }; -Command.prototype.execute = function() { +Command.prototype.execute = async function() { var self = this; if(this.params.length < 2) { return "Usage: --mws-add-permission "; } - if(!$tw.mws || !$tw.mws.store || !$tw.mws.store.sqlTiddlerDatabase) { + if(!$tw.mws || !$tw.mws.store || !$tw.mws.store.sql) { return "Error: MultiWikiServer or SQL database not initialized."; } var permission_name = this.params[0]; var description = this.params[1]; - $tw.mws.store.sqlTiddlerDatabase.createPermission(permission_name, description); + await $tw.mws.store.sql.createPermission(permission_name, description); self.callback(); return null; }; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-role.js b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-role.js index ec435a97ff3..fb60e478449 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-role.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-role.js @@ -23,21 +23,21 @@ var Command = function(params,commander,callback) { this.callback = callback; }; -Command.prototype.execute = function() { +Command.prototype.execute = async function() { var self = this; if(this.params.length < 2) { return "Usage: --mws-add-role "; } - if(!$tw.mws || !$tw.mws.store || !$tw.mws.store.sqlTiddlerDatabase) { + if(!$tw.mws || !$tw.mws.store || !$tw.mws.store.sql) { return "Error: MultiWikiServer or SQL database not initialized."; } var role_name = this.params[0]; var description = this.params[1]; - $tw.mws.store.sqlTiddlerDatabase.createRole(role_name, description); + await $tw.mws.store.sql.createRole(role_name, description); self.callback(null, "Role Created Successfully!"); return null; }; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-user.js b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-user.js index fc0c4e6e197..11c98e9cdf7 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-user.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-user.js @@ -25,14 +25,14 @@ var Command = function(params,commander,callback) { this.callback = callback; }; -Command.prototype.execute = function() { +Command.prototype.execute = async function() { var self = this; if(this.params.length < 2) { return "Usage: --mws-add-user [email]"; } - if(!$tw.mws || !$tw.mws.store || !$tw.mws.store.sqlTiddlerDatabase) { + if(!$tw.mws || !$tw.mws.store || !$tw.mws.store.sql) { return "Error: MultiWikiServer or SQL database not initialized."; } @@ -41,10 +41,10 @@ Command.prototype.execute = function() { var email = this.params[2] || username + "@example.com"; var hashedPassword = crypto.createHash("sha256").update(password).digest("hex"); - var user = $tw.mws.store.sqlTiddlerDatabase.getUserByUsername(username); + var user = await $tw.mws.store.sql.getUserByUsername(username); if(!user) { - $tw.mws.store.sqlTiddlerDatabase.createUser(username, email, hashedPassword); + await $tw.mws.store.sql.createUser(username, email, hashedPassword); console.log("User Account Created Successfully with username: " + username + " and password: " + password); self.callback(); } diff --git a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-assign-role-permission.js b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-assign-role-permission.js index 89ed568d932..e3d5df95818 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-assign-role-permission.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-assign-role-permission.js @@ -23,21 +23,21 @@ var Command = function(params,commander,callback) { this.callback = callback; }; -Command.prototype.execute = function() { +Command.prototype.execute = async function() { var self = this; if(this.params.length < 2) { return "Usage: --mws-assign-role-permission "; } - if(!$tw.mws || !$tw.mws.store || !$tw.mws.store.sqlTiddlerDatabase) { + if(!$tw.mws || !$tw.mws.store || !$tw.mws.store.sql) { return "Error: MultiWikiServer or SQL database not initialized."; } var role_name = this.params[0]; var permission_name = this.params[1]; - var role = $tw.mws.store.sqlTiddlerDatabase.getRoleByName(role_name); - var permission = $tw.mws.store.sqlTiddlerDatabase.getPermissionByName(permission_name); + var role = await $tw.mws.store.sql.getRoleByName(role_name); + var permission = await $tw.mws.store.sql.getPermissionByName(permission_name); if(!role) { return "Error: Unable to find Role: "+role_name; @@ -47,10 +47,10 @@ Command.prototype.execute = function() { return "Error: Unable to find Permission: "+permission_name; } - var permission = $tw.mws.store.sqlTiddlerDatabase.getPermissionByName(permission_name); + var permission = await $tw.mws.store.sql.getPermissionByName(permission_name); - $tw.mws.store.sqlTiddlerDatabase.addPermissionToRole(role.role_id, permission.permission_id); + await $tw.mws.store.sql.addPermissionToRole(role.role_id, permission.permission_id); self.callback(); return null; }; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-assign-user-role.js b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-assign-user-role.js index 2657dbdd332..07f5ccc2b31 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-assign-user-role.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-assign-user-role.js @@ -23,21 +23,21 @@ var Command = function(params,commander,callback) { this.callback = callback; }; -Command.prototype.execute = function() { +Command.prototype.execute = async function() { var self = this; if(this.params.length < 2) { return "Usage: --mws-assign-user-role "; } - if(!$tw.mws || !$tw.mws.store || !$tw.mws.store.sqlTiddlerDatabase) { + if(!$tw.mws || !$tw.mws.store || !$tw.mws.store.sql) { return "Error: MultiWikiServer or SQL database not initialized."; } var username = this.params[0]; var role_name = this.params[1]; - var role = $tw.mws.store.sqlTiddlerDatabase.getRoleByName(role_name); - var user = $tw.mws.store.sqlTiddlerDatabase.getUserByUsername(username); + var role = await $tw.mws.store.sql.getRoleByName(role_name); + var user = await $tw.mws.store.sql.getUserByUsername(username); if(!role) { return "Error: Unable to find Role: "+role_name; @@ -47,7 +47,7 @@ Command.prototype.execute = function() { return "Error: Unable to find user with the username "+username; } - $tw.mws.store.sqlTiddlerDatabase.addRoleToUser(user.user_id, role.role_id); + await $tw.mws.store.sql.addRoleToUser(user.user_id, role.role_id); console.log(role_name+" role has been assigned to user with username "+username) self.callback(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-create-bag.js b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-create-bag.js index c90088a8729..c33c6dfcd49 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-create-bag.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-create-bag.js @@ -25,7 +25,7 @@ var Command = function(params,commander,callback) { this.callback = callback; }; -Command.prototype.execute = function() { +Command.prototype.execute = async function() { var self = this; // Check parameters if(this.params.length < 1) { @@ -34,7 +34,7 @@ Command.prototype.execute = function() { var bagName = this.params[0], bagDescription = this.params[1] || bagName; // Create bag - var result = $tw.mws.store.createBag(bagName,bagDescription); + var result = await $tw.mws.store.createBag(bagName,bagDescription); if(result) { return result.message; } else { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-create-db.js b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-create-db.js new file mode 100644 index 00000000000..edd81b0d476 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-create-db.js @@ -0,0 +1,43 @@ +/*\ +title: $:/plugins/tiddlywiki/multiwikiserver/commands/mws-create-db.js +type: application/javascript +module-type: command + +Listen for HTTP requests and serve tiddlers + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +exports.info = { + name: "mws-create-db", + synchronous: true, + namedParameterMode: true, + mandatoryParameters: [] +}; + +var Command = function(params,commander,callback) { + var self = this; + this.params = params; + this.commander = commander; + // this.callback = callback; +}; + +Command.prototype.execute = async function() { + var self = this; + + // irony of all ironies, we still have to use the sqlite3 package to create the database + const sqlite = require("node-sqlite3-wasm"); + const db = new sqlite.Database($tw.mws.databasePath); + db.exec($tw.wiki.getTiddlerText("$:/plugins/tiddlywiki/multiwikiserver/prisma/schema.prisma.sql")); + db.close(); + console.log("db created at", $tw.mws.databasePath); + return null; +}; + +exports.Command = Command; + +})(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-create-recipe.js b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-create-recipe.js index 6515c817a20..c9e51b9f2fb 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-create-recipe.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-create-recipe.js @@ -27,7 +27,7 @@ var Command = function(params,commander,callback) { this.callback = callback; }; -Command.prototype.execute = function() { +Command.prototype.execute = async function() { var self = this; // Check parameters if(this.params.length < 1) { @@ -35,9 +35,9 @@ Command.prototype.execute = function() { } var recipeName = this.params[0], bagList = (this.params[1] || "").split(" "), - recipeDescription = this.params[2] || recipeNameName; + recipeDescription = this.params[2] || recipeName; // Create recipe - var result = $tw.mws.store.createRecipe(recipeName,bagList,recipeDescription); + var result = await $tw.mws.store.createRecipe(recipeName,bagList,recipeDescription); if(result) { return result.message; } else { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-ctrl-c.js b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-ctrl-c.js new file mode 100644 index 00000000000..5f8d32e570a --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-ctrl-c.js @@ -0,0 +1,37 @@ +/*\ +title: $:/plugins/tiddlywiki/multiwikiserver/commands/mws-ctrl-c.js +type: application/javascript +module-type: command + +Listen for HTTP requests and serve tiddlers + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +exports.info = { + name: "mws-ctrl-c", + synchronous: true, + namedParameterMode: true, + mandatoryParameters: [] +}; + +var Command = function(params,commander,callback) { + var self = this; + this.params = params; + this.commander = commander; + // this.callback = callback; +}; + +Command.prototype.execute = function() { + var self = this; + $tw.utils.log("(press ctrl-C to exit)","red"); + return null; +}; + +exports.Command = Command; + +})(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-listen.js b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-listen.js index 0ee10f9be2c..7c137c5b0eb 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-listen.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-listen.js @@ -31,16 +31,19 @@ Command.prototype.execute = function() { if(!$tw.boot.wikiTiddlersPath) { $tw.utils.warning("Warning: Wiki folder '" + $tw.boot.wikiPath + "' does not exist or is missing a tiddlywiki.info file"); } + + // the command can be called multiple times to setup multiple servers. + // the first call will create the router and add it to the serverManager + // all router params must be sent to the first call, subsequent calls will only use the server params + if(!$tw.mws.router){ + $tw.mws.serverManager.createRouter(self.params); + } + // Set up server - this.server = $tw.mws.serverManager.createServer({ - wiki: $tw.wiki, - variables: self.params - }); - this.server.listen(null,null,null,{ - callback: function() { - self.callback(); - } + $tw.mws.serverManager.listenCommand(self.params, () => { + this.callback(); }); + return null; }; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-load-archive.js b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-load-archive.js index 9e37cb32b08..284affbd4d3 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-load-archive.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-load-archive.js @@ -20,21 +20,20 @@ exports.info = { var Command = function(params,commander,callback) { this.params = params; this.commander = commander; - this.callback = callback; + // this.callback = callback; }; -Command.prototype.execute = function() { - var self = this; +Command.prototype.execute = async function() { // Check parameters if(this.params.length < 1) { return "Missing pathname"; } var archivePath = this.params[0]; - loadBackupArchive(archivePath); + await loadBackupArchive(archivePath); return null; }; -function loadBackupArchive(archivePath) { +async function loadBackupArchive(archivePath) { const fs = require("fs"), path = require("path"); // Iterate the bags @@ -43,7 +42,7 @@ function loadBackupArchive(archivePath) { const bagName = decodeURIComponent(bagFilename); console.log(`Reading bag ${bagName}`); const bagInfo = JSON.parse(fs.readFileSync(path.resolve(archivePath,"bags",bagFilename,"meta.json"),"utf8")); - $tw.mws.store.createBag(bagName,bagInfo.description,bagInfo.accesscontrol); + await $tw.mws.store.createBag(bagName,bagInfo.description,bagInfo.accesscontrol); if(fs.existsSync(path.resolve(archivePath,"bags",bagFilename,"tiddlers"))) { const tiddlerFilenames = fs.readdirSync(path.resolve(archivePath,"bags",bagFilename,"tiddlers")); for(const tiddlerFilename of tiddlerFilenames) { @@ -52,7 +51,7 @@ function loadBackupArchive(archivePath) { jsonTiddler = fs.readFileSync(tiddlerPath,"utf8"), tiddler = sanitiseTiddler(JSON.parse(jsonTiddler)); if(tiddler && tiddler.title) { - $tw.mws.store.saveBagTiddler(tiddler,bagName); + await $tw.mws.store.saveBagTiddler(tiddler,bagName); } else { console.log(`Malformed JSON tiddler in file ${tiddlerPath}`); } @@ -66,7 +65,7 @@ function loadBackupArchive(archivePath) { if(recipeFilename.endsWith(".json")) { const recipeName = decodeURIComponent(recipeFilename.substring(0,recipeFilename.length - ".json".length)); const jsonInfo = JSON.parse(fs.readFileSync(path.resolve(archivePath,"recipes",recipeFilename),"utf8")); - $tw.mws.store.createRecipe(recipeName,jsonInfo.bag_names,jsonInfo.description,jsonInfo.accesscontrol); + await $tw.mws.store.createRecipe(recipeName,jsonInfo.bag_names,jsonInfo.description,jsonInfo.accesscontrol); } } }; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-load-plugin-bags.js b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-load-plugin-bags.js index 40bfb37493d..e3e422fc824 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-load-plugin-bags.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-load-plugin-bags.js @@ -23,57 +23,68 @@ var Command = function(params,commander,callback) { this.callback = callback; }; -Command.prototype.execute = function() { - var self = this; - loadPluginBags(); +Command.prototype.execute = async function() { + await loadPluginBags(); return null; }; -function loadPluginBags() { +async function loadPluginBags() { const path = require("path"), fs = require("fs"); // Copy plugins - var makePluginBagName = function(type,publisher,name) { - return "$:/" + type + "/" + (publisher ? publisher + "/" : "") + name; - }, - savePlugin = function(pluginFields,type,publisher,name) { - const bagName = makePluginBagName(type,publisher,name); - const result = $tw.mws.store.createBag(bagName,pluginFields.description || "(no description)",{allowPrivilegedCharacters: true}); - if(result) { - console.log(`Error creating plugin bag ${bagname}: ${JSON.stringify(result)}`); - } - $tw.mws.store.saveBagTiddler(pluginFields,bagName); - }, - collectPlugins = function(folder,type,publisher) { - var pluginFolders = $tw.utils.getSubdirectories(folder) || []; - for(var p=0; p { + await store.createRecipe(options.recipeName,recipeList,options.recipeDescription); + await store.saveTiddlersFromPath(path.resolve(options.wikiPath,$tw.config.wikiTiddlersSubDir),options.bagName); + }); + } } diff --git a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-save-archive.js b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-save-archive.js index d8b67753ca7..27bb7c7674a 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-save-archive.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-save-archive.js @@ -20,21 +20,21 @@ exports.info = { var Command = function(params,commander,callback) { this.params = params; this.commander = commander; - this.callback = callback; + // this.callback = callback; }; -Command.prototype.execute = function() { +Command.prototype.execute = async function() { var self = this; // Check parameters if(this.params.length < 1) { return "Missing pathname"; } var archivePath = this.params[0]; - saveArchive(archivePath); + await saveArchive(archivePath); return null; }; -function saveArchive(archivePath) { +async function saveArchive(archivePath) { const fs = require("fs"), path = require("path"); function saveJsonFile(filename,json) { @@ -43,16 +43,22 @@ function saveArchive(archivePath) { $tw.utils.createFileDirectories(filepath); fs.writeFileSync(filepath,JSON.stringify(json,null,4)); } - for(const recipeInfo of $tw.mws.store.listRecipes()) { + for(const recipeInfo of await $tw.mws.store.listRecipes()) { console.log(`Recipe ${recipeInfo.recipe_name}`); saveJsonFile(`recipes/${$tw.utils.encodeURIComponentExtended(recipeInfo.recipe_name)}.json`,recipeInfo); } - for(const bagInfo of $tw.mws.store.listBags()) { + for(const bagInfo of await $tw.mws.store.listBags()) { console.log(`Bag ${bagInfo.bag_name}`); saveJsonFile(`bags/${$tw.utils.encodeURIComponentExtended(bagInfo.bag_name)}/meta.json`,bagInfo); - for(const title of $tw.mws.store.getBagTiddlers(bagInfo.bag_name)) { - const tiddlerInfo = $tw.mws.store.getBagTiddler(title,bagInfo.bag_name); - saveJsonFile(`bags/${$tw.utils.encodeURIComponentExtended(bagInfo.bag_name)}/tiddlers/${$tw.utils.encodeURIComponentExtended(title)}.json`,tiddlerInfo.tiddler); + for(const title of await $tw.mws.store.getBagTiddlers(bagInfo.bag_name)) { + const tiddlerInfo = await $tw.mws.store.getBagTiddler(title,bagInfo.bag_name); + const bagPart = $tw.utils.encodeURIComponentExtended(bagInfo.bag_name); + const titlePart = $tw.utils.encodeURIComponentExtended(title); + if(!tiddlerInfo) { + $tw.utils.warning(`Missing tiddler ${title} in bag ${bagInfo.bag_name}`); + continue; + } + saveJsonFile(`bags/${bagPart}/tiddlers/${titlePart}.json`,tiddlerInfo.tiddler); } } } diff --git a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-save-tiddler-text.js b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-save-tiddler-text.js index 524ec948a82..6af081cee89 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-save-tiddler-text.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-save-tiddler-text.js @@ -25,7 +25,7 @@ var Command = function(params,commander,callback) { this.callback = callback; }; -Command.prototype.execute = function() { +Command.prototype.execute = async function() { var self = this; // Check parameters if(this.params.length < 3) { @@ -35,7 +35,7 @@ Command.prototype.execute = function() { tiddlerTitle = this.params[1], tiddlerText = this.params[2]; // Save tiddler - $tw.mws.store.saveBagTiddler({title: tiddlerTitle,text: tiddlerText},bagName); + await $tw.mws.store.saveBagTiddler({title: tiddlerTitle,text: tiddlerText},bagName); return null; }; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/mws-server.js b/plugins/tiddlywiki/multiwikiserver/modules/mws-server.js deleted file mode 100644 index 132159a8fe9..00000000000 --- a/plugins/tiddlywiki/multiwikiserver/modules/mws-server.js +++ /dev/null @@ -1,596 +0,0 @@ -/*\ -title: $:/plugins/tiddlywiki/multiwikiserver/mws-server.js -type: application/javascript -module-type: library - -Serve tiddlers over http - -\*/ -(function(){ - -/*jslint node: true, browser: true */ -/*global $tw: false */ -"use strict"; - -if($tw.node) { - var util = require("util"), - fs = require("fs"), - url = require("url"), - path = require("path"), - querystring = require("querystring"), - crypto = require("crypto"), - zlib = require("zlib"), - aclMiddleware = require('$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js').middleware; -} - -/* -A simple HTTP server with regexp-based routes -options: variables - optional hashmap of variables to set (a misnomer - they are really constant parameters) - routes - optional array of routes to use - wiki - reference to wiki object -*/ -function Server(options) { - var self = this; - this.routes = options.routes || []; - this.authenticators = options.authenticators || []; - this.wiki = options.wiki; - this.boot = options.boot || $tw.boot; - this.sqlTiddlerDatabase = options.sqlTiddlerDatabase || $tw.mws.store.sqlTiddlerDatabase; - // Initialise the variables - this.variables = $tw.utils.extend({},this.defaultVariables); - if(options.variables) { - for(var variable in options.variables) { - if(options.variables[variable]) { - this.variables[variable] = options.variables[variable]; - } - } - } - // Setup the default required plugins - this.requiredPlugins = this.get("required-plugins").split(','); - // Initialise CSRF - this.csrfDisable = this.get("csrf-disable") === "yes"; - // Initialize Gzip compression - this.enableGzip = this.get("gzip") === "yes"; - // Initialize browser-caching - this.enableBrowserCache = this.get("use-browser-cache") === "yes"; - // Initialise authorization - var authorizedUserName; - if(this.get("username") && this.get("password")) { - authorizedUserName = this.get("username"); - } else if(this.get("credentials")) { - authorizedUserName = "(authenticated)"; - } else { - authorizedUserName = "(anon)"; - } - this.authorizationPrincipals = { - readers: (this.get("readers") || authorizedUserName).split(",").map($tw.utils.trim), - writers: (this.get("writers") || authorizedUserName).split(",").map($tw.utils.trim) - } - if(this.get("admin") || authorizedUserName !== "(anon)") { - this.authorizationPrincipals["admin"] = (this.get("admin") || authorizedUserName).split(',').map($tw.utils.trim) - } - // Load and initialise authenticators - $tw.modules.forEachModuleOfType("authenticator", function(title,authenticatorDefinition) { - // console.log("Loading authenticator " + title); - self.addAuthenticator(authenticatorDefinition.AuthenticatorClass); - }); - // Load route handlers - $tw.modules.forEachModuleOfType("mws-route", function(title,routeDefinition) { - self.addRoute(routeDefinition); - }); - // Initialise the http vs https - this.listenOptions = null; - this.protocol = "http"; - var tlsKeyFilepath = this.get("tls-key"), - tlsCertFilepath = this.get("tls-cert"), - tlsPassphrase = this.get("tls-passphrase"); - if(tlsCertFilepath && tlsKeyFilepath) { - this.listenOptions = { - key: fs.readFileSync(path.resolve(this.boot.wikiPath,tlsKeyFilepath),"utf8"), - cert: fs.readFileSync(path.resolve(this.boot.wikiPath,tlsCertFilepath),"utf8"), - passphrase: tlsPassphrase || '' - }; - this.protocol = "https"; - } - this.transport = require(this.protocol); - // Name the server and init the boot state - 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") || ""; -} - -/* -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 -response will be transmitted and the browser will use the cached data. -Only requests with status code 200 are considdered for caching. -request: request instance passed to the handler -response: response instance passed to the handler -statusCode: stauts code to send to the browser -headers: response headers (they will be augmented with an `Etag` header) -data: the data to send (passed to the end method of the response instance) -encoding: the encoding of the data to send (passed to the end method of the response instance) -*/ -function sendResponse(request,response,statusCode,headers,data,encoding) { - if(this.enableBrowserCache && (statusCode == 200)) { - var hash = crypto.createHash('md5'); - // Put everything into the hash that could change and invalidate the data that - // the browser already stored. The headers the data and the encoding. - hash.update(data); - hash.update(JSON.stringify(headers)); - if(encoding) { - hash.update(encoding); - } - var contentDigest = hash.digest("hex"); - // RFC 7232 section 2.3 mandates for the etag to be enclosed in quotes - headers["Etag"] = '"' + contentDigest + '"'; - headers["Cache-Control"] = "max-age=0, must-revalidate"; - // Check if any of the hashes contained within the if-none-match header - // matches the current hash. - // If one matches, do not send the data but tell the browser to use the - // cached data. - // We do not implement "*" as it makes no sense here. - var ifNoneMatch = request.headers["if-none-match"]; - if(ifNoneMatch) { - var matchParts = ifNoneMatch.split(",").map(function(etag) { - return etag.replace(/^[ "]+|[ "]+$/g, ""); - }); - if(matchParts.indexOf(contentDigest) != -1) { - response.writeHead(304,headers); - response.end(); - return; - } - } - } - /* - If the gzip=yes is set, check if the user agent permits compression. If so, - compress our response if the raw data is bigger than 2k. Compressing less - data is inefficient. Note that we use the synchronous functions from zlib - to stay in the imperative style. The current `Server` doesn't depend on - this, and we may just as well use the async versions. - */ - if(this.enableGzip && (data.length > 2048)) { - var acceptEncoding = request.headers["accept-encoding"] || ""; - if(/\bdeflate\b/.test(acceptEncoding)) { - headers["Content-Encoding"] = "deflate"; - data = zlib.deflateSync(data); - } else if(/\bgzip\b/.test(acceptEncoding)) { - headers["Content-Encoding"] = "gzip"; - data = zlib.gzipSync(data); - } - } - if(!response.headersSent) { - response.writeHead(statusCode,headers); - response.end(data,encoding); - } -} - -function redirect(request,response,statusCode,location) { - response.setHeader("Location",location); - response.statusCode = statusCode; - response.end() -} - -/* -Options include: -cbPartStart(headers,name,filename) - invoked when a file starts being received -cbPartChunk(chunk) - invoked when a chunk of a file is received -cbPartEnd() - invoked when a file finishes being received -cbFinished(err) - invoked when the all the form data has been processed -*/ -function streamMultipartData(request,options) { - // Check that the Content-Type is multipart/form-data - const contentType = request.headers['content-type']; - if(!contentType.startsWith("multipart/form-data")) { - return options.cbFinished("Expected multipart/form-data content type"); - } - // Extract the boundary string from the Content-Type header - const boundaryMatch = contentType.match(/boundary=(.+)$/); - if(!boundaryMatch) { - return options.cbFinished("Missing boundary in multipart/form-data"); - } - const boundary = boundaryMatch[1]; - const boundaryBuffer = Buffer.from("--" + boundary); - // Initialise - let buffer = Buffer.alloc(0); - let processingPart = false; - // Process incoming chunks - request.on("data", (chunk) => { - // Accumulate the incoming data - buffer = Buffer.concat([buffer, chunk]); - // Loop through any parts within the current buffer - while (true) { - if(!processingPart) { - // If we're not processing a part then we try to find a boundary marker - const boundaryIndex = buffer.indexOf(boundaryBuffer); - if(boundaryIndex === -1) { - // Haven't reached the boundary marker yet, so we should wait for more data - break; - } - // Look for the end of the headers - const endOfHeaders = buffer.indexOf("\r\n\r\n",boundaryIndex + boundaryBuffer.length); - if(endOfHeaders === -1) { - // Haven't reached the end of the headers, so we should wait for more data - break; - } - // Extract and parse headers - const headersPart = Uint8Array.prototype.slice.call(buffer,boundaryIndex + boundaryBuffer.length,endOfHeaders).toString(); - const currentHeaders = {}; - headersPart.split("\r\n").forEach(headerLine => { - const [key, value] = headerLine.split(": "); - currentHeaders[key.toLowerCase()] = value; - }); - // Parse the content disposition header - const contentDisposition = { - name: null, - filename: null - }; - if(currentHeaders["content-disposition"]) { - // Split the content-disposition header into semicolon-delimited parts - const parts = currentHeaders["content-disposition"].split(";").map(part => part.trim()); - // Iterate over each part to extract name and filename if they exist - parts.forEach(part => { - if(part.startsWith("name=")) { - // Remove "name=" and trim quotes - contentDisposition.name = part.substring(6,part.length - 1); - } else if(part.startsWith("filename=")) { - // Remove "filename=" and trim quotes - contentDisposition.filename = part.substring(10,part.length - 1); - } - }); - } - processingPart = true; - options.cbPartStart(currentHeaders,contentDisposition.name,contentDisposition.filename); - // Slice the buffer to the next part - buffer = Uint8Array.prototype.slice.call(buffer,endOfHeaders + 4); - } else { - const boundaryIndex = buffer.indexOf(boundaryBuffer); - if(boundaryIndex >= 0) { - // Return the part up to the boundary minus the terminating LF CR - options.cbPartChunk(Uint8Array.prototype.slice.call(buffer,0,boundaryIndex - 2)); - options.cbPartEnd(); - processingPart = false; - buffer = Uint8Array.prototype.slice.call(buffer,boundaryIndex); - } else { - // Return the rest of the buffer - options.cbPartChunk(buffer); - // Reset the buffer and wait for more data - buffer = Buffer.alloc(0); - break; - } - } - } - }); - // All done - request.on("end", () => { - options.cbFinished(null); - }); -} - -/* -Make an etag. Options include: -bag_name: -tiddler_id: -*/ -function makeTiddlerEtag(options) { - if(options.bag_name || options.tiddler_id) { - return "\"tiddler:" + options.bag_name + "/" + options.tiddler_id + "\""; - } else { - throw "Missing bag_name or tiddler_id"; - } -} - -Server.prototype.defaultVariables = { - port: "8080", - host: "127.0.0.1", - "required-plugins": "$:/plugins/tiddlywiki/filesystem,$:/plugins/tiddlywiki/tiddlyweb", - "root-tiddler": "$:/core/save/all", - "root-render-type": "text/plain", - "root-serve-type": "text/html", - "tiddler-render-type": "text/html", - "tiddler-render-template": "$:/core/templates/server/static.tiddler.html", - "system-tiddler-render-type": "text/plain", - "system-tiddler-render-template": "$:/core/templates/wikified-tiddler", - "debug-level": "none", - "gzip": "no", - "use-browser-cache": "no" -}; - -Server.prototype.get = function(name) { - return this.variables[name]; -}; - -Server.prototype.addRoute = function(route) { - this.routes.push(route); -}; - -Server.prototype.addAuthenticator = function(AuthenticatorClass) { - // Instantiate and initialise the authenticator - var authenticator = new AuthenticatorClass(this), - result = authenticator.init(); - if(typeof result === "string") { - $tw.utils.error("Error: " + result); - } else if(result) { - // Only use the authenticator if it initialised successfully - this.authenticators.push(authenticator); - } -}; - -Server.prototype.findMatchingRoute = function(request,state) { - for(var t=0; t { - const parts = cookie.split('='); - if (parts.length >= 2) { - const key = parts[0].trim(); - const value = parts.slice(1).join('=').trim(); - cookies[key] = decodeURIComponent(value); - } - }); - - return cookies; -} - -Server.prototype.authenticateUser = function(request, response) { - const {session: session_id} = this.parseCookieString(request.headers.cookie) - if (!session_id) { - return false; - } - // get user info - const user = this.sqlTiddlerDatabase.findUserBySessionId(session_id); - if (!user) { - return false - } - delete user.password; - const userRole = this.sqlTiddlerDatabase.getUserRoles(user.user_id); - user['isAdmin'] = userRole?.role_name?.toLowerCase() === 'admin' - user['sessionId'] = session_id - - return user -}; - -Server.prototype.requestAuthentication = function(response) { - if(!response.headersSent) { - response.writeHead(401, { - 'WWW-Authenticate': 'Basic realm="Secure Area"' - }); - response.end('Authentication required.'); - } -}; - -// Check if the anonymous IO configuration is set to allow both reads and writes -Server.prototype.getAnonymousAccessConfig = function() { - const allowReadsTiddler = this.wiki.getTiddlerText("$:/config/MultiWikiServer/AllowAnonymousReads", "undefined"); - const allowWritesTiddler = this.wiki.getTiddlerText("$:/config/MultiWikiServer/AllowAnonymousWrites", "undefined"); - const showAnonymousAccessModal = this.wiki.getTiddlerText("$:/config/MultiWikiServer/ShowAnonymousAccessModal", "undefined"); - - return { - allowReads: allowReadsTiddler === "yes", - allowWrites: allowWritesTiddler === "yes", - isEnabled: allowReadsTiddler !== "undefined" && allowWritesTiddler !== "undefined", - showAnonConfig: showAnonymousAccessModal === "yes" - }; -} - - -Server.prototype.requestHandler = function(request,response,options) { - options = options || {}; - const queryString = require("querystring"); - - // Authenticate the user - const authenticatedUser = this.authenticateUser(request, response); - const authenticatedUsername = authenticatedUser?.username; - - // Compose the state object - var self = this; - var state = {}; - state.wiki = options.wiki || self.wiki; - state.boot = options.boot || self.boot; - state.server = self; - state.urlInfo = url.parse(request.url); - state.queryParameters = querystring.parse(state.urlInfo.query); - state.pathPrefix = options.pathPrefix || this.get("path-prefix") || ""; - state.sendResponse = sendResponse.bind(self,request,response); - state.redirect = redirect.bind(self,request,response); - state.streamMultipartData = streamMultipartData.bind(self,request); - state.makeTiddlerEtag = makeTiddlerEtag.bind(self); - state.authenticatedUser = authenticatedUser; - state.authenticatedUsername = authenticatedUsername; - - // Get the principals authorized to access this resource - state.authorizationType = options.authorizationType || this.methodMappings[request.method] || "readers"; - - // Check whether anonymous access is granted - state.allowAnon = false; //this.isAuthorized(state.authorizationType,null); - var {allowReads, allowWrites, isEnabled, showAnonConfig} = this.getAnonymousAccessConfig(); - state.anonAccessConfigured = isEnabled; - state.allowAnon = isEnabled && (request.method === 'GET' ? allowReads : allowWrites); - state.allowAnonReads = allowReads; - state.allowAnonWrites = allowWrites; - state.showAnonConfig = !!state.authenticatedUser?.isAdmin && showAnonConfig; - state.firstGuestUser = this.sqlTiddlerDatabase.listUsers().length === 0 && !state.authenticatedUser; - - // Authorize with the authenticated username - if(!this.isAuthorized(state.authorizationType,state.authenticatedUsername) && !response.headersSent) { - response.writeHead(403,"'" + state.authenticatedUsername + "' is not authorized to access '" + this.servername + "'"); - response.end(); - return; - } - - // Find the route that matches this path - var route = self.findMatchingRoute(request,state); - - // If the route is configured to use ACL middleware, check that the user has permission - if(route?.useACL) { - const permissionName = this.methodACLPermMappings[route.method]; - aclMiddleware(request,response,state,route.entityName,permissionName) - } - - // Optionally output debug info - if(self.get("debug-level") !== "none") { - console.log("Request path:",JSON.stringify(state.urlInfo)); - console.log("Request headers:",JSON.stringify(request.headers)); - console.log("authenticatedUsername:",state.authenticatedUsername); - } - - // Return a 404 if we didn't find a route - if(!route && !response.headersSent) { - response.writeHead(404); - response.end(); - return; - } - - // If this is a write, check for the CSRF header unless globally disabled, or disabled for this route - if(!this.csrfDisable && !route.csrfDisable && state.authorizationType === "writers" && request.headers["x-requested-with"] !== "TiddlyWiki" && !response.headersSent) { - response.writeHead(403,"'X-Requested-With' header required to login to '" + this.servername + "'"); - response.end(); - return; - } - if (response.headersSent) return; - // Receive the request body if necessary and hand off to the route handler - if(route.bodyFormat === "stream" || request.method === "GET" || request.method === "HEAD") { - // Let the route handle the request stream itself - route.handler(request,response,state); - } else if(route.bodyFormat === "string" || route.bodyFormat === "www-form-urlencoded" || !route.bodyFormat) { - // Set the encoding for the incoming request - request.setEncoding("utf8"); - var data = ""; - request.on("data",function(chunk) { - data += chunk.toString(); - }); - request.on("end",function() { - if(route.bodyFormat === "www-form-urlencoded") { - data = queryString.parse(data); - } - state.data = data; - route.handler(request,response,state); - }); - } else if(route.bodyFormat === "buffer") { - var data = []; - request.on("data",function(chunk) { - data.push(chunk); - }); - request.on("end",function() { - state.data = Buffer.concat(data); - route.handler(request,response,state); - }) - } else { - response.writeHead(400,"Invalid bodyFormat " + route.bodyFormat + " in route " + route.method + " " + route.path.source); - response.end(); - } -}; - -/* -Listen for requests -port: optional port number (falls back to value of "port" variable) -host: optional host address (falls back to value of "host" variable) -prefix: optional prefix (falls back to value of "path-prefix" variable) -callback: optional callback(err) to be invoked when the listener is up and running -*/ -Server.prototype.listen = function(port,host,prefix,options) { - var self = this; - // Handle defaults for port and host - port = port || this.get("port"); - host = host || this.get("host"); - prefix = prefix || this.get("path-prefix") || ""; - // Check for the port being a string and look it up as an environment variable - if(parseInt(port,10).toString() !== port) { - port = process.env[port] || 8080; - } - // Warn if required plugins are missing - var missing = []; - for (var index=0; index 0) { - var error = "Warning: Plugin(s) required for client-server operation are missing.\n"+ - "\""+ missing.join("\", \"")+"\""; - $tw.utils.warning(error); - } - // Create the server - var server = this.transport.createServer(this.listenOptions || {},function(request,response,options) { - if(self.get("debug-level") !== "none") { - var start = $tw.utils.timer(); - response.on("finish",function() { - console.log("Response time:",request.method,request.url,$tw.utils.timer() - start); - }); - } - self.requestHandler(request,response,options); - }); - // Display the port number after we've started listening (the port number might have been specified as zero, in which case we will get an assigned port) - server.on("listening",function() { - // Stop listening when we get the "th-quit" hook - $tw.hooks.addHook("th-quit",function() { - server.close(); - }); - // Log listening details - var address = server.address(), - url = self.protocol + "://" + (address.family === "IPv6" ? "[" + address.address + "]" : address.address) + ":" + address.port + prefix; - $tw.utils.log("Serving on " + url,"brown/orange"); - $tw.utils.log("(press ctrl-C to exit)","red"); - if(options.callback) { - options.callback(null); - } - }); - // Listen - return server.listen(port,host); -}; - -exports.Server = Server; - -})(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/router.js b/plugins/tiddlywiki/multiwikiserver/modules/router.js new file mode 100644 index 00000000000..077da70d508 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/router.js @@ -0,0 +1,591 @@ +/*\ +title: $:/plugins/tiddlywiki/multiwikiserver/router.js +type: application/javascript +module-type: library + +Serve tiddlers over http + +\*/ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Router = void 0; +const assert_1 = require("assert"); +const zlib_1 = require("zlib"); +const crypto_1 = require("crypto"); +const sql_tiddler_store_1 = require("./store/sql-tiddler-store"); +const server_1 = require("./server"); +const acl = require("$:/plugins/tiddlywiki/multiwikiserver/routes/helpers/acl-middleware.js"); +const FinishedResponse = Symbol("FinishedResponse"); +class Router { + constructor(options) { + var _a; + this.routes = []; + this.authenticators = []; + this.methodMappings = { + "GET": "readers", + "OPTIONS": "readers", + "HEAD": "readers", + "PUT": "writers", + "POST": "writers", + "DELETE": "writers" + }; + this.methodACLPermMappings = { + "GET": "READ", + "OPTIONS": "READ", + "HEAD": "READ", + "PUT": "WRITE", + "POST": "WRITE", + "DELETE": "WRITE" + }; + this.store = options.store; + this.wiki = options.wiki; + // delete falsy keys so that we can use the default values + for (const key in options.variables) { + if (!options.variables[key]) { + delete options.variables[key]; + } + } + // Initialise the variables (Object.assign scoops peroperties up from left to right) + this.variables = Object.assign({}, server_1.defaultVariables, (_a = options.variables) !== null && _a !== void 0 ? _a : {}); + // Setup the default required plugins + this.requiredPlugins = this.get("required-plugins").split(','); + // Initialise CSRF + this.csrfDisable = this.get("csrf-disable") === "yes"; + // Initialize Gzip compression + this.enableGzip = this.get("gzip") === "yes"; + // Initialize browser-caching + this.enableBrowserCache = this.get("use-browser-cache") === "yes"; + // Warn if required plugins are missing + const missing = this.requiredPlugins.filter(title => { + return !$tw.wiki.getTiddler(title); + }); + if (missing.length > 0) { + const error = "Warning: Plugin(s) required for client-server operation are missing.\n" + + "\"" + missing.join("\", \"") + "\""; + $tw.utils.warning(error); + } + // Initialise authorization + let authorizedUserName; + if (this.get("username") && this.get("password")) { + authorizedUserName = this.get("username") || ""; //redundant for type checker + } + else if (this.get("credentials")) { + authorizedUserName = "(authenticated)"; + } + else { + authorizedUserName = "(anon)"; + } + this.authorizations = { + readers: (this.get("readers") || authorizedUserName).split(",").map($tw.utils.trim), + writers: (this.get("writers") || authorizedUserName).split(",").map($tw.utils.trim), + /** @type {string[] | undefined} */ + admin: undefined + }; + if (this.get("admin") || authorizedUserName !== "(anon)") { + this.authorizations["admin"] = (this.get("admin") || authorizedUserName).split(',').map($tw.utils.trim); + } + // Load and initialise authenticators + $tw.modules.forEachModuleOfType("authenticator", (title, authenticatorDefinition) => { + this.addAuthenticator(authenticatorDefinition.AuthenticatorClass, title); + }); + // Load route handlers + $tw.modules.forEachModuleOfType("mws-route", (title, routeDefinition) => { + this.addRoute(routeDefinition, title); + }); + } + get(key) { + return this.variables[key]; + } + addRoute(route, title) { + if (!route) + throw new Error(`Route ${title} is not defined`); + if (this.methodMappings[route.method] === undefined) { + throw new Error(`Route ${title} does not have a valid method, expected one of ${Object.keys(this.methodMappings).join(", ")}`); + } + if (!route.path) { + throw new Error(`Route ${title} does not have a path`); + } + if (route.useACL) { + const permissionName = this.methodACLPermMappings[route.method]; + if (!route.entityName) { + throw new Error(`Route ${title} is configured to use ACL middleware but does not specify an entityName`); + } + if (!permissionName) { + throw new Error(`Route ${title} is configured to use ACL middleware but the route method does not support ACL`); + } + } + this.routes.push(route); + } + addAuthenticator(AuthenticatorClass, title) { + // Instantiate and initialise the authenticator + var authenticator = new AuthenticatorClass(this), result = authenticator.init(); + if (typeof result === "string") { + $tw.utils.error("Error: " + result); + } + else if (result) { + // Only use the authenticator if it initialised successfully + this.authenticators.push(authenticator); + } + } + serverManagerRequestHandler(server, request, response) { + // $tw.mws.connection.$transaction(async (prisma) => { + // the database transaction gets committed when the promise resolves + // and rolled back if it rejects + this.routeRequest(server, request, response).catch(console.error); + // },{maxWait: 30000, timeout: 20000}).catch(console.error); + } + async routeRequest(server, request, response, options) { + // returning the response object in order to make sure we call response.end() because the route + // handler should never resolve the promise until it finishes writing the response. + // After that, the database connection gets closed or returned to the pool. + // An error should be thrown if the connection has unfinished business when end is called, + // because that probably means we forgot to await something. + if (this.get("debug-level") !== "none") { + var start = $tw.utils.timer(); + response.on("finish", function () { + console.log("Response time:", request.method, request.url, $tw.utils.timer() - start); + }); + } + // Compose the state object + const state = await this.makeRequestState(server, request, response, options); + // Authorize with the authenticated username + if (!this.isAuthorized(state.authorizationType, state.authenticatedUsername)) { + if (!response.headersSent) + response.writeHead(403, "'" + state.authenticatedUsername + "' is not authorized"); + return state.end(); + } + // Find the route that matches this path + var route = this.findMatchingRoute(request, state); + // Return a 404 if we didn't find a route + if (!route) { + if (!response.headersSent) + response.writeHead(404); + return state.end(); + } + // If the route is configured to use ACL middleware, check that the user has permission + if (route.useACL) { + (0, assert_1.ok)(route.entityName); + const permissionName = this.methodACLPermMappings[route.method]; + await acl.middleware(request, response, state, route.entityName, permissionName); + } + // Optionally output debug info + if (this.get("debug-level") !== "none") { + console.log("Request path:", JSON.stringify(state.urlInfo)); + console.log("Request headers:", JSON.stringify(request.headers)); + console.log("authenticatedUsername:", state.authenticatedUsername); + } + // If this is a write, check for the CSRF header unless globally disabled, or disabled for this route + if (!this.csrfDisable + && !route.csrfDisable + && state.authorizationType === "writers" + && request.headers["x-requested-with"] !== "TiddlyWiki" + && !response.headersSent) { + if (!response.headersSent) + response.writeHead(403, "'X-Requested-With' header required to login"); + return state.end(); + } + // if we've sent headers already, it was a denied request + if (response.headersSent) + return state.end(); + // Receive the request body if necessary and hand off to the route handler + if (route.bodyFormat === "stream" || request.method === "GET" || request.method === "HEAD") { + // Let the route handle the request stream itself + await route.handler(request, response, state); + return state.end(); + } + else if (route.bodyFormat === "string" + || route.bodyFormat === "www-form-urlencoded" + || !route.bodyFormat) { + // Set the encoding for the incoming request + request.setEncoding("utf8"); + await this.readBodyString(request, route, state); + await route.handler(request, response, state); + return state.end(); + } + else if (route.bodyFormat === "buffer") { + await this.readBodyBuffer(request, state); + await route.handler(request, response, state); + return state.end(); + } + else { + $tw.utils.warning(`Invalid bodyFormat ${route.bodyFormat} in route ${route.method} ${route.path.source}`); + response.writeHead(500); + return state.end(); + } + } + findMatchingRoute(request, state) { + for (var t = 0; t < this.routes.length; t++) { + var potentialRoute = this.routes[t], pathRegExp = potentialRoute.path, pathname = state.urlInfo.pathname, match; + if (state.pathPrefix) { + if (pathname.substr(0, state.pathPrefix.length) === state.pathPrefix) { + pathname = pathname.substr(state.pathPrefix.length) || "/"; + match = pathRegExp.exec(pathname); + } + else { + match = null; + } + } + else { + match = pathRegExp.exec(pathname); + } + // Allow POST as a synonym for PUT because HTML doesn't allow PUT forms + if (match && (request.method === potentialRoute.method || (request.method === "POST" && potentialRoute.method === "PUT"))) { + for (var p = 1; p < match.length; p++) { + state.params.push(match[p]); + } + return potentialRoute; + } + } + return null; + } + ; + async readBodyString(request, route, state) { + await (new Promise((resolve) => { + let data = ""; + request.on("data", function (chunk) { + data += chunk.toString(); + }); + request.on("end", function () { + if (route.bodyFormat === "www-form-urlencoded") { + state.data = new URLSearchParams(data); + } + else { + state.data = data; + } + resolve(); + }); + })); + } + async readBodyBuffer(request, state) { + await (new Promise((resolve) => { + const data = []; + request.on("data", function (chunk) { + data.push(chunk); + }); + request.on("end", function () { + state.data = Buffer.concat(data); + resolve(); + }); + })); + } + async authenticateUser(request, response) { + var _a; + const { session: session_id } = this.parseCookieString(request.headers.cookie); + if (!session_id) { + return null; + } + // get user info + const user = await this.store.sql.findUserBySessionId(session_id); + if (!user) { + return null; + } + //@ts-expect-error because password is not optional + delete user.password; + const userRole = await this.store.sql.getUserRoles(user.user_id); + return Object.assign(Object.assign({}, user), { isAdmin: ((_a = userRole === null || userRole === void 0 ? void 0 : userRole.role_name) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'admin', sessionId: session_id, password: undefined }); + } + ; + isAuthorized(authorizationType, username) { + var principals = this.authorizations[authorizationType] || []; + return principals.indexOf("(anon)") !== -1 + || (username && (principals.indexOf("(authenticated)") !== -1 + || principals.indexOf(username) !== -1)); + } + requestAuthentication(response) { + if (!response.headersSent) { + response.writeHead(401, { + 'WWW-Authenticate': 'Basic realm="Secure Area"' + }); + response.end('Authentication required.'); + } + } + ; + // Check if the anonymous IO configuration is set to allow both reads and writes + getAnonymousAccessConfig() { + const allowReadsTiddler = this.store.adminWiki.getTiddlerText("$:/config/MultiWikiServer/AllowAnonymousReads", "undefined"); + const allowWritesTiddler = this.store.adminWiki.getTiddlerText("$:/config/MultiWikiServer/AllowAnonymousWrites", "undefined"); + const showAnonymousAccessModal = this.store.adminWiki.getTiddlerText("$:/config/MultiWikiServer/ShowAnonymousAccessModal", "undefined"); + return { + allowReads: allowReadsTiddler === "yes", + allowWrites: allowWritesTiddler === "yes", + isEnabled: allowReadsTiddler !== "undefined" && allowWritesTiddler !== "undefined", + showAnonConfig: showAnonymousAccessModal === "yes" + }; + } + parseCookieString(cookieString) { + const cookies = {}; + if (typeof cookieString !== 'string') + return cookies; + cookieString.split(';').forEach(cookie => { + const parts = cookie.split('='); + if (parts.length >= 2) { + const key = parts[0].trim(); + const value = parts.slice(1).join('=').trim(); + cookies[key] = decodeURIComponent(value); + } + }); + return cookies; + } + async makeRequestState(server, request, response, options = {}) { + // Authenticate the user + const authenticatedUser = await this.authenticateUser(request, response); + const authenticatedUsername = authenticatedUser === null || authenticatedUser === void 0 ? void 0 : authenticatedUser.username; + // Get the principals authorized to access this resource + const authorizationType = options.authorizationType + || this.methodMappings[request.method] + || "readers"; + var { allowReads, allowWrites, isEnabled, showAnonConfig } = this.getAnonymousAccessConfig(); + // // this is slightly hacky, but we're sending the connection off through the various channels. + // // I mean, we could just pass the connection to the handler, but that would be too easy. + // const { prisma, finish } = await new Promise<{ + // prisma: PrismaTxnClient, + // finish: (value: void | PromiseLike) => void + // }>((resolveInit) => { + // $tw.mws.connection.$transaction(async (prisma) => { + // await new Promise((finish) => { + // resolveInit({ prisma, finish }); + // }); + // }); + // }); + const urlInfo = new URL(request.url, server.origin()); + return { + authorizationType, + pathPrefix: options.pathPrefix || server.pathPrefix || "", + store: new sql_tiddler_store_1.SqlTiddlerStore({ + attachmentStore: this.store.attachmentStore, + adminWiki: this.store.adminWiki, + prisma: $tw.mws.connection, + }), + urlInfo, + queryParameters: urlInfo.searchParams, + sendResponse: this.sendResponse.bind(this, request, response), + redirect: this.redirect.bind(this, request, response), + streamMultipartData: this.streamMultipartData.bind(this, request), + makeTiddlerEtag: this.makeTiddlerEtag.bind(this), + authenticatedUser, + authenticatedUsername, + anonAccessConfigured: isEnabled, + allowAnon: isEnabled && (request.method === 'GET' ? allowReads : allowWrites), + allowAnonReads: allowReads, + allowAnonWrites: allowWrites, + showAnonConfig: !!(authenticatedUser === null || authenticatedUser === void 0 ? void 0 : authenticatedUser.isAdmin) && showAnonConfig, + firstGuestUser: !authenticatedUser && (await this.store.sql.listUsers()).length === 0, + data: undefined, + params: [], + end: () => { + // if no response was sent, send a 500 + if (!response.headersSent) { + response.writeHead(500); + response.end("Internal server error"); + $tw.utils.error("Response not sent " + request.method + " " + request.url); + } + else { + response.write = () => { + throw new Error("Cannot write to response after it has been ended"); + }; + response.end(); + } + // + return { [FinishedResponse]: true }; + } + }; + } + sendResponse(request, response, statusCode, headers, data, encoding) { + if (typeof data === "string" && encoding === undefined) { + $tw.utils.error("Missing encoding for string data, we assume utf8"); + encoding = "utf8"; + } + if (this.enableBrowserCache && (statusCode == 200)) { + var hash = (0, crypto_1.createHash)('md5'); + // Put everything into the hash that could change and invalidate the data that + // the browser already stored. The headers the data and the encoding. + if (data !== undefined) + hash.update(data); + hash.update(JSON.stringify(headers)); + if (encoding) { + hash.update(encoding); + } + var contentDigest = hash.digest("hex"); + // RFC 7232 section 2.3 mandates for the etag to be enclosed in quotes + headers["Etag"] = '"' + contentDigest + '"'; + headers["Cache-Control"] = "max-age=0, must-revalidate"; + // Check if any of the hashes contained within the if-none-match header + // matches the current hash. + // If one matches, do not send the data but tell the browser to use the + // cached data. + // We do not implement "*" as it makes no sense here. + var ifNoneMatch = request.headers["if-none-match"]; + if (ifNoneMatch) { + var matchParts = ifNoneMatch.split(",").map(function (/** @type {string} */ etag) { + return etag.replace(/^[ "]+|[ "]+$/g, ""); + }); + if (matchParts.indexOf(contentDigest) != -1) { + response.writeHead(304, headers); + response.end(); + return; + } + } + } + /* + If the gzip=yes is set, check if the user agent permits compression. If so, + compress our response if the raw data is bigger than 2k. Compressing less + data is inefficient. Note that we use the synchronous functions from zlib + to stay in the imperative style. The current `Server` doesn't depend on + this, and we may just as well use the async versions. + */ + if (this.enableGzip && data && (data.length > 2048)) { + var acceptEncoding = request.headers["accept-encoding"] || ""; + if (/\bdeflate\b/.test(acceptEncoding)) { + headers["Content-Encoding"] = "deflate"; + data = (0, zlib_1.deflateSync)(data); + } + else if (/\bgzip\b/.test(acceptEncoding)) { + headers["Content-Encoding"] = "gzip"; + data = (0, zlib_1.gzipSync)(data); + } + } + if (!response.headersSent) { + response.writeHead(statusCode, headers); + if (typeof data === "string") + response.end(data, encoding !== null && encoding !== void 0 ? encoding : "utf8"); + else + response.end(data); + } + } + /** + * @this {Server} + * @param {IncomingMessage} request + * @param {ServerResponse} response + * @param {number} statusCode + * @param {string} location + */ + redirect(request, response, statusCode, location) { + response.setHeader("Location", location); + response.statusCode = statusCode; + response.end(); + } + /* + Options include: + cbPartStart(headers,name,filename) - invoked when a file starts being received + cbPartChunk(chunk) - invoked when a chunk of a file is received + cbPartEnd() - invoked when a file finishes being received + cbFinished(err) - invoked when the all the form data has been processed + */ + /** + * + * @param {import("http").IncomingMessage} request + * @param {Object} options + * @param {(headers: Object, name: string | null, filename: string | null) => void} options.cbPartStart + * @param {(chunk: Buffer) => void} options.cbPartChunk + * @param {() => void} options.cbPartEnd + * @param {(err: string | null) => void} options.cbFinished + */ + streamMultipartData(request, options) { + // Check that the Content-Type is multipart/form-data + const contentType = request.headers['content-type']; + if (!(contentType === null || contentType === void 0 ? void 0 : contentType.startsWith("multipart/form-data"))) { + return options.cbFinished("Expected multipart/form-data content type"); + } + // Extract the boundary string from the Content-Type header + const boundaryMatch = contentType.match(/boundary=(.+)$/); + if (!boundaryMatch) { + return options.cbFinished("Missing boundary in multipart/form-data"); + } + const boundary = boundaryMatch[1]; + const boundaryBuffer = Buffer.from("--" + boundary); + // Initialise + let buffer = Buffer.alloc(0); + let processingPart = false; + // Process incoming chunks + request.on("data", (chunk) => { + // Accumulate the incoming data + buffer = Buffer.concat([buffer, chunk]); + // Loop through any parts within the current buffer + while (true) { + if (!processingPart) { + // If we're not processing a part then we try to find a boundary marker + const boundaryIndex = buffer.indexOf(boundaryBuffer); + if (boundaryIndex === -1) { + // Haven't reached the boundary marker yet, so we should wait for more data + break; + } + // Look for the end of the headers + const endOfHeaders = buffer.indexOf("\r\n\r\n", boundaryIndex + boundaryBuffer.length); + if (endOfHeaders === -1) { + // Haven't reached the end of the headers, so we should wait for more data + break; + } + // Extract and parse headers + const headersPart = Uint8Array.prototype.slice.call(buffer, boundaryIndex + boundaryBuffer.length, endOfHeaders).toString(); + /** @type {Record} */ + const currentHeaders = {}; + headersPart.split("\r\n").forEach(headerLine => { + const [key, value] = headerLine.split(": "); + currentHeaders[key.toLowerCase()] = value; + }); + // Parse the content disposition header + const contentDisposition = { + name: null, + filename: null + }; + if (currentHeaders["content-disposition"]) { + // Split the content-disposition header into semicolon-delimited parts + const parts = currentHeaders["content-disposition"].split(";").map(part => part.trim()); + // Iterate over each part to extract name and filename if they exist + parts.forEach(part => { + if (part.startsWith("name=")) { + // Remove "name=" and trim quotes + contentDisposition.name = part.substring(6, part.length - 1); + } + else if (part.startsWith("filename=")) { + // Remove "filename=" and trim quotes + contentDisposition.filename = part.substring(10, part.length - 1); + } + }); + } + processingPart = true; + options.cbPartStart(currentHeaders, contentDisposition.name, contentDisposition.filename); + // Slice the buffer to the next part + buffer = Buffer.from(buffer.slice(endOfHeaders + 4)); + } + else { + const boundaryIndex = buffer.indexOf(boundaryBuffer); + if (boundaryIndex >= 0) { + // Return the part up to the boundary minus the terminating LF CR + options.cbPartChunk(Buffer.from(buffer.slice(0, boundaryIndex - 2))); + options.cbPartEnd(); + processingPart = false; + buffer = Buffer.from(buffer.slice(boundaryIndex)); + } + else { + // Return the rest of the buffer + options.cbPartChunk(buffer); + // Reset the buffer and wait for more data + buffer = Buffer.alloc(0); + break; + } + } + } + }); + // All done + request.on("end", () => { + options.cbFinished(null); + }); + } + /** + Make an etag. + @param {Object} options + @param {string} options.bag_name + @param {string} options.tiddler_id + */ + makeTiddlerEtag(options) { + if (options.bag_name || options.tiddler_id) { + return "\"tiddler:" + options.bag_name + "/" + options.tiddler_id + "\""; + } + else { + throw "Missing bag_name or tiddler_id"; + } + } +} +exports.Router = Router; +//# sourceMappingURL=router.js.map \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/modules/router.js.map b/plugins/tiddlywiki/multiwikiserver/modules/router.js.map new file mode 100644 index 00000000000..ce31148f522 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/router.js.map @@ -0,0 +1 @@ +{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/change-user-password.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/change-user-password.js index 303b8e4e6e6..d5e91ecfeeb 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/change-user-password.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/change-user-password.js @@ -21,15 +21,11 @@ exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; -exports.handler = function (request, response, state) { - var userId = state.data.userId; - // Clean up any existing error/success messages - $tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/change-password/" + userId + "/error"); - $tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/change-password/" + userId + "/success"); - $tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/login/error"); +/** @type {ServerRouteHandler<0, "www-form-urlencoded">} */ +exports.handler = async function (request, response, state) { if(!state.authenticatedUser) { - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/login/error", text: "You must be logged in to change passwords" })); @@ -37,16 +33,27 @@ exports.handler = function (request, response, state) { response.end(); return; } + var userId = state.data.get("userId"); + var newPassword = state.data.get("newPassword"); + var confirmPassword = state.data.get("confirmPassword"); + if(!userId || !newPassword || !confirmPassword) { + response.writeHead(400); + response.end(); + return; + } + // Clean up any existing error/success messages + state.store.adminWiki.deleteTiddler("$:/temp/mws/change-password/" + userId + "/error"); + state.store.adminWiki.deleteTiddler("$:/temp/mws/change-password/" + userId + "/success"); + state.store.adminWiki.deleteTiddler("$:/temp/mws/login/error"); + + var auth = authenticator(state.store.sql); - var auth = authenticator(state.server.sqlTiddlerDatabase); - var newPassword = state.data.newPassword; - var confirmPassword = state.data.confirmPassword; var currentUserId = state.authenticatedUser.user_id; var hasPermission = ($tw.utils.parseInt(userId) === currentUserId) || state.authenticatedUser.isAdmin; if(!hasPermission) { - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/change-password/" + userId + "/error", text: "You don't have permission to change this user's password" })); @@ -56,7 +63,7 @@ exports.handler = function (request, response, state) { } if(newPassword !== confirmPassword) { - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/change-password/" + userId + "/error", text: "New passwords do not match" })); @@ -65,10 +72,10 @@ exports.handler = function (request, response, state) { return; } - var userData = state.server.sqlTiddlerDatabase.getUser(userId); + var userData = await state.store.sql.getUser(userId); if(!userData) { - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/change-password/" + userId + "/error", text: "User not found" })); @@ -78,9 +85,9 @@ exports.handler = function (request, response, state) { } var newHash = auth.hashPassword(newPassword); - var result = state.server.sqlTiddlerDatabase.updateUserPassword(userId, newHash); + var result = await state.store.sql.updateUserPassword(userId, newHash); - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/change-password/" + userId + "/success", text: result.message })); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-acl.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-acl.js index a4c4768e4e5..07e5b52b14b 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-acl.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-acl.js @@ -6,13 +6,14 @@ module-type: mws-route POST /admin/delete-acl \*/ -(function () { +(function() { /*jslint node: true, browser: true */ /*global $tw: false */ "use strict"; - var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware; + + var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/routes/helpers/acl-middleware.js").middleware; exports.method = "POST"; @@ -23,18 +24,20 @@ POST /admin/delete-acl exports.csrfDisable = true; - exports.handler = function (request, response, state) { - var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase; - var recipe_name = state.data.recipe_name; - var bag_name = state.data.bag_name; - var acl_id = state.data.acl_id; - var entity_type = state.data.entity_type; - - aclMiddleware(request, response, state, entity_type, "WRITE"); + /** @type {ServerRouteHandler<0, "www-form-urlencoded">} */ + exports.handler = async function(request, response, state) { + var sqlTiddlerDatabase = state.store.sql; + var recipe_name = state.data.get("recipe_name"); + var bag_name = state.data.get("bag_name"); + var acl_id = state.data.get("acl_id"); + var entity_type = state.data.get("entity_type"); - sqlTiddlerDatabase.deleteACL(acl_id); + await aclMiddleware(request, response, state, entity_type, "WRITE"); + if(response.headersSent) return; + + await sqlTiddlerDatabase.deleteACL(acl_id); - response.writeHead(302, { "Location": "/admin/acl/" + recipe_name + "/" + bag_name }); + response.writeHead(302, {"Location": "/admin/acl/" + recipe_name + "/" + bag_name}); response.end(); }; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-bag-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-bag-tiddler.js index 722ef2d8c09..998998c2734 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-bag-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-bag-tiddler.js @@ -12,20 +12,21 @@ DELETE /bags/:bag_name/tiddler/:title /*global $tw: false */ "use strict"; -var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware; +var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/routes/helpers/acl-middleware.js").middleware; exports.method = "DELETE"; exports.path = /^\/bags\/([^\/]+)\/tiddlers\/(.+)$/; - -exports.handler = function(request,response,state) { - aclMiddleware(request, response, state, "bag", "WRITE"); +/** @type {ServerRouteHandler<2>} */ +exports.handler = async function(request,response,state) { + await aclMiddleware(request, response, state, "bag", "WRITE"); + if(response.headersSent) return; // Get the parameters var bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]), title = $tw.utils.decodeURIComponentSafe(state.params[1]); if(bag_name) { if(!response.headersSent) { - var result = $tw.mws.store.deleteTiddler(title,bag_name); + var result = await state.store.deleteTiddler(title,bag_name); response.writeHead(204, "OK", { "X-Revision-Number": result.tiddler_id.toString(), Etag: state.makeTiddlerEtag(result), diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-role.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-role.js index 571545b1519..da787e6ebae 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-role.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-role.js @@ -19,10 +19,10 @@ POST /admin/delete-role exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; - - exports.handler = function (request, response, state) { - var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase; - var role_id = state.data.role_id; + /** @type {ServerRouteHandler<0,"www-form-urlencoded">} */ + exports.handler = async function (request, response, state) { + var sqlTiddlerDatabase = state.store.sql; + var role_id = state.data.get("role_id"); if(!state.authenticatedUser || !state.authenticatedUser.isAdmin) { response.writeHead(403, "Forbidden"); @@ -31,7 +31,7 @@ POST /admin/delete-role } // Check if the role exists - var role = sqlTiddlerDatabase.getRoleById(role_id); + var role = await sqlTiddlerDatabase.getRoleById(role_id); if(!role) { response.writeHead(404, "Not Found"); response.end("Role not found"); @@ -39,13 +39,13 @@ POST /admin/delete-role } // Check if the role is in use - var isRoleInUse = sqlTiddlerDatabase.isRoleInUse(role_id); + var isRoleInUse = await sqlTiddlerDatabase.isRoleInUse(role_id); if(isRoleInUse) { - sqlTiddlerDatabase.deleteUserRolesByRoleId(role_id); + await sqlTiddlerDatabase.deleteUserRolesByRoleId(role_id); } // Delete the role - sqlTiddlerDatabase.deleteRole(role_id); + await sqlTiddlerDatabase.deleteRole(role_id); // Redirect back to the roles management page response.writeHead(302, { "Location": "/admin/roles" }); response.end(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-user-account.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-user-account.js index 5fb0f219f28..b03872fcdcf 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-user-account.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-user-account.js @@ -19,14 +19,15 @@ exports.path = /^\/delete-user-account\/?$/; exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; +/** @type {ServerRouteHandler<0,"www-form-urlencoded">} */ +exports.handler = async function (request, response, state) { -exports.handler = function (request, response, state) { - var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase; - var userId = state.data.userId; + var userId = state.data.get("userId"); + // Check if user is admin if(!state.authenticatedUser || !state.authenticatedUser.isAdmin) { - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/delete-user/error", text: "You must be an administrator to delete user accounts" })); @@ -37,7 +38,7 @@ exports.handler = function (request, response, state) { // Prevent admin from deleting their own account if(state.authenticatedUser.user_id === userId) { - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/delete-user/error", text: "Cannot delete your own account" })); @@ -47,9 +48,9 @@ exports.handler = function (request, response, state) { } // Check if the user exists - var user = sqlTiddlerDatabase.getUser(userId); + var user = await state.store.sql.getUser(userId); if(!user) { - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/delete-user/error", text: "User not found" })); @@ -59,9 +60,9 @@ exports.handler = function (request, response, state) { } // Check if this is the last admin account - var adminRole = sqlTiddlerDatabase.getRoleByName("ADMIN"); + var adminRole = await state.store.sql.getRoleByName("ADMIN"); if(!adminRole) { - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/delete-user/error", text: "Admin role not found" })); @@ -70,9 +71,9 @@ exports.handler = function (request, response, state) { return; } - var adminUsers = sqlTiddlerDatabase.listUsersByRoleId(adminRole.role_id); + var adminUsers = await state.store.sql.listUsersByRoleId(adminRole.role_id); if(adminUsers.length <= 1 && adminUsers.some(admin => admin.user_id === parseInt(userId))) { - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/delete-user/error", text: "Cannot delete the last admin account" })); @@ -81,9 +82,9 @@ exports.handler = function (request, response, state) { return; } - sqlTiddlerDatabase.deleteUserRolesByUserId(userId); - sqlTiddlerDatabase.deleteUserSessions(userId); - sqlTiddlerDatabase.deleteUser(userId); + await state.store.sql.deleteUserRolesByUserId(userId); + await state.store.sql.deleteUserSessions(userId); + await state.store.sql.deleteUser(userId); // Redirect back to the users management page response.writeHead(302, { "Location": "/admin/users" }); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-acl.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-acl.js index 1c6e2f1b7e8..84dd2a98724 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-acl.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-acl.js @@ -14,15 +14,15 @@ GET /admin/acl exports.method = "GET"; exports.path = /^\/admin\/acl\/(.+)$/; - -exports.handler = function (request, response, state) { - var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase; +/** @type {ServerRouteHandler<1>} */ +exports.handler = async function (request, response, state) { + var sqlTiddlerDatabase = state.store.sql; var params = state.params[0].split("/") var recipeName = params[0]; var bagName = params[params.length - 1]; - var recipes = sqlTiddlerDatabase.listRecipes() - var bags = sqlTiddlerDatabase.listBags() + var recipes = await sqlTiddlerDatabase.listRecipes() + var bags = await sqlTiddlerDatabase.listBags() var recipe = recipes.find((entry) => entry.recipe_name === recipeName && entry.bag_names.includes(bagName)) var bag = bags.find((entry) => entry.bag_name === bagName); @@ -33,16 +33,23 @@ exports.handler = function (request, response, state) { return; } - var recipeAclRecords = sqlTiddlerDatabase.getEntityAclRecords(recipe.recipe_name); - var bagAclRecords = sqlTiddlerDatabase.getEntityAclRecords(bag.bag_name); - var roles = state.server.sqlTiddlerDatabase.listRoles(); - var permissions = state.server.sqlTiddlerDatabase.listPermissions(); + var recipeAclRecords = await sqlTiddlerDatabase.getEntityAclRecords(recipe.recipe_name); + var bagAclRecords = await sqlTiddlerDatabase.getEntityAclRecords(bag.bag_name); + var roles = await state.store.sql.listRoles(); + var permissions = await state.store.sql.listPermissions(); // This ensures that the user attempting to view the ACL management page has permission to do so - if(!state.authenticatedUser?.isAdmin && - !state.firstGuestUser && - (!state.authenticatedUser || (recipeAclRecords.length > 0 && !sqlTiddlerDatabase.hasRecipePermission(state.authenticatedUser.user_id, recipeName, 'WRITE'))) - ){ + async function canContinue() { + if(state.firstGuestUser) return true; + if(!state.authenticatedUser) return false; + if(state.authenticatedUser.isAdmin) return true; + if(recipeAclRecords.length === 0) return false; + return await sqlTiddlerDatabase.hasRecipePermission( + state.authenticatedUser.user_id, recipeName, "WRITE"); + } + + if(!await canContinue()) + { response.writeHead(403, "Forbidden"); response.end(); return @@ -51,35 +58,39 @@ exports.handler = function (request, response, state) { // Enhance ACL records with role and permission details recipeAclRecords = recipeAclRecords.map(record => { var role = roles.find(role => role.role_id === record.role_id); + if(!role) $tw.utils.warning("Role not found for record " + record.acl_id); var permission = permissions.find(perm => perm.permission_id === record.permission_id); + if(!permission) $tw.utils.warning("Permission not found for record " + record.acl_id); return ({ ...record, role, permission, - role_name: role.role_name, - role_description: role.description, - permission_name: permission.permission_name, - permission_description: permission.description + role_name: role?.role_name, + role_description: role?.description, + permission_name: permission?.permission_name, + permission_description: permission?.description }) }); bagAclRecords = bagAclRecords.map(record => { var role = roles.find(role => role.role_id === record.role_id); + if(!role) $tw.utils.warning("Role not found for record " + record.acl_id); var permission = permissions.find(perm => perm.permission_id === record.permission_id); + if(!permission) $tw.utils.warning("Permission not found for record " + record.acl_id); return ({ ...record, role, permission, - role_name: role.role_name, - role_description: role.description, - permission_name: permission.permission_name, - permission_description: permission.description + role_name: role?.role_name, + role_description: role?.description, + permission_name: permission?.permission_name, + permission_description: permission?.description }) }); response.writeHead(200, "OK", { "Content-Type": "text/html" }); - var html = $tw.mws.store.adminWiki.renderTiddler("text/plain", "$:/plugins/tiddlywiki/multiwikiserver/templates/page", { + var html = state.store.adminWiki.renderTiddler("text/plain", "$:/plugins/tiddlywiki/multiwikiserver/templates/page", { variables: { "page-content": "$:/plugins/tiddlywiki/multiwikiserver/templates/manage-acl", "roles-list": JSON.stringify(roles), diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler-blob.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler-blob.js index 28d23212c43..93aebafe4c5 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler-blob.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler-blob.js @@ -12,25 +12,30 @@ GET /bags/:bag_name/tiddler/:title/blob /*global $tw: false */ "use strict"; -var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware; +var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/routes/helpers/acl-middleware.js").middleware; exports.method = "GET"; exports.path = /^\/bags\/([^\/]+)\/tiddlers\/([^\/]+)\/blob$/; - -exports.handler = function(request,response,state) { - aclMiddleware(request, response, state, "bag", "READ"); +/** @type {ServerRouteHandler<2>} */ +exports.handler = async function(request,response,state) { + await aclMiddleware(request, response, state, "bag", "READ"); + if(response.headersSent) return; // Get the parameters const bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]), title = $tw.utils.decodeURIComponentSafe(state.params[1]); if(bag_name) { - const result = $tw.mws.store.getBagTiddlerStream(title,bag_name); + const result = await state.store.getBagTiddlerStream(title,bag_name); if(result && !response.headersSent) { response.writeHead(200, "OK",{ Etag: state.makeTiddlerEtag(result), "Content-Type": result.type, }); result.stream.pipe(response); + await new Promise((resolve,reject) => { + result.stream.on("end",resolve); + result.stream.on("error",reject); + }); return; } } diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler.js index 52b169652b0..ff16906ed6d 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler.js @@ -16,18 +16,19 @@ fallback= // Optional redirect if the tiddler is not found /*global $tw: false */ "use strict"; -var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware; +var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/routes/helpers/acl-middleware.js").middleware; exports.method = "GET"; exports.path = /^\/bags\/([^\/]+)\/tiddlers\/(.+)$/; - -exports.handler = function(request,response,state) { - aclMiddleware(request, response, state, "bag", "READ"); +/** @type {ServerRouteHandler<2>} */ +exports.handler = async function(request,response,state) { + await aclMiddleware(request, response, state, "bag", "READ"); + if(response.headersSent) return; // Get the parameters const bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]), title = $tw.utils.decodeURIComponentSafe(state.params[1]), - tiddlerInfo = $tw.mws.store.getBagTiddler(title,bag_name); + tiddlerInfo = await state.store.getBagTiddler(title,bag_name); if(tiddlerInfo && tiddlerInfo.tiddler) { // If application/json is requested then this is an API request, and gets the response in JSON if(request.headers.accept && request.headers.accept.indexOf("application/json") !== -1) { @@ -38,7 +39,7 @@ exports.handler = function(request,response,state) { return; } else { // This is not a JSON API request, we should return the raw tiddler content - const result = $tw.mws.store.getBagTiddlerStream(title,bag_name); + const result = await state.store.getBagTiddlerStream(title,bag_name); if(result) { if(!response.headersSent){ response.writeHead(200, "OK",{ @@ -47,6 +48,10 @@ exports.handler = function(request,response,state) { }); } result.stream.pipe(response); + await new Promise((resolve,reject) => { + result.stream.on("end",resolve); + result.stream.on("error",reject); + }); return; } else { if(!response.headersSent){ @@ -58,10 +63,10 @@ exports.handler = function(request,response,state) { } } else { // Redirect to fallback URL if tiddler not found - if(state.queryParameters.fallback) { + if(state.queryParameters.get("fallback")) { if (!response.headersSent){ response.writeHead(302, "OK",{ - "Location": state.queryParameters.fallback + "Location": state.queryParameters.get("fallback") }); response.end(); } diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag.js index 7d262b83fcf..4f7431a6077 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag.js @@ -20,16 +20,16 @@ exports.path = /^\/bags\/([^\/]+)(\/?)$/; exports.useACL = true; exports.entityName = "bag" - -exports.handler = function (request, response, state) { +/** @type {ServerRouteHandler<2>} */ +exports.handler = async function (request, response, state) { // Redirect if there is no trailing slash. We do this so that the relative URL specified in the upload form works correctly if (state.params[1] !== "/") { - state.redirect(301, state.urlInfo.path + "/"); + state.redirect(301, state.urlInfo.pathname + "/"); return; } // Get the parameters var bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]), - bagTiddlers = bag_name && $tw.mws.store.getBagTiddlers(bag_name); + bagTiddlers = bag_name && await state.store.getBagTiddlers(bag_name); if (bag_name && bagTiddlers) { // If application/json is requested then this is an API request, and gets the response in JSON if (request.headers.accept && request.headers.accept.indexOf("application/json") !== -1) { @@ -40,7 +40,7 @@ exports.handler = function (request, response, state) { response.writeHead(200, "OK", { "Content-Type": "text/html" }); - var html = $tw.mws.store.adminWiki.renderTiddler("text/plain", "$:/plugins/tiddlywiki/multiwikiserver/templates/page", { + var html = state.store.adminWiki.renderTiddler("text/plain", "$:/plugins/tiddlywiki/multiwikiserver/templates/page", { variables: { "page-content": "$:/plugins/tiddlywiki/multiwikiserver/templates/get-bag", "bag-name": bag_name, diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-index.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-index.js index 1b5dc96fe6f..ecf168f52e4 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-index.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-index.js @@ -1,3 +1,4 @@ +/* eslint-disable implicit-arrow-linebreak */ /*\ title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/get-index.js type: application/javascript @@ -15,37 +16,58 @@ GET /?show_system=true exports.method = "GET"; exports.path = /^\/$/; - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler<0>} */ +exports.handler = async function(request,response,state) { // Get the bag and recipe information - var bagList = $tw.mws.store.listBags(), - recipeList = $tw.mws.store.listRecipes(), - sqlTiddlerDatabase = state.server.sqlTiddlerDatabase; + var bagList = await state.store.listBags(), + recipeList = await state.store.listRecipes(); // If application/json is requested then this is an API request, and gets the response in JSON if(request.headers.accept && request.headers.accept.indexOf("application/json") !== -1) { - state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify(recipes),"utf8"); + state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify(recipeList),"utf8"); } else { // This is not a JSON API request, we should return the raw tiddler content response.writeHead(200, "OK",{ "Content-Type": "text/html" }); // filter bags and recipies by user's read access from ACL - var allowedRecipes = recipeList.filter(recipe => recipe.recipe_name.startsWith("$:/") || state.authenticatedUser?.isAdmin || sqlTiddlerDatabase.hasRecipePermission(state.authenticatedUser?.user_id, recipe.recipe_name, 'READ') || state.allowAnon && state.allowAnonReads); - var allowedBags = bagList.filter(bag => bag.bag_name.startsWith("$:/") || state.authenticatedUser?.isAdmin || sqlTiddlerDatabase.hasBagPermission(state.authenticatedUser?.user_id, bag.bag_name, 'READ') || state.allowAnon && state.allowAnonReads); - allowedRecipes = allowedRecipes.map(recipe => { - return { - ...recipe, - has_acl_access: state.authenticatedUser?.isAdmin || recipe.owner_id === state.authenticatedUser?.user_id || sqlTiddlerDatabase.hasRecipePermission(state.authenticatedUser?.user_id, recipe.recipe_name, 'WRITE') - } - }); + const allowedRecipes =await filterAsync(recipeList, async recipe => + recipe.recipe_name.startsWith("$:/") + || state.authenticatedUser?.isAdmin + || await state.store.sql.hasRecipePermission( + state.authenticatedUser?.user_id, + recipe.recipe_name, + 'READ' + ) + || state.allowAnon && state.allowAnonReads + ); + + const allowedBags = await filterAsync(bagList, async bag => + bag.bag_name.startsWith("$:/") + || state.authenticatedUser?.isAdmin + || await state.store.sql.hasBagPermission( + state.authenticatedUser?.user_id, + bag.bag_name, + 'READ' + ) + || state.allowAnon && state.allowAnonReads + ); + + const allowedRecipesWithWrite = await mapAsync(allowedRecipes, async recipe => ({ + ...recipe, + has_acl_access: state.authenticatedUser?.isAdmin + || recipe.owner_id === state.authenticatedUser?.user_id + || await state.store.sql.hasRecipePermission( + state.authenticatedUser?.user_id, recipe.recipe_name, 'WRITE') + })) + // Render the html - var html = $tw.mws.store.adminWiki.renderTiddler("text/plain","$:/plugins/tiddlywiki/multiwikiserver/templates/page",{ + var html = state.store.adminWiki.renderTiddler("text/plain","$:/plugins/tiddlywiki/multiwikiserver/templates/page",{ variables: { - "show-system": state.queryParameters.show_system || "off", + "show-system": state.queryParameters.get("show_system") || "off", "page-content": "$:/plugins/tiddlywiki/multiwikiserver/templates/get-index", "bag-list": JSON.stringify(allowedBags), - "recipe-list": JSON.stringify(allowedRecipes), + "recipe-list": JSON.stringify(allowedRecipesWithWrite), "username": state.authenticatedUser ? state.authenticatedUser.username : state.firstGuestUser ? "Anonymous User" : "Guest", "user-is-admin": state.authenticatedUser && state.authenticatedUser.isAdmin ? "yes" : "no", "first-guest-user": state.firstGuestUser ? "yes" : "no", @@ -58,5 +80,38 @@ exports.handler = function(request,response,state) { response.end(); } }; +/** + * @template T + * @template U + * @template V + * @param {T[]} array + * @param {(this: V, value: T, index: number, array: T[]) => U} callback + * @param {V} [thisArg] + * @returns {Promise} + */ +async function mapAsync (array, callback, thisArg) { + const results = new Array(array.length); + for (let index = 0; index < array.length; index++) { + results[index] = await callback.call(thisArg, array[index], index, array); + } + return results; +}; +/** + * @template T + * @template U + * @param {T[]} array + * @param {(this: U, value: T, index: number, array: T[]) => Promise} callback + * @param {U} [thisArg] + * @returns {Promise} + */ +async function filterAsync (array, callback, thisArg) { + const results = []; + for (let index = 0; index < array.length; index++) { + if (await callback.call(thisArg, array[index], index, array)) { + results.push(array[index]); + } + } + return results; +} }()); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-login.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-login.js index dd0421a66ea..a69fe49b0b6 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-login.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-login.js @@ -15,19 +15,19 @@ GET /login exports.method = "GET"; exports.path = /^\/login$/; - -exports.handler = function(request,response,state) { - // Check if the user already has a valid session - var authenticatedUser = state.server.authenticateUser(request, response); - if(authenticatedUser) { +/** @type {ServerRouteHandler<0>} */ +exports.handler = async function(request,response,state) { + // // Check if the user already has a valid session + // var authenticatedUser = await state.server.authenticateUser(request, response); + if(state.authenticatedUser) { // User is already logged in, redirect to home page response.writeHead(302, { "Location": "/" }); response.end(); return; } - var loginTiddler = $tw.mws.store.adminWiki.getTiddler("$:/plugins/tiddlywiki/multiwikiserver/auth/form/login"); + var loginTiddler = state.store.adminWiki.getTiddler("$:/plugins/tiddlywiki/multiwikiserver/auth/form/login"); if(loginTiddler) { - var text = $tw.mws.store.adminWiki.renderTiddler("text/html", loginTiddler.fields.title); + var text = state.store.adminWiki.renderTiddler("text/html", loginTiddler.fields.title); response.writeHead(200, { "Content-Type": "text/html" }); response.end(text); } else { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-events.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-events.js index bfdc40c187d..3ca7960460f 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-events.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-events.js @@ -21,15 +21,15 @@ const SSE_HEARTBEAT_INTERVAL_MS = 10 * 1000; exports.method = "GET"; exports.path = /^\/recipes\/([^\/]+)\/events$/; - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler<1>} */ +exports.handler = async function(request,response,state) { // Get the parameters const recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]); let last_known_tiddler_id = 0; if(request.headers["Last-Event-ID"]) { last_known_tiddler_id = $tw.utils.parseNumber(request.headers["Last-Event-ID"]); - } else if(state.queryParameters.last_known_tiddler_id) { - last_known_tiddler_id = $tw.utils.parseNumber(state.queryParameters.last_known_tiddler_id); + } else if(state.queryParameters.get("last_known_tiddler_id")) { + last_known_tiddler_id = $tw.utils.parseNumber(state.queryParameters.get("last_known_tiddler_id")); } if(recipe_name) { // Start streaming the response @@ -43,9 +43,9 @@ exports.handler = function(request,response,state) { response.write(':keep-alive\n\n'); },SSE_HEARTBEAT_INTERVAL_MS); // Method to get changed tiddler events and send to the client - function sendUpdates() { + async function sendUpdates() { // Get the tiddlers in the recipe since the last known tiddler_id - var recipeTiddlers = $tw.mws.store.getRecipeTiddlers(recipe_name,{ + var recipeTiddlers = await state.store.getRecipeTiddlers(recipe_name,{ include_deleted: true, last_known_tiddler_id: last_known_tiddler_id }); @@ -59,7 +59,7 @@ exports.handler = function(request,response,state) { response.write(`event: change\n`) let data = tiddlerInfo; if(!tiddlerInfo.is_deleted) { - const tiddler = $tw.mws.store.getRecipeTiddler(tiddlerInfo.title,recipe_name); + const tiddler = await state.store.getRecipeTiddler(tiddlerInfo.title,recipe_name); if(tiddler) { data = $tw.utils.extend({},data,{tiddler: tiddler.tiddler}) } @@ -71,12 +71,14 @@ exports.handler = function(request,response,state) { } } // Send current and future changes - sendUpdates(); - $tw.mws.store.addEventListener("change",sendUpdates); + await sendUpdates(); + // eslint-disable-next-line @typescript-eslint/no-misused-promises + state.store.addEventListener("change",sendUpdates); // Clean up when the connection closes response.on("close",function () { clearInterval(heartbeatTimer); - $tw.mws.store.removeEventListener("change",sendUpdates); + // eslint-disable-next-line @typescript-eslint/no-misused-promises + state.store.removeEventListener("change",sendUpdates); }); return; } diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddler.js index a50657ce58a..123c60f064e 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddler.js @@ -23,12 +23,12 @@ exports.path = /^\/recipes\/([^\/]+)\/tiddlers\/(.+)$/; // exports.useACL = true; exports.entityName = "recipe" - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler<2>} */ +exports.handler = async function(request,response,state) { // Get the parameters var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]), title = $tw.utils.decodeURIComponentSafe(state.params[1]), - tiddlerInfo = $tw.mws.store.getRecipeTiddler(title,recipe_name); + tiddlerInfo = await state.store.getRecipeTiddler(title,recipe_name); if(tiddlerInfo && tiddlerInfo.tiddler) { // If application/json is requested then this is an API request, and gets the response in JSON if(request.headers.accept && request.headers.accept.indexOf("application/json") !== -1) { @@ -41,7 +41,7 @@ exports.handler = function(request,response,state) { return; } else { // This is not a JSON API request, we should return the raw tiddler content - const result = $tw.mws.store.getBagTiddlerStream(title,tiddlerInfo.bag_name); + const result = await state.store.getBagTiddlerStream(title,tiddlerInfo.bag_name); if(result) { if(!response.headersSent){ response.writeHead(200, "OK",{ @@ -50,6 +50,10 @@ exports.handler = function(request,response,state) { }); } result.stream.pipe(response); + await new Promise((resolve,reject) => { + result.stream.on("end",resolve); + result.stream.on("error",reject); + }); return; } else { if(!response.headersSent){ @@ -62,9 +66,9 @@ exports.handler = function(request,response,state) { } else { if(!response.headersSent) { // Redirect to fallback URL if tiddler not found - if(state.queryParameters.fallback) { + if(state.queryParameters.get("fallback")) { response.writeHead(302, "OK",{ - "Location": state.queryParameters.fallback + "Location": state.queryParameters.get("fallback") }); response.end(); return; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddlers-json.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddlers-json.js index e16e3d10a55..1b0cee55f41 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddlers-json.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddlers-json.js @@ -15,16 +15,16 @@ GET /recipes/:recipe_name/tiddlers.json?last_known_tiddler_id=:last_known_tiddle exports.method = "GET"; exports.path = /^\/recipes\/([^\/]+)\/tiddlers.json$/; - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler<1>} */ +exports.handler = async function(request,response,state) { if(!response.headersSent) { // Get the parameters var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]); if(recipe_name) { // Get the tiddlers in the recipe, optionally since the specified last known tiddler_id - var recipeTiddlers = $tw.mws.store.getRecipeTiddlers(recipe_name,{ - include_deleted: state.queryParameters.include_deleted === "true", - last_known_tiddler_id: state.queryParameters.last_known_tiddler_id + var recipeTiddlers = await state.store.getRecipeTiddlers(recipe_name,{ + include_deleted: state.queryParameters.get("include_deleted") === "true", + last_known_tiddler_id: $tw.utils.parseNumber(state.queryParameters.get("last_known_tiddler_id")) ?? undefined, }); if(recipeTiddlers) { state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify(recipeTiddlers),"utf8"); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-system.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-system.js index a5346c1a658..be8e41fe334 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-system.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-system.js @@ -23,8 +23,8 @@ exports.method = "GET"; exports.path = /^\/\.system\/(.+)$/; const SYSTEM_FILE_TITLE_PREFIX = "$:/plugins/tiddlywiki/multiwikiserver/system-files/"; - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler<1>} */ +exports.handler = async function(request,response,state) { // Get the parameters const filename = $tw.utils.decodeURIComponentSafe(state.params[0]), title = SYSTEM_FILE_TITLE_PREFIX + filename, diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-users.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-users.js index 142258aa0d5..d9b1ff2e0f8 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-users.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-users.js @@ -15,12 +15,12 @@ GET /admin/users exports.method = "GET"; exports.path = /^\/admin\/users$/; - -exports.handler = function(request,response,state) { - var userList = state.server.sqlTiddlerDatabase.listUsers(); - if (request.url.includes("*")) { - $tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/post-user/error"); - $tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/post-user/success"); +/** @type {ServerRouteHandler<0>} */ +exports.handler = async function(request,response,state) { + var userList = await state.store.sql.listUsers(); + if(request.url.includes("*")) { + state.store.adminWiki.deleteTiddler("$:/temp/mws/post-user/error"); + state.store.adminWiki.deleteTiddler("$:/temp/mws/post-user/success"); } // Ensure userList is an array @@ -29,7 +29,7 @@ exports.handler = function(request,response,state) { console.error("userList is not an array"); } - if(!state.authenticatedUser.isAdmin && !state.firstGuestUser) { + if(!state.authenticatedUser?.isAdmin && !state.firstGuestUser) { response.writeHead(403, "Forbidden", { "Content-Type": "text/plain" }); response.end("Forbidden"); return; @@ -49,7 +49,7 @@ exports.handler = function(request,response,state) { }); // Render the html - var html = $tw.mws.store.adminWiki.renderTiddler("text/plain","$:/plugins/tiddlywiki/multiwikiserver/templates/page",{ + var html = state.store.adminWiki.renderTiddler("text/plain","$:/plugins/tiddlywiki/multiwikiserver/templates/page",{ variables: { "page-content": "$:/plugins/tiddlywiki/multiwikiserver/templates/get-users", "user-list": JSON.stringify(userList), diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-wiki.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-wiki.js index 1765f5e208f..2acbd0fe6e7 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-wiki.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-wiki.js @@ -19,79 +19,91 @@ exports.path = /^\/wiki\/([^\/]+)$/; exports.useACL = true; exports.entityName = "recipe" - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler<1>} */ +exports.handler = async function(request,response,state) { // Get the recipe name from the parameters var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]), - recipeTiddlers = recipe_name && $tw.mws.store.getRecipeTiddlers(recipe_name); + recipeTiddlers = recipe_name && await state.store.getRecipeTiddlers(recipe_name); + + console.log("GET /wiki/:recipe_name",recipe_name,!!recipeTiddlers); // Check request is valid - if(recipe_name && recipeTiddlers) { - // Start the response - response.writeHead(200, "OK",{ - "Content-Type": "text/html" - }); - // Get the tiddlers in the recipe - // Render the template - var template = $tw.mws.store.adminWiki.renderTiddler("text/plain","$:/core/templates/tiddlywiki5.html",{ - variables: { - saveTiddlerFilter: ` - $:/boot/boot.css - $:/boot/boot.js - $:/boot/bootprefix.js - $:/core - $:/library/sjcl.js - $:/plugins/tiddlywiki/multiwikiclient - $:/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"); - } - function writeTiddler(tiddlerFields) { - response.write(JSON.stringify(tiddlerFields).replace(/[`, + markerPos = template.indexOf(marker); + if(markerPos === -1) { + throw new Error("Cannot find tiddler store in template"); + } + /** + * + * @param {Record} tiddlerFields + */ + function writeTiddler(tiddlerFields) { + response.write(JSON.stringify(tiddlerFields).replace(/} */ + bagInfo = {}, + /** @type {Record} */ + revisionInfo = {}; + + for(const recipeTiddlerInfo of recipeTiddlers){ + var result = await state.store.getRecipeTiddler(recipeTiddlerInfo.title,recipe_name); + if(result) { + bagInfo[result.tiddler.title] = result.bag_name; + revisionInfo[result.tiddler.title] = result.tiddler_id.toString(); + writeTiddler(result.tiddler); + } + } + + writeTiddler({ + title: "$:/state/multiwikiclient/tiddlers/bag", + text: JSON.stringify(bagInfo), + type: "application/json" + }); + writeTiddler({ + title: "$:/state/multiwikiclient/tiddlers/revision", + text: JSON.stringify(revisionInfo), + type: "application/json" + }); + writeTiddler({ + title: "$:/config/multiwikiclient/recipe", + text: recipe_name + }); + writeTiddler({ + title: "$:/state/multiwikiclient/recipe/last_tiddler_id", + text: (await state.store.getRecipeLastTiddlerId(recipe_name) || 0).toString() + }); + response.write(template.substring(markerPos + marker.length)) + // Finish response + response.end(); + }; }()); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-roles.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-roles.js index e6400dbb92c..5f131469d49 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-roles.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-roles.js @@ -15,13 +15,13 @@ GET /admin/manage-roles exports.method = "GET"; exports.path = /^\/admin\/roles\/?$/; - -exports.handler = function(request, response, state) { - if (request.url.includes("*")) { - $tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/post-role/error"); - $tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/post-role/success"); +/** @type {ServerRouteHandler<0>} */ +exports.handler = async function(request, response, state) { + if(request.url.includes("*")) { + state.store.adminWiki.deleteTiddler("$:/temp/mws/post-role/error"); + state.store.adminWiki.deleteTiddler("$:/temp/mws/post-role/success"); } - var roles = state.server.sqlTiddlerDatabase.listRoles(); + var roles = await state.store.sql.listRoles(); var editRoleId = request.url.includes("?") ? request.url.split("?")[1]?.split("=")[1] : null; var editRole = editRoleId ? roles.find(role => role.role_id === $tw.utils.parseInt(editRoleId, 10)) : null; @@ -30,7 +30,7 @@ exports.handler = function(request, response, state) { editRoleId = null; } - var html = $tw.mws.store.adminWiki.renderTiddler("text/plain", "$:/plugins/tiddlywiki/multiwikiserver/templates/page", { + var html = state.store.adminWiki.renderTiddler("text/plain", "$:/plugins/tiddlywiki/multiwikiserver/templates/page", { variables: { "page-content": "$:/plugins/tiddlywiki/multiwikiserver/templates/manage-roles", "roles-list": JSON.stringify(roles), diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-user.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-user.js index cada04b832f..290efaadc2c 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-user.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-user.js @@ -15,27 +15,27 @@ GET /admin/users/:user_id exports.method = "GET"; exports.path = /^\/admin\/users\/([^\/]+)\/?$/; - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler<1>} */ +exports.handler = async function(request,response,state) { var user_id = $tw.utils.decodeURIComponentSafe(state.params[0]); - var userData = state.server.sqlTiddlerDatabase.getUser(user_id); + var userData = await state.store.sql.getUser(user_id); // Clean up any existing error/success messages if the user_id is different from the "$:/temp/mws/user-info/preview-user-id" var lastPreviewedUser = $tw.wiki.getTiddlerText("$:/temp/mws/user-info/" + user_id + "/preview-user-id"); if(user_id !== lastPreviewedUser || request.url.includes("preview")) { - $tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/change-password/" + user_id + "/ error"); - $tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/change-password/" + user_id + "/success"); - $tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/login/error"); - $tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/delete-user/" + user_id + "/error"); - $tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/delete-user/" + user_id + "/success"); - $tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/update-profile/" + user_id + "/error"); - $tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/update-profile/" + user_id + "/success"); + state.store.adminWiki.deleteTiddler("$:/temp/mws/change-password/" + user_id + "/ error"); + state.store.adminWiki.deleteTiddler("$:/temp/mws/change-password/" + user_id + "/success"); + state.store.adminWiki.deleteTiddler("$:/temp/mws/login/error"); + state.store.adminWiki.deleteTiddler("$:/temp/mws/delete-user/" + user_id + "/error"); + state.store.adminWiki.deleteTiddler("$:/temp/mws/delete-user/" + user_id + "/success"); + state.store.adminWiki.deleteTiddler("$:/temp/mws/update-profile/" + user_id + "/error"); + state.store.adminWiki.deleteTiddler("$:/temp/mws/update-profile/" + user_id + "/success"); } if(!userData) { response.writeHead(404, "Not Found", {"Content-Type": "text/html"}); - var errorHtml = $tw.mws.store.adminWiki.renderTiddler("text/plain", "$:/plugins/tiddlywiki/multiwikiserver/templates/error", { + var errorHtml = state.store.adminWiki.renderTiddler("text/plain", "$:/plugins/tiddlywiki/multiwikiserver/templates/error", { variables: { "error-message": "User not found" } @@ -46,7 +46,7 @@ exports.handler = function(request,response,state) { } // Check if the user is trying to access their own profile or is an admin - var hasPermission = ($tw.utils.parseInt(user_id) === state.authenticatedUser.user_id) || state.authenticatedUser.isAdmin; + var hasPermission = ($tw.utils.parseInt(user_id) === state.authenticatedUser?.user_id) || state.authenticatedUser?.isAdmin; if(!hasPermission) { response.writeHead(403, "Forbidden", { "Content-Type": "text/plain" }); response.end("Forbidden"); @@ -63,13 +63,13 @@ exports.handler = function(request,response,state) { }; // Get all roles which the user has been assigned - var userRole = state.server.sqlTiddlerDatabase.getUserRoles(user_id); - var allRoles = state.server.sqlTiddlerDatabase.listRoles(); + var userRole = await state.store.sql.getUserRoles(user_id); + var allRoles = await state.store.sql.listRoles(); // sort allRoles by placing the user's role at the top of the list allRoles.sort(function(a, b){ return (a.role_id === userRole?.role_id ? -1 : 1) }); - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/user-info/" + user_id + "/preview-user-id", text: user_id })); @@ -79,7 +79,7 @@ exports.handler = function(request,response,state) { }); // Render the html - var html = $tw.mws.store.adminWiki.renderTiddler("text/plain", "$:/plugins/tiddlywiki/multiwikiserver/templates/page", { + var html = state.store.adminWiki.renderTiddler("text/plain", "$:/plugins/tiddlywiki/multiwikiserver/templates/page", { variables: { "page-content": "$:/plugins/tiddlywiki/multiwikiserver/templates/manage-user", "user": JSON.stringify(user), diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-acl.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-acl.js index 63a9f414f1b..4fe846d4497 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-acl.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-acl.js @@ -6,8 +6,11 @@ module-type: mws-route POST /admin/post-acl \*/ -(function () { +(function () { + const {okEntityType, okType} = require("$:/plugins/tiddlywiki/multiwikiserver/store/sql-tiddler-database"); + const {ok} = require("assert"); + /*jslint node: true, browser: true */ /*global $tw: false */ "use strict"; @@ -19,21 +22,27 @@ exports.path = /^\/admin\/post-acl\/?$/; exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; - -exports.handler = function (request, response, state) { - var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase; - var entity_type = state.data.entity_type; - var recipe_name = state.data.recipe_name; - var bag_name = state.data.bag_name; - var role_id = state.data.role_id; - var permission_id = state.data.permission_id; +/** @type {ServerRouteHandler<0,"www-form-urlencoded">} */ +exports.handler = async function (request, response, state) { + var sqlTiddlerDatabase = state.store.sql; + var entity_type = state.data.get("entity_type"); + var recipe_name = state.data.get("recipe_name"); + var bag_name = state.data.get("bag_name"); + var role_id = state.data.get("role_id") ?? ""; + var permission_id = state.data.get("permission_id") ?? ""; var isRecipe = entity_type === "recipe" + ok(role_id, "role_id is required"); + ok(permission_id, "permission_id is required"); + okEntityType(entity_type); + const entity_name = isRecipe ? recipe_name : bag_name; + okType(entity_name, "string", (isRecipe ? "recipe_name" : "bag_name") + " is required"); + try { - var entityAclRecords = sqlTiddlerDatabase.getACLByName(entity_type, isRecipe ? recipe_name : bag_name, true); + var entityAclRecords = await sqlTiddlerDatabase.getACLByName(entity_type, entity_name, true); var aclExists = entityAclRecords.some((record) => ( - record.role_id == role_id && record.permission_id == permission_id + record.role_id == +role_id && record.permission_id == +permission_id )) // This ensures that the user attempting to modify the ACL has permission to do so @@ -50,11 +59,11 @@ exports.handler = function (request, response, state) { return } - sqlTiddlerDatabase.createACL( - isRecipe ? recipe_name : bag_name, + await sqlTiddlerDatabase.createACL( + entity_name, entity_type, - role_id, - permission_id + +role_id, + +permission_id ) response.writeHead(302, { "Location": "/admin/acl/" + recipe_name + "/" + bag_name }); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-anon-config.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-anon-config.js index e1e841516c4..480f71567cb 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-anon-config.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-anon-config.js @@ -19,8 +19,8 @@ exports.path = /^\/admin\/post-anon-config\/?$/; exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; - -exports.handler = function(request, response, state) { +/** @type {ServerRouteHandler<0,"www-form-urlencoded">} */ +exports.handler = async function(request, response, state) { // Check if user is authenticated and is admin if(!state.authenticatedUser || !state.authenticatedUser.isAdmin) { response.writeHead(401, "Unauthorized", { "Content-Type": "text/plain" }); @@ -28,8 +28,8 @@ exports.handler = function(request, response, state) { return; } - var allowReads = state.data.allowReads === "on"; - var allowWrites = state.data.allowWrites === "on"; + var allowReads = state.data.get("allowReads") === "on"; + var allowWrites = state.data.get("allowWrites") === "on"; // Update the configuration tiddlers var wiki = $tw.wiki; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-anon.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-anon.js index 911b6ef971c..b8fd18258d6 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-anon.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-anon.js @@ -19,8 +19,8 @@ exports.path = /^\/admin\/anon\/?$/; exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; - -exports.handler = function(request, response, state) { +/** @type {ServerRouteHandler<0,"www-form-urlencoded">} */ +exports.handler = async function(request, response, state) { // Check if user is authenticated and is admin if(!state.authenticatedUser || !state.authenticatedUser.isAdmin) { response.writeHead(401, "Unauthorized", { "Content-Type": "text/plain" }); @@ -30,8 +30,7 @@ exports.handler = function(request, response, state) { // Update the configuration tiddlers - var wiki = $tw.wiki; - wiki.addTiddler({ + $tw.wiki.addTiddler({ title: "$:/config/MultiWikiServer/ShowAnonymousAccessModal", text: "yes" }); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag-tiddlers.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag-tiddlers.js index 0f520b1ba59..b371069c7a9 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag-tiddlers.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag-tiddlers.js @@ -23,8 +23,8 @@ exports.csrfDisable = true; exports.useACL = true; exports.entityName = "bag" - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler<1, "stream">} */ +exports.handler = async function(request,response,state) { const path = require("path"), fs = require("fs"), processIncomingStream = require("$:/plugins/tiddlywiki/multiwikiserver/routes/helpers/multipart-forms.js").processIncomingStream; @@ -32,7 +32,7 @@ exports.handler = function(request,response,state) { var bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]); // Process the incoming data processIncomingStream({ - store: $tw.mws.store, + store: state.store, state: state, response: response, bag_name: bag_name, @@ -55,7 +55,7 @@ exports.handler = function(request,response,state) { `); // Render the html - var html = $tw.mws.store.adminWiki.renderTiddler("text/html","$:/plugins/tiddlywiki/multiwikiserver/templates/post-bag-tiddlers",{ + var html = state.store.adminWiki.renderTiddler("text/html","$:/plugins/tiddlywiki/multiwikiserver/templates/post-bag-tiddlers",{ variables: { "bag-name": bag_name, "imported-titles": JSON.stringify(results) diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag.js index bd59b06427f..d9d61934a5d 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag.js @@ -28,10 +28,10 @@ exports.csrfDisable = true; exports.useACL = true; exports.entityName = "bag" - -exports.handler = function(request,response,state) { - if(state.data.bag_name) { - const result = $tw.mws.store.createBag(state.data.bag_name,state.data.description); +/** @type {ServerRouteHandler<0,"www-form-urlencoded">} */ +exports.handler = async function(request,response,state) { + if(state.data.get("bag_name")) { + const result = await state.store.createBag(state.data.get("bag_name"),state.data.get("description")); if(!result) { state.sendResponse(302,{ "Content-Type": "text/plain", diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-login.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-login.js index b2bc0ff4df4..c580ff9cc88 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-login.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-login.js @@ -16,7 +16,7 @@ password /*jslint node: true, browser: true */ /*global $tw: false */ "use strict"; -var authenticator = require('$:/plugins/tiddlywiki/multiwikiserver/auth/authentication.js').Authenticator; +var authenticator = require("$:/plugins/tiddlywiki/multiwikiserver/auth/authentication.js").Authenticator; exports.method = "POST"; @@ -25,16 +25,16 @@ exports.path = /^\/login$/; exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; - -exports.handler = function(request,response,state) { - var auth = authenticator(state.server.sqlTiddlerDatabase); - var username = state.data.username; - var password = state.data.password; - var user = state.server.sqlTiddlerDatabase.getUserByUsername(username); +/** @type {ServerRouteHandler<0,"www-form-urlencoded">} */ +exports.handler = async function(request,response,state) { + var auth = authenticator(state.store.sql); + var username = state.data.get("username"); + var password = state.data.get("password"); + var user = await state.store.sql.getUserByUsername(username); var isPasswordValid = auth.verifyPassword(password, user ? user.password : null) if(user && isPasswordValid) { - var sessionId = auth.createSession(user.user_id); + var sessionId = await auth.createSession(user.user_id); var returnUrl = state.server.parseCookieString(request.headers.cookie).returnUrl response.setHeader('Set-Cookie', `session=${sessionId}; HttpOnly; Path=/`); if(request.headers.accept && request.headers.accept.indexOf("application/json") !== -1) { @@ -47,7 +47,7 @@ exports.handler = function(request,response,state) { }); } } else { - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/login/error", text: "Invalid username or password" })); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-logout.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-logout.js index 36d901b4467..86e6a361a24 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-logout.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-logout.js @@ -17,11 +17,11 @@ exports.method = "POST"; exports.path = /^\/logout$/; exports.csrfDisable = true; - -exports.handler = function(request,response,state) { - // if(state.authenticatedUser) { - state.server.sqlTiddlerDatabase.deleteSession(state.authenticatedUser.sessionId); - // } +/** @type {ServerRouteHandler<0>} */ +exports.handler = async function(request,response,state) { + if(state.authenticatedUser) { + await state.store.sql.deleteSession(state.authenticatedUser.sessionId); + } var cookies = request.headers.cookie ? request.headers.cookie.split(";") : []; for(var i = 0; i < cookies.length; i++) { var cookie = cookies[i].trim().split("=")[0]; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-recipe.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-recipe.js index aa38986a002..c5ebec1e169 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-recipe.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-recipe.js @@ -29,15 +29,15 @@ exports.csrfDisable = true; exports.useACL = true; exports.entityName = "recipe" - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler<0,"www-form-urlencoded">} */ +exports.handler = async function(request,response,state) { var server = state.server, sqlTiddlerDatabase = server.sqlTiddlerDatabase - if(state.data.recipe_name && state.data.bag_names) { - const result = $tw.mws.store.createRecipe(state.data.recipe_name,$tw.utils.parseStringArray(state.data.bag_names),state.data.description); + if(state.data.get("recipe_name") && state.data.get("bag_names")) { + const result = await state.store.createRecipe(state.data.get("recipe_name"),$tw.utils.parseStringArray(state.data.get("bag_names")),state.data.get("description")); if(!result) { if(state.authenticatedUser) { - sqlTiddlerDatabase.assignRecipeToUser(state.data.recipe_name,state.authenticatedUser.user_id); + await sqlTiddlerDatabase.assignRecipeToUser(state.data.get("recipe_name"),state.authenticatedUser.user_id); } state.sendResponse(302,{ "Content-Type": "text/plain", diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-role.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-role.js index 9692c7d689d..216092b699a 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-role.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-role.js @@ -19,14 +19,14 @@ exports.path = /^\/admin\/post-role\/?$/; exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; - -exports.handler = function (request, response, state) { - var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase; - var role_name = state.data.role_name; - var role_description = state.data.role_description; +/** @type {ServerRouteHandler<0,"www-form-urlencoded">} */ +exports.handler = async function (request, response, state) { + var sqlTiddlerDatabase = state.store.sql; + var role_name = state.data.get("role_name"); + var role_description = state.data.get("role_description"); if(!state.authenticatedUser || !state.authenticatedUser.isAdmin) { - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/post-role/error", text: "Unauthorized access. Admin privileges required." })); @@ -36,7 +36,7 @@ exports.handler = function (request, response, state) { } if(!role_name || !role_description) { - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/post-role/error", text: "Role name and description are required" })); @@ -47,9 +47,9 @@ exports.handler = function (request, response, state) { try { // Check if role already exists - var existingRole = sqlTiddlerDatabase.getRole(role_name); + var existingRole = await sqlTiddlerDatabase.getRole(role_name); if(existingRole) { - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/post-role/error", text: "Role already exists" })); @@ -58,9 +58,9 @@ exports.handler = function (request, response, state) { return; } - sqlTiddlerDatabase.createRole(role_name, role_description); + await sqlTiddlerDatabase.createRole(role_name, role_description); - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/post-role/success", text: "Role created successfully" })); @@ -69,7 +69,7 @@ exports.handler = function (request, response, state) { } catch(error) { console.error("Error creating role:", error); - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/post-role/error", text: "Error creating role: " + error.message })); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-user.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-user.js index ff3acbfc916..9df9ceb94dc 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-user.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-user.js @@ -22,25 +22,25 @@ exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; -function deleteTempTiddlers() { - setTimeout(function() { - $tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/queryParams"); - $tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/post-user/error"); - $tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/post-user/success"); - }, 1000); -} -exports.handler = function(request, response, state) { - var current_user_id = state.authenticatedUser.user_id; - var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase; - var username = state.data.username; - var email = state.data.email; - var password = state.data.password; - var confirmPassword = state.data.confirmPassword; +/** @type {ServerRouteHandler<0, "www-form-urlencoded">} */ +exports.handler = async function(request, response, state) { + function deleteTempTiddlers() { + setTimeout(function() { + state.store.adminWiki.deleteTiddler("$:/temp/mws/queryParams"); + state.store.adminWiki.deleteTiddler("$:/temp/mws/post-user/error"); + state.store.adminWiki.deleteTiddler("$:/temp/mws/post-user/success"); + }, 1000); + } + var sqlTiddlerDatabase = state.store.sql; + var username = state.data.get("username"); + var email = state.data.get("email"); + var password = state.data.get("password"); + var confirmPassword = state.data.get("confirmPassword"); var queryParamsTiddlerTitle = "$:/temp/mws/queryParams"; if(!state.authenticatedUser && !state.firstGuestUser) { - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/post-user/error", text: "Unauthorized access" })); @@ -51,11 +51,11 @@ exports.handler = function(request, response, state) { } if(!username || !email || !password || !confirmPassword) { - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/post-user/error", text: "All fields are required" })); - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: queryParamsTiddlerTitle, username: username, email: email, @@ -67,11 +67,11 @@ exports.handler = function(request, response, state) { } if(password !== confirmPassword) { - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/post-user/error", text: "Passwords do not match" })); - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/queryParams", username: username, email: email, @@ -84,23 +84,23 @@ exports.handler = function(request, response, state) { try { // Check if username or email already exists - var existingUser = sqlTiddlerDatabase.getUserByUsername(username); - var existingUserByEmail = sqlTiddlerDatabase.getUserByEmail(email); + var existingUser = await sqlTiddlerDatabase.getUserByUsername(username); + var existingUserByEmail = await sqlTiddlerDatabase.getUserByEmail(email); if(existingUser || existingUserByEmail) { - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/post-user/error", text: existingUser ? "User with this username already exists" : "User account with this email already exists" })); - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: queryParamsTiddlerTitle, username: username, email: email, })); - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ - title: "$:/temp/mws/queryParams", - username: username, + state.store.adminWiki.addTiddler(new $tw.Tiddler({ + title: "$:/temp/mws/queryParams", + username: username, email: email, })); response.writeHead(302, { "Location": "/admin/users" }); @@ -109,23 +109,23 @@ exports.handler = function(request, response, state) { return; } - var hasUsers = sqlTiddlerDatabase.listUsers().length > 0; + var hasUsers = (await sqlTiddlerDatabase.listUsers()).length > 0; var hashedPassword = crypto.createHash("sha256").update(password).digest("hex"); // Create new user - var userId = sqlTiddlerDatabase.createUser(username, email, hashedPassword); + var userId = await sqlTiddlerDatabase.createUser(username, email, hashedPassword); if(!hasUsers) { try { // If this is the first guest user, assign admin privileges - sqlTiddlerDatabase.setUserAdmin(userId, true); + await sqlTiddlerDatabase.setUserAdmin(userId); // Create a session for the new admin user var auth = require("$:/plugins/tiddlywiki/multiwikiserver/auth/authentication.js").Authenticator; var authenticator = auth(sqlTiddlerDatabase); - var sessionId = authenticator.createSession(userId); + var sessionId = await authenticator.createSession(userId); - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/post-user/success", text: "Admin user created successfully" })); @@ -135,11 +135,11 @@ exports.handler = function(request, response, state) { deleteTempTiddlers(); return; } catch(adminError) { - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/post-user/error", text: "Error creating admin user" })); - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: queryParamsTiddlerTitle, username: username, email: email, @@ -150,33 +150,33 @@ exports.handler = function(request, response, state) { return; } } else { - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/post-user/success", text: "User created successfully" })); - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: queryParamsTiddlerTitle, username: username, email: email, })); // assign role to user - var roles = sqlTiddlerDatabase.listRoles(); + var roles = await sqlTiddlerDatabase.listRoles(); var role = roles.find(function(role) { return role.role_name.toUpperCase() !== "ADMIN"; }); if(role) { - sqlTiddlerDatabase.addRoleToUser(userId, role.role_id); + await sqlTiddlerDatabase.addRoleToUser(userId, role.role_id); } response.writeHead(302, {"Location": "/admin/users/"+userId}); response.end(); deleteTempTiddlers(); } } catch(error) { - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/post-user/error", text: "Error creating user: " + error.message })); - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: queryParamsTiddlerTitle, username: username, email: email, diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-bag.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-bag.js index d174ee8cea2..47ef640aacf 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-bag.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-bag.js @@ -19,13 +19,13 @@ exports.path = /^\/bags\/(.+)$/; exports.useACL = true; exports.entityName = "bag" - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler<1>} */ +exports.handler = async function(request,response,state) { // Get the parameters var bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]), data = $tw.utils.parseJSONSafe(state.data); if(bag_name && data) { - var result = $tw.mws.store.createBag(bag_name,data.description); + var result = await state.store.createBag(bag_name,data.description); if(!result) { state.sendResponse(204,{ "Content-Type": "text/plain" diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe-tiddler.js index 25279cdd0d8..29065f4a7c1 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe-tiddler.js @@ -19,14 +19,14 @@ exports.path = /^\/recipes\/([^\/]+)\/tiddlers\/(.+)$/; exports.useACL = true; exports.entityName = "recipe" - -exports.handler = function (request, response, state) { +/** @type {ServerRouteHandler<2>} */ +exports.handler = async function (request, response, state) { // Get the parameters var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]), title = $tw.utils.decodeURIComponentSafe(state.params[1]), fields = $tw.utils.parseJSONSafe(state.data); if(recipe_name && title === fields.title) { - var result = $tw.mws.store.saveRecipeTiddler(fields, recipe_name); + var result = await state.store.saveRecipeTiddler(fields, recipe_name); if(!response.headersSent) { if(result) { response.writeHead(204, "OK", { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe.js index 002c5e4dbd9..b3548ba01d7 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe.js @@ -19,13 +19,13 @@ exports.path = /^\/recipes\/(.+)$/; exports.useACL = true; exports.entityName = "recipe" - -exports.handler = function (request, response, state) { +/** @type {ServerRouteHandler<1>} */ +exports.handler = async function (request, response, state) { // Get the parameters var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]), data = $tw.utils.parseJSONSafe(state.data); if(recipe_name && data) { - var result = $tw.mws.store.createRecipe(recipe_name, data.bag_names, data.description); + var result = await state.store.createRecipe(recipe_name, data.bag_names, data.description); if(!result) { state.sendResponse(204, { "Content-Type": "text/plain" @@ -33,9 +33,7 @@ exports.handler = function (request, response, state) { } else { state.sendResponse(400, { "Content-Type": "text/plain" - }, - result.message, - "utf8"); + }, result.message, "utf8"); } } else { if(!response.headersSent) { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-role.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-role.js index 081ba9b7374..05a650af85e 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-role.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-role.js @@ -19,21 +19,21 @@ exports.path = /^\/admin\/roles\/([^\/]+)\/?$/; exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; - -exports.handler = function(request, response, state) { - var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase; +/** @type {ServerRouteHandler<1,"www-form-urlencoded">} */ +exports.handler = async function(request, response, state) { + var sqlTiddlerDatabase = state.store.sql; var role_id = state.params[0]; - var role_name = state.data.role_name; - var role_description = state.data.role_description; + var role_name = state.data.get("role_name"); + var role_description = state.data.get("role_description"); - if(!state.authenticatedUser.isAdmin) { + if(!state.authenticatedUser?.isAdmin) { response.writeHead(403, "Forbidden"); response.end(); return; } // get the role - var role = sqlTiddlerDatabase.getRoleById(role_id); + var role = await sqlTiddlerDatabase.getRoleById(role_id); if(!role) { response.writeHead(404, "Role not found"); @@ -48,7 +48,7 @@ exports.handler = function(request, response, state) { } try { - sqlTiddlerDatabase.updateRole( + await sqlTiddlerDatabase.updateRole( role_id, role_name, role_description diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-user-profile.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-user-profile.js index 3cbc0669000..246ce764074 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-user-profile.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-user-profile.js @@ -19,10 +19,10 @@ exports.path = /^\/update-user-profile\/?$/; exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; - -exports.handler = function (request,response,state) { +/** @type {ServerRouteHandler<0,"www-form-urlencoded">} */ +exports.handler = async function (request,response,state) { if(!state.authenticatedUser) { - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/login/error", text: "You must be logged in to update profiles" })); @@ -31,16 +31,16 @@ exports.handler = function (request,response,state) { return; } - var userId = state.data.userId; - var username = state.data.username; - var email = state.data.email; - var roleId = state.data.role; + var userId = state.data.get("userId"); + var username = state.data.get("username"); + var email = state.data.get("email"); + var roleId = state.data.get("role"); var currentUserId = state.authenticatedUser.user_id; var hasPermission = ($tw.utils.parseInt(userId) === currentUserId) || state.authenticatedUser.isAdmin; if(!hasPermission) { - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/update-profile/" + userId + "/error", text: "You don't have permission to update this profile" })); @@ -50,19 +50,19 @@ exports.handler = function (request,response,state) { } if(!state.authenticatedUser.isAdmin) { - var userRole = state.server.sqlTiddlerDatabase.getUserRoles(userId); + var userRole = await state.store.sql.getUserRoles(userId); roleId = userRole.role_id; } - var result = state.server.sqlTiddlerDatabase.updateUser(userId, username, email, roleId); + var result = await state.store.sql.updateUser(userId, username, email, roleId); if(result.success) { - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/update-profile/" + userId + "/success", text: result.message })); } else { - $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ + state.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/update-profile/" + userId + "/error", text: result.message })); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js index 7210ed82c68..dd1293a8ee4 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js @@ -1,5 +1,5 @@ /*\ -title: $:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js +title: $:/plugins/tiddlywiki/multiwikiserver/routes/helpers/acl-middleware.js type: application/javascript module-type: library @@ -34,23 +34,31 @@ function redirectToLogin(response, returnUrl) { response.end(); } }; - -exports.middleware = function (request, response, state, entityType, permissionName) { +/** + * + * @param {IncomingMessage} request + * @param {ServerResponse} response + * @param {ServerState} state + * @param {string | null} entityType + * @param {string} permissionName + * @returns + */ +exports.middleware = async function middleware(request, response, state, entityType, permissionName) { var extensionRegex = /\.[A-Za-z0-9]{1,4}$/; - var server = state.server, - sqlTiddlerDatabase = $tw.mws.store.sqlTiddlerDatabase || server.sqlTiddlerDatabase, + var + sqlTiddlerDatabase = state.store.sql, entityName = state.data ? (state.data[entityType+"_name"] || state.params[0]) : state.params[0]; // First, replace '%3A' with ':' to handle TiddlyWiki's system tiddlers var partiallyDecoded = entityName?.replace(/%3A/g, ":"); // Then use decodeURIComponent for the rest var decodedEntityName = decodeURIComponent(partiallyDecoded); - var aclRecord = sqlTiddlerDatabase.getACLByName(entityType, decodedEntityName); + var aclRecord = await sqlTiddlerDatabase.getACLByName(entityType, decodedEntityName); var isGetRequest = request.method === "GET"; var hasAnonymousAccess = state.allowAnon ? (isGetRequest ? state.allowAnonReads : state.allowAnonWrites) : false; var anonymousAccessConfigured = state.anonAccessConfigured; - var entity = sqlTiddlerDatabase.getEntityByName(entityType, decodedEntityName); + var entity = await sqlTiddlerDatabase.getEntityByName(entityType, decodedEntityName); var isAdmin = state.authenticatedUser?.isAdmin; if(isAdmin) { @@ -60,8 +68,8 @@ exports.middleware = function (request, response, state, entityType, permissionN if(entity?.owner_id) { if(state.authenticatedUser?.user_id && (state.authenticatedUser?.user_id !== entity.owner_id) || !state.authenticatedUser?.user_id && !hasAnonymousAccess) { const hasPermission = state.authenticatedUser?.user_id ? - entityType === 'recipe' ? sqlTiddlerDatabase.hasRecipePermission(state.authenticatedUser?.user_id, decodedEntityName, isGetRequest ? 'READ' : 'WRITE') - : sqlTiddlerDatabase.hasBagPermission(state.authenticatedUser?.user_id, decodedEntityName, isGetRequest ? 'READ' : 'WRITE') + entityType === 'recipe' ? await sqlTiddlerDatabase.hasRecipePermission(state.authenticatedUser?.user_id, decodedEntityName, isGetRequest ? 'READ' : 'WRITE') + : await sqlTiddlerDatabase.hasBagPermission(state.authenticatedUser?.user_id, decodedEntityName, isGetRequest ? 'READ' : 'WRITE') : false if(!response.headersSent && !hasPermission) { response.writeHead(403, "Forbidden"); @@ -79,7 +87,7 @@ exports.middleware = function (request, response, state, entityType, permissionN return; } else { // Get permission record - const permission = sqlTiddlerDatabase.getPermissionByName(permissionName); + const permission = await sqlTiddlerDatabase.getPermissionByName(permissionName); // ACL Middleware will only apply if the entity has a middleware record if(aclRecord && aclRecord?.permission_id === permission?.permission_id) { // If not authenticated and anonymous access is not allowed, request authentication @@ -92,7 +100,15 @@ exports.middleware = function (request, response, state, entityType, permissionN } // Check ACL permission - var hasPermission = request.method === "POST" || sqlTiddlerDatabase.checkACLPermission(state.authenticatedUser.user_id, entityType, decodedEntityName, permissionName, entity?.owner_id) + var hasPermission = request.method === "POST" + || await sqlTiddlerDatabase.checkACLPermission( + state.authenticatedUser?.user_id, + entityType, + decodedEntityName, + permissionName, + entity?.owner_id + ); + if(!hasPermission && !hasAnonymousAccess) { if(!response.headersSent) { response.writeHead(403, "Forbidden"); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/multipart-forms.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/multipart-forms.js index 6a46699fabb..7b066c3e1e8 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/multipart-forms.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/multipart-forms.js @@ -19,6 +19,15 @@ response - provided by server.js bag_name - name of bag to write to callback - invoked as callback(err,results). Results is an array of titles of imported tiddlers */ +/** + * + * @param {Object} options + * @param {SqlTiddlerStore} options.store + * @param {ServerState} options.state + * @param {ServerResponse} options.response + * @param {string} options.bag_name + * @param {function} options.callback + */ exports.processIncomingStream = function(options) { const self = this; const path = require("path"), @@ -72,7 +81,7 @@ exports.processIncomingStream = function(options) { } else { const partFile = parts.find(part => part.name === "file-to-upload" && !!part.filename); if(!partFile) { - return state.sendResponse(400, {"Content-Type": "text/plain"},"Missing file to upload"); + return options.state.sendResponse(400, {"Content-Type": "text/plain"},"Missing file to upload"); } const type = partFile.headers["content-type"]; const tiddlerFields = { @@ -89,9 +98,12 @@ exports.processIncomingStream = function(options) { filepath: partFile.inboxFilename, type: type, hash: partFile.hash + }).then(() => { + $tw.utils.deleteDirectory(inboxPath); + options.callback(null,[tiddlerFields.title]); + }, err => { + options.callback(err); }); - $tw.utils.deleteDirectory(inboxPath); - options.callback(null,[tiddlerFields.title]); } } }); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/server.js b/plugins/tiddlywiki/multiwikiserver/modules/server.js new file mode 100644 index 00000000000..8e5ce80d867 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/server.js @@ -0,0 +1,151 @@ +/*\ +title: $:/plugins/tiddlywiki/multiwikiserver/server.js +type: application/javascript +module-type: library + +Serve tiddlers over http + +\*/ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Server = exports.ServerManager = exports.defaultVariables = void 0; +const assert_1 = require("assert"); +const fs_1 = require("fs"); +const http = require("http"); +const https = require("https"); +const router_1 = require("./router"); +exports.defaultVariables = { + port: "8080", + host: "127.0.0.1", + "required-plugins": "$:/plugins/tiddlywiki/filesystem,$:/plugins/tiddlywiki/tiddlyweb", + "root-tiddler": "$:/core/save/all", + "root-render-type": "text/plain", + "root-serve-type": "text/html", + "tiddler-render-type": "text/html", + "tiddler-render-template": "$:/core/templates/server/static.tiddler.html", + "system-tiddler-render-type": "text/plain", + "system-tiddler-render-template": "$:/core/templates/wikified-tiddler", + "debug-level": "none", +}; +class ServerManager { + constructor() { + this.servers = new Map(); + this.isClosing = false; + this.onListen = (server) => { + // Log listening details + $tw.utils.log("Serving on " + server.origin(), "brown/orange"); + }; + this.onError = (server, err) => { + $tw.utils.warning(`Error serving on ${server.origin()}: ${err.message}`); + }; + this.onClose = (server) => { + if (this.isClosing) { + $tw.utils.log(`Server closed: ${server.origin()}`, "green"); + } + else { + $tw.utils.warning("Server closed unexpectedly: " + server.origin(), "red"); + server.listen(); // Attempt to restart the listener + } + }; + this.onRequest = (server, request, response) => { + $tw.utils.warning(new Error("The mws-listen command has not been run yet.")); + response.writeHead(500).end("Server is not started yet."); + }; + // Stop listening when we get the "th-quit" hook + $tw.hooks.addHook("th-quit", () => { + this.isClosing = true; + this.servers.forEach(server => server.close()); + }); + } + /** require and create the router, attaching it to the server manager */ + createRouter(params) { + $tw.mws.router = new router_1.Router({ wiki: $tw.wiki, variables: params, store: $tw.mws.store }); + this.onRequest = $tw.mws.router.serverManagerRequestHandler.bind($tw.mws.router); + } + createServer(options, listening) { + const server2 = this.mapServer(options, listening); + this.servers.set(options, server2); + server2.listen(); + return server2; + } + listenCommand(params, listening) { + var _a, _b; + // Handle defaults for port and host + return this.createServer({ + address: params.host || exports.defaultVariables.host, + port: +((_a = params.port) !== null && _a !== void 0 ? _a : "") || +((_b = process.env.PORT) !== null && _b !== void 0 ? _b : "") || +exports.defaultVariables.port, + path: params["path-prefix"] || "", + tlsKeyFile: params["tls-key-file"], + tlsCertFile: params["tls-cert-file"], + tlsPass: params["tls-passphrase"], + }, listening); + } + mapServer(server, listening) { + const { address, port, path, tlsKeyFile, tlsCertFile, tlsPass } = server; + if (tlsKeyFile || tlsCertFile || tlsPass) { + if ((!tlsKeyFile || !tlsCertFile)) { + throw new Error("TLS Key and Cert must be provided together, TLS Passphrase is optional, " + + "but if it is provided then TLS Key and Cert are also required."); + } + const server2 = new Server(https.createServer({ + key: (0, fs_1.readFileSync)(tlsKeyFile), + cert: (0, fs_1.readFileSync)(tlsCertFile), + passphrase: tlsPass, + }), "https", address, port, path, () => { this.onListen(server2); listening === null || listening === void 0 ? void 0 : listening(); }, this.onError, this.onClose, this.onRequest); + return server2; + } + else { + const server2 = new Server(http.createServer(), "http", address, port, path, () => { this.onListen(server2); listening === null || listening === void 0 ? void 0 : listening(); }, this.onError, this.onClose, this.onRequest); + return server2; + } + } +} +exports.ServerManager = ServerManager; +class Server { + constructor(server, protocol, host, port, pathPrefix, onListen, onError, onClose, onRequest) { + this.server = server; + this.protocol = protocol; + this.host = host; + this.port = port; + this.pathPrefix = pathPrefix; + (0, assert_1.ok)(this.protocol === "http" || this.protocol === "https", "Expected protocol to be http or https"); + (0, assert_1.ok)(typeof this.host === "string", "Expected host to be a string"); + (0, assert_1.ok)(typeof this.port === "number" && this.port > 0, "Expected port to be defined"); + (0, assert_1.ok)(!this.pathPrefix || this.pathPrefix.startsWith("/") && !this.pathPrefix.endsWith("/"), "Expected pathPrefix to start with a / but NOT end with one"); + this.server.on("listening", onListen.bind(null, this)); + this.server.on("error", onError.bind(null, this)); + this.server.on("close", onClose.bind(null, this)); + this.server.on("request", onRequest.bind(null, this)); + } + address() { + const address = this.server.address(); + if (!address) { + return null; + } + if (typeof address === "string") { + throw new Error("Expected server.address() to return an object"); + } + return { + protocol: this.protocol, + family: address.family, + address: address.address, + port: address.port, + }; + } + origin() { + const address = this.address(); + if (!address) { + return null; + } + const host = address.family === "IPv6" ? "[" + address.address + "]" : address.address; + return `${address.protocol}://${host}:${address.port}`; + } + listen() { + this.server.listen(this.port, this.host); + } + close() { + this.server.close(); + } +} +exports.Server = Server; +//# sourceMappingURL=server.js.map \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/modules/server.js.map b/plugins/tiddlywiki/multiwikiserver/modules/server.js.map new file mode 100644 index 00000000000..9838f6dea22 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/server.js.map @@ -0,0 +1 @@ +{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/modules/startup.js b/plugins/tiddlywiki/multiwikiserver/modules/startup.js index 42686e889be..01d128b9b95 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/startup.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/startup.js @@ -6,6 +6,7 @@ module-type: startup Multi wiki server initialisation \*/ +//@ts-check (function(){ /*jslint node: true, browser: true */ @@ -16,43 +17,44 @@ Multi wiki server initialisation exports.name = "multiwikiserver"; exports.platforms = ["node"]; exports.before = ["story"]; -exports.synchronous = true; +exports.synchronous = false; -exports.startup = function() { - const store = setupStore(); - $tw.mws = { - store: store, - serverManager: new ServerManager({ - store: store - }) - }; -} - -function setupStore() { +exports.startup = async function() { const path = require("path"); + // Create and initialise the attachment store and the tiddler store - const AttachmentStore = require("$:/plugins/tiddlywiki/multiwikiserver/store/attachments.js").AttachmentStore, - attachmentStore = new AttachmentStore({ - storePath: path.resolve($tw.boot.wikiPath,"store/") - }), - SqlTiddlerStore = require("$:/plugins/tiddlywiki/multiwikiserver/store/sql-tiddler-store.js").SqlTiddlerStore, - store = new SqlTiddlerStore({ - databasePath: path.resolve($tw.boot.wikiPath,"store/database.sqlite"), - engine: $tw.wiki.getTiddlerText("$:/config/MultiWikiServer/Engine","better"), // better || wasm - attachmentStore: attachmentStore - }); - return store; -} + /** @type {typeof import("../src/store/attachments")} */ + const { AttachmentStore } = require("./store/attachments.js") + const attachmentStore = new AttachmentStore({ + storePath: path.resolve($tw.boot.wikiPath, "store/") + }); -function ServerManager(store) { - this.servers = []; -} + const databasePath = path.resolve($tw.boot.wikiPath, "store/database.sqlite"); + + $tw.utils.createFileDirectories(databasePath); + + const {PrismaClient} = require("@prisma/client"); + const connection = new PrismaClient({ + datasourceUrl: `file:${databasePath}?connection_limit=5`, + log: [ "info", "warn", "error"] + }); -ServerManager.prototype.createServer = function(options) { - const MWSServer = require("$:/plugins/tiddlywiki/multiwikiserver/mws-server.js").Server, - server = new MWSServer(options); - this.servers.push(server); - return server; + const {SqlTiddlerStore} = require("./store/sql-tiddler-store.js"); + const store = new SqlTiddlerStore({adminWiki: $tw.wiki, attachmentStore, prisma: connection}); + + const { ServerManager } = require("./server.js"); + const serverManager = new ServerManager(); + + // router will be set by the first mws-listen command + $tw.mws = { + store, serverManager, connection, databasePath, + transaction: async (type, fn) => { + return await connection.$transaction(async prisma => { + const store = new SqlTiddlerStore({adminWiki: $tw.wiki, attachmentStore, prisma, transactionType: type}); + return await fn(store); + }); + } + }; } })(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/attachments.js b/plugins/tiddlywiki/multiwikiserver/modules/store/attachments.js index 352f96a8386..59947eff713 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/attachments.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/attachments.js @@ -8,174 +8,162 @@ Class to handle the attachments in the filing system The store folder looks like this: store/ - inbox/ - files that are in the process of being uploaded via a multipart form upload - 202402282125432742/ - 0 - 1 - ... - ... - files/ - files that are the text content of large tiddlers - b7def178-79c4-4d88-b7a4-39763014a58b/ - data.jpg - the extension is provided for convenience when directly inspecting the file system - meta.json - contains: - { - "filename": "data.jpg", - "type": "video/mp4", - "uploaded": "2024021821224823" - } - database.sql - The database file (managed by sql-tiddler-database.js) + inbox/ - files that are in the process of being uploaded via a multipart form upload + 202402282125432742/ + 0 + 1 + ... + ... + files/ - files that are the text content of large tiddlers + b7def178-79c4-4d88-b7a4-39763014a58b/ + data.jpg - the extension is provided for convenience when directly inspecting the file system + meta.json - contains: + { + "filename": "data.jpg", + "type": "video/mp4", + "uploaded": "2024021821224823" + } + database.sql - The database file (managed by sql-tiddler-database.js) \*/ - -(function() { - +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.AttachmentStore = void 0; /* Class to handle an attachment store. Options include: storePath - path to the store */ -function AttachmentStore(options) { - options = options || {}; - this.storePath = options.storePath; +class AttachmentStore { + constructor(options) { + this.storePath = options.storePath; + } + /* + Check if an attachment name is valid + */ + isValidAttachmentName(attachment_name) { + const re = new RegExp("^[a-f0-9]{64}$"); + return re.test(attachment_name); + } + /* + Saves an attachment to a file. Options include: + + text: text content (may be binary) + type: MIME type of content + reference: reference to use for debugging + _canonical_uri: canonical uri of the content + */ + saveAttachment(options) { + const path = require("path"), fs = require("fs"); + // Compute the content hash for naming the attachment + const contentHash = $tw.sjcl.codec.hex.fromBits($tw.sjcl.hash.sha256.hash(options.text)).slice(0, 64).toString(); + // Choose the best file extension for the attachment given its type + const contentTypeInfo = $tw.config.contentTypeInfo[options.type] || $tw.config.contentTypeInfo["application/octet-stream"]; + // Creat the attachment directory + const attachmentPath = path.resolve(this.storePath, "files", contentHash); + $tw.utils.createDirectory(attachmentPath); + // Save the data file + const dataFilename = "data" + contentTypeInfo.extension; + fs.writeFileSync(path.resolve(attachmentPath, dataFilename), options.text, contentTypeInfo.encoding); + // Save the meta.json file + fs.writeFileSync(path.resolve(attachmentPath, "meta.json"), JSON.stringify({ + _canonical_uri: options._canonical_uri, + created: $tw.utils.stringifyDate(new Date()), + modified: $tw.utils.stringifyDate(new Date()), + contentHash: contentHash, + filename: dataFilename, + type: options.type + }, null, 4)); + return contentHash; + } + /* + Adopts an attachment file into the store + */ + adoptAttachment(incomingFilepath, type, hash, _canonical_uri) { + const path = require("path"), fs = require("fs"); + // Choose the best file extension for the attachment given its type + const contentTypeInfo = $tw.config.contentTypeInfo[type] || $tw.config.contentTypeInfo["application/octet-stream"]; + // Creat the attachment directory + const attachmentPath = path.resolve(this.storePath, "files", hash); + $tw.utils.createDirectory(attachmentPath); + // Rename the data file + const dataFilename = "data" + contentTypeInfo.extension, dataFilepath = path.resolve(attachmentPath, dataFilename); + fs.renameSync(incomingFilepath, dataFilepath); + // Save the meta.json file + fs.writeFileSync(path.resolve(attachmentPath, "meta.json"), JSON.stringify({ + _canonical_uri: _canonical_uri, + created: $tw.utils.stringifyDate(new Date()), + modified: $tw.utils.stringifyDate(new Date()), + contentHash: hash, + filename: dataFilename, + type: type + }, null, 4)); + return hash; + } + /* + Get an attachment ready to stream. Returns null if there is an error or: + stream: filestream of file + type: type of file + */ + getAttachmentStream(attachment_name) { + const path = require("path"), fs = require("fs"); + // Check the attachment name + if (this.isValidAttachmentName(attachment_name)) { + // Construct the path to the attachment directory + const attachmentPath = path.resolve(this.storePath, "files", attachment_name); + // Read the meta.json file + const metaJsonPath = path.resolve(attachmentPath, "meta.json"); + if (fs.existsSync(metaJsonPath) && fs.statSync(metaJsonPath).isFile()) { + const meta = $tw.utils.parseJSONSafe(fs.readFileSync(metaJsonPath, "utf8"), function () { return null; }); + if (meta) { + const dataFilepath = path.resolve(attachmentPath, meta.filename); + // Check if the data file exists + if (fs.existsSync(dataFilepath) && fs.statSync(dataFilepath).isFile()) { + // Stream the file + return { + stream: fs.createReadStream(dataFilepath), + type: meta.type + }; + } + } + } + } + // An error occured + return null; + } + /* + Get the size of an attachment file given the contentHash. + Returns the size in bytes, or null if the file doesn't exist. + */ + getAttachmentFileSize(contentHash) { + const path = require("path"), fs = require("fs"); + // Construct the path to the attachment directory + const attachmentPath = path.resolve(this.storePath, "files", contentHash); + // Read the meta.json file + const metaJsonPath = path.resolve(attachmentPath, "meta.json"); + if (fs.existsSync(metaJsonPath) && fs.statSync(metaJsonPath).isFile()) { + const meta = $tw.utils.parseJSONSafe(fs.readFileSync(metaJsonPath, "utf8"), function () { return null; }); + if (meta) { + const dataFilepath = path.resolve(attachmentPath, meta.filename); + // Check if the data file exists and return its size + if (fs.existsSync(dataFilepath) && fs.statSync(dataFilepath).isFile()) { + return fs.statSync(dataFilepath).size; + } + } + } + // Return null if the file doesn't exist or there was an error + return null; + } + getAttachmentMetadata(attachmentBlob) { + const path = require("path"), fs = require("fs"); + const attachmentPath = path.resolve(this.storePath, "files", attachmentBlob); + const metaJsonPath = path.resolve(attachmentPath, "meta.json"); + if (fs.existsSync(metaJsonPath)) { + const metadata = JSON.parse(fs.readFileSync(metaJsonPath, "utf8")); + return metadata; + } + return null; + } } - -/* -Check if an attachment name is valid -*/ -AttachmentStore.prototype.isValidAttachmentName = function(attachment_name) { - const re = new RegExp('^[a-f0-9]{64}$'); - return re.test(attachment_name); -}; - -/* -Saves an attachment to a file. Options include: - -text: text content (may be binary) -type: MIME type of content -reference: reference to use for debugging -_canonical_uri: canonical uri of the content -*/ -AttachmentStore.prototype.saveAttachment = function(options) { - const path = require("path"), - fs = require("fs"); - // Compute the content hash for naming the attachment - const contentHash = $tw.sjcl.codec.hex.fromBits($tw.sjcl.hash.sha256.hash(options.text)).slice(0,64).toString(); - // Choose the best file extension for the attachment given its type - const contentTypeInfo = $tw.config.contentTypeInfo[options.type] || $tw.config.contentTypeInfo["application/octet-stream"]; - // Creat the attachment directory - const attachmentPath = path.resolve(this.storePath,"files",contentHash); - $tw.utils.createDirectory(attachmentPath); - // Save the data file - const dataFilename = "data" + contentTypeInfo.extension; - fs.writeFileSync(path.resolve(attachmentPath,dataFilename),options.text,contentTypeInfo.encoding); - // Save the meta.json file - fs.writeFileSync(path.resolve(attachmentPath,"meta.json"),JSON.stringify({ - _canonical_uri: options._canonical_uri, - created: $tw.utils.stringifyDate(new Date()), - modified: $tw.utils.stringifyDate(new Date()), - contentHash: contentHash, - filename: dataFilename, - type: options.type - },null,4)); - return contentHash; -}; - -/* -Adopts an attachment file into the store -*/ -AttachmentStore.prototype.adoptAttachment = function(incomingFilepath,type,hash,_canonical_uri) { - const path = require("path"), - fs = require("fs"); - // Choose the best file extension for the attachment given its type - const contentTypeInfo = $tw.config.contentTypeInfo[type] || $tw.config.contentTypeInfo["application/octet-stream"]; - // Creat the attachment directory - const attachmentPath = path.resolve(this.storePath,"files",hash); - $tw.utils.createDirectory(attachmentPath); - // Rename the data file - const dataFilename = "data" + contentTypeInfo.extension, - dataFilepath = path.resolve(attachmentPath,dataFilename); - fs.renameSync(incomingFilepath,dataFilepath); - // Save the meta.json file - fs.writeFileSync(path.resolve(attachmentPath,"meta.json"),JSON.stringify({ - _canonical_uri: _canonical_uri, - created: $tw.utils.stringifyDate(new Date()), - modified: $tw.utils.stringifyDate(new Date()), - contentHash: hash, - filename: dataFilename, - type: type - },null,4)); - return hash; -}; - -/* -Get an attachment ready to stream. Returns null if there is an error or: -stream: filestream of file -type: type of file -*/ -AttachmentStore.prototype.getAttachmentStream = function(attachment_name) { - const path = require("path"), - fs = require("fs"); - // Check the attachment name - if(this.isValidAttachmentName(attachment_name)) { - // Construct the path to the attachment directory - const attachmentPath = path.resolve(this.storePath,"files",attachment_name); - // Read the meta.json file - const metaJsonPath = path.resolve(attachmentPath,"meta.json"); - if(fs.existsSync(metaJsonPath) && fs.statSync(metaJsonPath).isFile()) { - const meta = $tw.utils.parseJSONSafe(fs.readFileSync(metaJsonPath,"utf8"),function() {return null;}); - if(meta) { - const dataFilepath = path.resolve(attachmentPath,meta.filename); - // Check if the data file exists - if(fs.existsSync(dataFilepath) && fs.statSync(dataFilepath).isFile()) { - // Stream the file - return { - stream: fs.createReadStream(dataFilepath), - type: meta.type - }; - } - } - } - } - // An error occured - return null; -}; - -/* -Get the size of an attachment file given the contentHash. -Returns the size in bytes, or null if the file doesn't exist. -*/ -AttachmentStore.prototype.getAttachmentFileSize = function(contentHash) { - const path = require("path"), - fs = require("fs"); - // Construct the path to the attachment directory - const attachmentPath = path.resolve(this.storePath, "files", contentHash); - // Read the meta.json file - const metaJsonPath = path.resolve(attachmentPath, "meta.json"); - if(fs.existsSync(metaJsonPath) && fs.statSync(metaJsonPath).isFile()) { - const meta = $tw.utils.parseJSONSafe(fs.readFileSync(metaJsonPath, "utf8"), function() { return null; }); - if(meta) { - const dataFilepath = path.resolve(attachmentPath, meta.filename); - // Check if the data file exists and return its size - if(fs.existsSync(dataFilepath) && fs.statSync(dataFilepath).isFile()) { - return fs.statSync(dataFilepath).size; - } - } - } - // Return null if the file doesn't exist or there was an error - return null; -}; - -AttachmentStore.prototype.getAttachmentMetadata = function(attachmentBlob) { - const path = require("path"), - fs = require("fs"); - const attachmentPath = path.resolve(this.storePath, "files", attachmentBlob); - const metaJsonPath = path.resolve(attachmentPath, "meta.json"); - if(fs.existsSync(metaJsonPath)) { - const metadata = JSON.parse(fs.readFileSync(metaJsonPath, "utf8")); - return metadata; - } - return null; -}; exports.AttachmentStore = AttachmentStore; - -})(); +//# sourceMappingURL=attachments.js.map \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/attachments.js.map b/plugins/tiddlywiki/multiwikiserver/modules/store/attachments.js.map new file mode 100644 index 00000000000..21c9b6a84a8 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/attachments.js.map @@ -0,0 +1 @@ +{"version":3,"file":"attachments.js","sourceRoot":"","sources":["../../src/store/attachments.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-engine.js b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-engine.js deleted file mode 100644 index 00d15edf370..00000000000 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-engine.js +++ /dev/null @@ -1,142 +0,0 @@ -/*\ -title: $:/plugins/tiddlywiki/multiwikiserver/store/sql-engine.js -type: application/javascript -module-type: library - -Low level functions to work with the SQLite engine, either via better-sqlite3 or node-sqlite3-wasm. - -This class is intended to encapsulate all engine-specific logic. - -\*/ - -(function() { - -/* -Create a database engine. Options include: - -databasePath - path to the database file (can be ":memory:" or missing to get a temporary database) -engine - wasm | better -*/ -function SqlEngine(options) { - options = options || {}; - // Initialise transaction mechanism - this.transactionDepth = 0; - // Initialise the statement cache - this.statements = Object.create(null); // Hashmap by SQL text of statement objects - // Choose engine - this.engine = options.engine || "node"; // node | wasm | better - // Create the database file directories if needed - if(options.databasePath) { - $tw.utils.createFileDirectories(options.databasePath); - } - // Create the database - const databasePath = options.databasePath || ":memory:"; - let Database; - switch(this.engine) { - case "node": - ({ DatabaseSync: Database } = require("node:sqlite")); - break; - case "wasm": - ({ Database } = require("node-sqlite3-wasm")); - break; - case "better": - Database = require("better-sqlite3"); - break; - } - this.db = new Database(databasePath,{ - verbose: undefined && console.log - }); - // Turn on WAL mode for better-sqlite3 - if(this.engine === "better") { - // See https://github.com/WiseLibs/better-sqlite3/blob/master/docs/performance.md - this.db.pragma("journal_mode = WAL"); - } -} - -SqlEngine.prototype.close = function() { - for(const sql in this.statements) { - if(this.statements[sql].finalize) { - this.statements[sql].finalize(); - } - } - this.statements = Object.create(null); - this.db.close(); - this.db = undefined; -}; - -SqlEngine.prototype.normaliseParams = function(params) { - params = params || {}; - const result = Object.create(null); - for(const paramName in params) { - if(this.engine !== "wasm" && paramName.startsWith("$")) { - result[paramName.slice(1)] = params[paramName]; - } else { - result[paramName] = params[paramName]; - } - } - return result; -}; - -SqlEngine.prototype.prepareStatement = function(sql) { - if(!(sql in this.statements)) { - this.statements[sql] = this.db.prepare(sql); - } - return this.statements[sql]; -}; - -SqlEngine.prototype.runStatement = function(sql,params) { - params = this.normaliseParams(params); - const statement = this.prepareStatement(sql); - return statement.run(params); -}; - -SqlEngine.prototype.runStatementGet = function(sql,params) { - params = this.normaliseParams(params); - const statement = this.prepareStatement(sql); - return statement.get(params); -}; - -SqlEngine.prototype.runStatementGetAll = function(sql,params) { - params = this.normaliseParams(params); - const statement = this.prepareStatement(sql); - return statement.all(params); -}; - -SqlEngine.prototype.runStatements = function(sqlArray) { - for(const sql of sqlArray) { - this.runStatement(sql); - } -}; - -/* -Execute the given function in a transaction, committing if successful but rolling back if an error occurs. Returns whatever the given function returns. - -Calls to this function can be safely nested, but only the topmost call will actually take place in a transaction. - -TODO: better-sqlite3 provides its own transaction method which we should be using if available -*/ -SqlEngine.prototype.transaction = function(fn) { - const alreadyInTransaction = this.transactionDepth > 0; - this.transactionDepth++; - try { - if(alreadyInTransaction) { - return fn(); - } else { - this.runStatement(`BEGIN TRANSACTION`); - try { - var result = fn(); - this.runStatement(`COMMIT TRANSACTION`); - } catch(e) { - this.runStatement(`ROLLBACK TRANSACTION`); - throw(e); - } - return result; - } - } finally { - this.transactionDepth--; - } -}; - -exports.SqlEngine = SqlEngine; - -})(); \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js index 14f8641f4ce..aea91b5c0b6 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js @@ -9,1471 +9,1861 @@ This class is intended to encapsulate all the SQL queries used to access the dat Validation is for the most part left to the caller \*/ - -(function() { - -/* -Create a tiddler store. Options include: - -databasePath - path to the database file (can be ":memory:" to get a temporary database) -engine - wasm | better -*/ -function SqlTiddlerDatabase(options) { - options = options || {}; - const SqlEngine = require("$:/plugins/tiddlywiki/multiwikiserver/store/sql-engine.js").SqlEngine; - this.engine = new SqlEngine({ - databasePath: options.databasePath, - engine: options.engine - }); - this.entityTypeToTableMap = { - bag: { - table: "bags", - column: "bag_name" - }, - recipe: { - table: "recipes", - column: "recipe_name" - } - }; +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.SqlTiddlerDatabase = void 0; +exports.okType = okType; +exports.okTypeTruthy = okTypeTruthy; +exports.okEntityType = okEntityType; +const assert_1 = require("assert"); +const TYPEOF_ENUM = typeof ""; +function okType(value, type, msg) { + switch (type) { + case "null": + (0, assert_1.ok)(value === null, msg); + break; + case "nully": + (0, assert_1.ok)(value === null || value === undefined, msg); + break; + default: (0, assert_1.ok)(typeof value === type, msg); + } } - -SqlTiddlerDatabase.prototype.close = function() { - this.engine.close(); -}; - - -SqlTiddlerDatabase.prototype.transaction = function(fn) { - return this.engine.transaction(fn); -}; - -SqlTiddlerDatabase.prototype.createTables = function() { - this.engine.runStatements([` - -- Users table - CREATE TABLE IF NOT EXISTS users ( - user_id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - email TEXT UNIQUE NOT NULL, - password TEXT NOT NULL, - created_at TEXT DEFAULT (datetime('now')), - last_login TEXT - ) - `,` - -- User Session table - CREATE TABLE IF NOT EXISTS sessions ( - user_id INTEGER NOT NULL, - session_id TEXT NOT NULL, - created_at TEXT NOT NULL, - last_accessed TEXT NOT NULL, - PRIMARY KEY (session_id), - FOREIGN KEY (user_id) REFERENCES users(user_id) - ) - `,` - -- Groups table - CREATE TABLE IF NOT EXISTS groups ( - group_id INTEGER PRIMARY KEY AUTOINCREMENT, - group_name TEXT UNIQUE NOT NULL, - description TEXT - ) - `,` - -- Roles table - CREATE TABLE IF NOT EXISTS roles ( - role_id INTEGER PRIMARY KEY AUTOINCREMENT, - role_name TEXT UNIQUE NOT NULL, - description TEXT - ) - `,` - -- Permissions table - CREATE TABLE IF NOT EXISTS permissions ( - permission_id INTEGER PRIMARY KEY AUTOINCREMENT, - permission_name TEXT UNIQUE NOT NULL, - description TEXT - ) - `,` - -- User-Group association table - CREATE TABLE IF NOT EXISTS user_groups ( - user_id INTEGER, - group_id INTEGER, - PRIMARY KEY (user_id, group_id), - FOREIGN KEY (user_id) REFERENCES users(user_id), - FOREIGN KEY (group_id) REFERENCES groups(group_id) - ) - `,` - -- User-Role association table - CREATE TABLE IF NOT EXISTS user_roles ( - user_id INTEGER, - role_id INTEGER, - PRIMARY KEY (user_id, role_id), - FOREIGN KEY (user_id) REFERENCES users(user_id), - FOREIGN KEY (role_id) REFERENCES roles(role_id) - ) - `,` - -- Group-Role association table - CREATE TABLE IF NOT EXISTS group_roles ( - group_id INTEGER, - role_id INTEGER, - PRIMARY KEY (group_id, role_id), - FOREIGN KEY (group_id) REFERENCES groups(group_id), - FOREIGN KEY (role_id) REFERENCES roles(role_id) - ) - `,` - -- Role-Permission association table - CREATE TABLE IF NOT EXISTS role_permissions ( - role_id INTEGER, - permission_id INTEGER, - PRIMARY KEY (role_id, permission_id), - FOREIGN KEY (role_id) REFERENCES roles(role_id), - FOREIGN KEY (permission_id) REFERENCES permissions(permission_id) - ) - `,` - -- Bags have names and access control settings - CREATE TABLE IF NOT EXISTS bags ( - bag_id INTEGER PRIMARY KEY AUTOINCREMENT, - bag_name TEXT UNIQUE NOT NULL, - accesscontrol TEXT NOT NULL, - description TEXT NOT NULL - ) - `,` - -- Recipes have names... - CREATE TABLE IF NOT EXISTS recipes ( - recipe_id INTEGER PRIMARY KEY AUTOINCREMENT, - recipe_name TEXT UNIQUE NOT NULL, - description TEXT NOT NULL, - owner_id INTEGER, - FOREIGN KEY (owner_id) REFERENCES users(user_id) - ) - `,` - -- ...and recipes also have an ordered list of bags - CREATE TABLE IF NOT EXISTS recipe_bags ( - recipe_id INTEGER NOT NULL, - bag_id INTEGER NOT NULL, - position INTEGER NOT NULL, - 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 NOT NULL, - title TEXT NOT NULL, - is_deleted BOOLEAN NOT NULL, - attachment_blob TEXT, -- null or the name of an attachment blob - 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 NOT NULL, - field_value TEXT NOT NULL, - FOREIGN KEY (tiddler_id) REFERENCES tiddlers(tiddler_id) ON UPDATE CASCADE ON DELETE CASCADE, - UNIQUE (tiddler_id, field_name) - ) - `,` - -- ACL table (using bag/recipe ids directly) - CREATE TABLE IF NOT EXISTS acl ( - acl_id INTEGER PRIMARY KEY AUTOINCREMENT, - entity_name TEXT NOT NULL, - entity_type TEXT NOT NULL CHECK (entity_type IN ('bag', 'recipe')), - role_id INTEGER, - permission_id INTEGER, - FOREIGN KEY (role_id) REFERENCES roles(role_id), - FOREIGN KEY (permission_id) REFERENCES permissions(permission_id) - ) - `,` - -- Indexes for performance (we can add more as needed based on query patterns) - CREATE INDEX IF NOT EXISTS idx_tiddlers_bag_id ON tiddlers(bag_id) - `,` - CREATE INDEX IF NOT EXISTS idx_fields_tiddler_id ON fields(tiddler_id) - `,` - CREATE INDEX IF NOT EXISTS idx_recipe_bags_recipe_id ON recipe_bags(recipe_id) - `,` - CREATE INDEX IF NOT EXISTS idx_acl_entity_id ON acl(entity_name) - `]); -}; - -SqlTiddlerDatabase.prototype.listBags = function() { - const rows = this.engine.runStatementGetAll(` - SELECT bag_name, bag_id, accesscontrol, description - FROM bags - ORDER BY bag_name - `); - return rows; -}; - -/* -Create or update a bag -Returns the bag_id of the bag -*/ -SqlTiddlerDatabase.prototype.createBag = function(bag_name,description,accesscontrol) { - accesscontrol = accesscontrol || ""; - // Run the queries - var bag = this.engine.runStatement(` - INSERT OR IGNORE INTO bags (bag_name, accesscontrol, description) - VALUES ($bag_name, '', '') - `,{ - $bag_name: bag_name - }); - const updateBags = this.engine.runStatement(` - UPDATE bags - SET accesscontrol = $accesscontrol, - description = $description - WHERE bag_name = $bag_name - `,{ - $bag_name: bag_name, - $accesscontrol: accesscontrol, - $description: description - }); - return updateBags.lastInsertRowid; -}; - -/* -Returns array of {recipe_name:,recipe_id:,description:,bag_names: []} -*/ -SqlTiddlerDatabase.prototype.listRecipes = function() { - const rows = this.engine.runStatementGetAll(` - SELECT r.recipe_name, r.recipe_id, r.description, r.owner_id, b.bag_name, rb.position - FROM recipes AS r - JOIN recipe_bags AS rb ON rb.recipe_id = r.recipe_id - JOIN bags AS b ON rb.bag_id = b.bag_id - ORDER BY r.recipe_name, rb.position - `); - const results = []; - let currentRecipeName = null, currentRecipeIndex = -1; - for(const row of rows) { - if(row.recipe_name !== currentRecipeName) { - currentRecipeName = row.recipe_name; - currentRecipeIndex += 1; - results.push({ - recipe_name: row.recipe_name, - recipe_id: row.recipe_id, - description: row.description, - owner_id: row.owner_id, - bag_names: [] - }); - } - results[currentRecipeIndex].bag_names.push(row.bag_name); - } - return results; -}; - -/* -Create or update a recipe -Returns the recipe_id of the recipe -*/ -SqlTiddlerDatabase.prototype.createRecipe = function(recipe_name,bag_names,description) { - // Run the queries - this.engine.runStatement(` - -- Delete existing recipe_bags entries for this recipe - DELETE FROM recipe_bags WHERE recipe_id = (SELECT recipe_id FROM recipes WHERE recipe_name = $recipe_name) - `,{ - $recipe_name: recipe_name - }); - const updateRecipes = this.engine.runStatement(` - -- Create the entry in the recipes table if required - INSERT OR REPLACE INTO recipes (recipe_name, description) - VALUES ($recipe_name, $description) - `,{ - $recipe_name: recipe_name, - $description: description - }); - this.engine.runStatement(` - INSERT INTO recipe_bags (recipe_id, bag_id, position) - SELECT r.recipe_id, b.bag_id, j.key as position - FROM recipes r - JOIN bags b - INNER JOIN json_each($bag_names) AS j ON j.value = b.bag_name - WHERE r.recipe_name = $recipe_name - `,{ - $recipe_name: recipe_name, - $bag_names: JSON.stringify(bag_names) - }); - - return updateRecipes.lastInsertRowid; -}; - -/* -Assign a recipe to a user -*/ -SqlTiddlerDatabase.prototype.assignRecipeToUser = function(recipe_name,user_id) { - this.engine.runStatement(` - UPDATE recipes SET owner_id = $user_id WHERE recipe_name = $recipe_name - `,{ - $recipe_name: recipe_name, - $user_id: user_id - }); -}; - -/* -Returns {tiddler_id:} -*/ -SqlTiddlerDatabase.prototype.saveBagTiddler = function(tiddlerFields,bag_name,attachment_blob) { - attachment_blob = attachment_blob || null; - // Update the tiddlers table - var info = this.engine.runStatement(` - INSERT OR REPLACE INTO tiddlers (bag_id, title, is_deleted, attachment_blob) - VALUES ( - (SELECT bag_id FROM bags WHERE bag_name = $bag_name), - $title, - FALSE, - $attachment_blob - ) - `,{ - $title: tiddlerFields.title, - $attachment_blob: attachment_blob, - $bag_name: bag_name - }); - // Update the fields table - this.engine.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: bag_name, - $field_values: JSON.stringify(Object.assign({},tiddlerFields,{title: undefined})) - }); - return { - tiddler_id: info.lastInsertRowid - } -}; - -/* -Returns {tiddler_id:,bag_name:} or null if the recipe is empty -*/ -SqlTiddlerDatabase.prototype.saveRecipeTiddler = function(tiddlerFields,recipe_name,attachment_blob) { - // Find the topmost bag in the recipe - var row = this.engine.runStatementGet(` - 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: recipe_name - }); - if(!row) { - return null; - } - // Save the tiddler to the topmost bag - var info = this.saveBagTiddler(tiddlerFields,row.bag_name,attachment_blob); - return { - tiddler_id: info.tiddler_id, - bag_name: row.bag_name - }; -}; - -/* -Returns {tiddler_id:} of the delete marker -*/ -SqlTiddlerDatabase.prototype.deleteTiddler = function(title,bag_name) { - // Delete the fields of this tiddler - this.engine.runStatement(` - 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: bag_name - }); - // Mark the tiddler itself as deleted - const rowDeleteMarker = this.engine.runStatement(` - INSERT OR REPLACE INTO tiddlers (bag_id, title, is_deleted, attachment_blob) - VALUES ( - (SELECT bag_id FROM bags WHERE bag_name = $bag_name), - $title, - TRUE, - NULL - ) - `,{ - $title: title, - $bag_name: bag_name - }); - return {tiddler_id: rowDeleteMarker.lastInsertRowid}; -}; - -/* -returns {tiddler_id:,tiddler:,attachment_blob:} -*/ -SqlTiddlerDatabase.prototype.getBagTiddler = function(title,bag_name) { - const rowTiddler = this.engine.runStatementGet(` - SELECT t.tiddler_id, t.attachment_blob - FROM bags AS b - INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id - WHERE t.title = $title AND b.bag_name = $bag_name AND t.is_deleted = FALSE - `,{ - $title: title, - $bag_name: bag_name - }); - if(!rowTiddler) { - return null; - } - const rows = this.engine.runStatementGetAll(` - SELECT field_name, field_value, tiddler_id - FROM fields - WHERE tiddler_id = $tiddler_id - `,{ - $tiddler_id: rowTiddler.tiddler_id - }); - if(rows.length === 0) { - return null; - } else { - return { - tiddler_id: rows[0].tiddler_id, - attachment_blob: rowTiddler.attachment_blob, - tiddler: rows.reduce((accumulator,value) => { - accumulator[value["field_name"]] = value.field_value; - return accumulator; - },{title: title}) - }; - } -}; - -/* -Returns {bag_name:, tiddler: {fields}, tiddler_id:, attachment_blob:} -*/ -SqlTiddlerDatabase.prototype.getRecipeTiddler = function(title,recipe_name) { - const rowTiddlerId = this.engine.runStatementGet(` - SELECT t.tiddler_id, t.attachment_blob, b.bag_name - FROM bags AS b - INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id - 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 - AND t.is_deleted = FALSE - ORDER BY rb.position DESC - LIMIT 1 - `,{ - $title: title, - $recipe_name: recipe_name - }); - if(!rowTiddlerId) { - return null; - } - // Get the fields - const rows = this.engine.runStatementGetAll(` - SELECT field_name, field_value - FROM fields - WHERE tiddler_id = $tiddler_id - `,{ - $tiddler_id: rowTiddlerId.tiddler_id - }); - return { - bag_name: rowTiddlerId.bag_name, - tiddler_id: rowTiddlerId.tiddler_id, - attachment_blob: rowTiddlerId.attachment_blob, - tiddler: rows.reduce((accumulator,value) => { - accumulator[value["field_name"]] = value.field_value; - return accumulator; - },{title: title}) - }; -}; - -/* -Checks if a user has permission to access a recipe -*/ -SqlTiddlerDatabase.prototype.hasRecipePermission = function(userId, recipeName, permissionName) { - try { - // check if the user is the owner of the entity - const recipe = this.engine.runStatementGet(` - SELECT owner_id - FROM recipes - WHERE recipe_name = $recipe_name - `, { - $recipe_name: recipeName - }); - - if(!!recipe?.owner_id && recipe?.owner_id === userId) { - return true; - } else { - var permission = this.checkACLPermission(userId, "recipe", recipeName, permissionName, recipe?.owner_id) - return permission; - } - - } catch (error) { - console.error(error) - return false - } -}; - -/* -Checks if a user has permission to access a bag -*/ -SqlTiddlerDatabase.prototype.hasBagPermission = function(userId, bagName, permissionName) { - return this.checkACLPermission(userId, "bag", bagName, permissionName) -}; - -SqlTiddlerDatabase.prototype.getACLByName = function(entityType, entityName, fetchAll) { - const entityInfo = this.entityTypeToTableMap[entityType]; - if (!entityInfo) { - throw new Error("Invalid entity type: " + entityType); - } - - // First, check if there's an ACL record for the entity and get the permission_id - var checkACLExistsQuery = ` - SELECT acl.*, permissions.permission_name - FROM acl - LEFT JOIN permissions ON acl.permission_id = permissions.permission_id - WHERE acl.entity_type = $entity_type - AND acl.entity_name = $entity_name - `; - - if (!fetchAll) { - checkACLExistsQuery += ' LIMIT 1' - } - - const aclRecord = this.engine[fetchAll ? 'runStatementGetAll' : 'runStatementGet'](checkACLExistsQuery, { - $entity_type: entityType, - $entity_name: entityName - }); - - return aclRecord; +function okTypeTruthy(value, type, msg) { + switch (type) { + case "null": + (0, assert_1.ok)(value === null, msg); + break; + case "nully": + (0, assert_1.ok)(value === null || value === undefined, msg); + break; + default: (0, assert_1.ok)(typeof value === type, msg); + } } - -SqlTiddlerDatabase.prototype.checkACLPermission = function(userId, entityType, entityName, permissionName, ownerId) { - try { - // if the entityName starts with "$:/", we'll assume its a system bag/recipe, then grant the user permission - if(entityName.startsWith("$:/")) { - return true; - } - - const aclRecords = this.getACLByName(entityType, entityName, true); - const aclRecord = aclRecords.find(record => record.permission_name === permissionName); - - // If no ACL record exists, return true for hasPermission - if ((!aclRecord && !ownerId && aclRecords.length === 0) || ((!!aclRecord && !!ownerId) && ownerId === userId)) { - return true; - } - - // If ACL record exists, check for user permission using the retrieved permission_id - const checkPermissionQuery = ` - SELECT * - FROM users u - JOIN user_roles ur ON u.user_id = ur.user_id - JOIN roles r ON ur.role_id = r.role_id - JOIN acl a ON r.role_id = a.role_id - WHERE u.user_id = $user_id - AND a.entity_type = $entity_type - AND a.entity_name = $entity_name - AND a.permission_id = $permission_id - LIMIT 1 - `; - - const result = this.engine.runStatementGet(checkPermissionQuery, { - $user_id: userId, - $entity_type: entityType, - $entity_name: entityName, - $permission_id: aclRecord?.permission_id - }); - - let hasPermission = result !== undefined; - - return hasPermission; - - } catch (error) { - console.error(error); - return false - } -}; - -/** - * Returns the ACL records for an entity (bag or recipe) - */ -SqlTiddlerDatabase.prototype.getEntityAclRecords = function(entityName) { - const checkACLExistsQuery = ` - SELECT * - FROM acl - WHERE entity_name = $entity_name - `; - - const aclRecords = this.engine.runStatementGetAll(checkACLExistsQuery, { - $entity_name: entityName - }); - - return aclRecords +function okEntityType(value) { + (0, assert_1.ok)(value === "bag" || value === "recipe", "Invalid entity type: " + value); } - -/* -Get the entity by name -*/ -SqlTiddlerDatabase.prototype.getEntityByName = function(entityType, entityName) { - const entityInfo = this.entityTypeToTableMap[entityType]; - if (entityInfo) { - return this.engine.runStatementGet(`SELECT * FROM ${entityInfo.table} WHERE ${entityInfo.column} = $entity_name`, { - $entity_name: entityName - }); - } - return null; -} - -/* -Get the titles of the tiddlers in a bag. Returns an empty array for bags that do not exist -*/ -SqlTiddlerDatabase.prototype.getBagTiddlers = function(bag_name) { - const rows = this.engine.runStatementGetAll(` - SELECT DISTINCT title, tiddler_id - FROM tiddlers - WHERE bag_id IN ( - SELECT bag_id - FROM bags - WHERE bag_name = $bag_name - ) - AND tiddlers.is_deleted = FALSE - ORDER BY title ASC - `,{ - $bag_name: bag_name - }); - return rows; -}; - -/* -Get the tiddler_id of the newest tiddler in a bag. Returns null for bags that do not exist -*/ -SqlTiddlerDatabase.prototype.getBagLastTiddlerId = function(bag_name) { - const row = this.engine.runStatementGet(` - SELECT tiddler_id - FROM tiddlers - WHERE bag_id IN ( - SELECT bag_id - FROM bags - WHERE bag_name = $bag_name - ) - ORDER BY tiddler_id DESC - LIMIT 1 - `,{ - $bag_name: bag_name - }); - if(row) { - return row.tiddler_id; - } else { - return null; - } -}; - -/* -Get the metadata of the tiddlers in a recipe as an array [{title:,tiddler_id:,bag_name:,is_deleted:}], -sorted in ascending order of tiddler_id. - -Options include: - -limit: optional maximum number of results to return -last_known_tiddler_id: tiddler_id of the last known update. Only returns tiddlers that have been created, modified or deleted since -include_deleted: boolean, defaults to false - -Returns null for recipes that do not exist -*/ -SqlTiddlerDatabase.prototype.getRecipeTiddlers = function(recipe_name,options) { - options = options || {}; - // Get the recipe ID - const rowsCheckRecipe = this.engine.runStatementGet(` - SELECT recipe_id FROM recipes WHERE recipes.recipe_name = $recipe_name - `,{ - $recipe_name: recipe_name - }); - if(!rowsCheckRecipe) { - return null; - } - const recipe_id = rowsCheckRecipe.recipe_id; - // Compose the query to get the tiddlers - const params = { - $recipe_id: recipe_id - } - if(options.limit) { - params.$limit = options.limit.toString(); - } - if(options.last_known_tiddler_id) { - params.$last_known_tiddler_id = options.last_known_tiddler_id; - } - const rows = this.engine.runStatementGetAll(` - SELECT title, tiddler_id, is_deleted, bag_name - FROM ( - SELECT t.title, t.tiddler_id, t.is_deleted, b.bag_name, MAX(rb.position) AS position - FROM bags AS b - INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id - INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id - WHERE rb.recipe_id = $recipe_id - ${options.include_deleted ? "" : "AND t.is_deleted = FALSE"} - ${options.last_known_tiddler_id ? "AND tiddler_id > $last_known_tiddler_id" : ""} - GROUP BY t.title - ORDER BY t.title, tiddler_id DESC - ${options.limit ? "LIMIT $limit" : ""} - ) - `,params); - return rows; -}; - -/* -Get the tiddler_id of the newest tiddler in a recipe. Returns null for recipes that do not exist -*/ -SqlTiddlerDatabase.prototype.getRecipeLastTiddlerId = function(recipe_name) { - const row = this.engine.runStatementGet(` - SELECT t.title, t.tiddler_id, b.bag_name, MAX(rb.position) AS position - FROM bags AS b - INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id - 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.tiddler_id DESC - LIMIT 1 - `,{ - $recipe_name: recipe_name - }); - if(row) { - return row.tiddler_id; - } else { - return null; - } -}; - -SqlTiddlerDatabase.prototype.deleteAllTiddlersInBag = function(bag_name) { - // Delete the fields - this.engine.runStatement(` - DELETE FROM fields - WHERE tiddler_id IN ( - SELECT tiddler_id - FROM tiddlers - WHERE bag_id = (SELECT bag_id FROM bags WHERE bag_name = $bag_name) - AND is_deleted = FALSE - ) - `,{ - $bag_name: bag_name - }); - // Mark the tiddlers as deleted - this.engine.runStatement(` - UPDATE tiddlers - SET is_deleted = TRUE - WHERE bag_id = (SELECT bag_id FROM bags WHERE bag_name = $bag_name) - AND is_deleted = FALSE - `,{ - $bag_name: bag_name - }); -}; - /* -Get the names of the bags in a recipe. Returns an empty array for recipes that do not exist -*/ -SqlTiddlerDatabase.prototype.getRecipeBags = function(recipe_name) { - const rows = this.engine.runStatementGetAll(` - SELECT bags.bag_name - FROM bags - JOIN ( - SELECT rb.bag_id, rb.position as position - 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 - ORDER BY position - `,{ - $recipe_name: recipe_name - }); - return rows.map(value => value.bag_name); -}; - -/* -Get the attachment value of a bag, if any exist -*/ -SqlTiddlerDatabase.prototype.getBagTiddlerAttachmentBlob = function(title,bag_name) { - const row = this.engine.runStatementGet(` - SELECT t.attachment_blob - FROM bags AS b - INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id - WHERE t.title = $title AND b.bag_name = $bag_name AND t.is_deleted = FALSE - `, { - $title: title, - $bag_name: bag_name - }); - return row ? row.attachment_blob : null; -}; - -/* -Get the attachment value of a recipe, if any exist +Create a tiddler store. Options include: + +databasePath - path to the database file (can be ":memory:" to get a temporary database) +engine - wasm | better */ -SqlTiddlerDatabase.prototype.getRecipeTiddlerAttachmentBlob = function(title,recipe_name) { - const row = this.engine.runStatementGet(` - SELECT t.attachment_blob - 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 AND t.is_deleted = FALSE - ORDER BY rb.position DESC - LIMIT 1 - `, { - $title: title, - $recipe_name: recipe_name - }); - return row ? row.attachment_blob : null; -}; - -// User CRUD operations -SqlTiddlerDatabase.prototype.createUser = function(username, email, password) { - const result = this.engine.runStatement(` - INSERT INTO users (username, email, password) - VALUES ($username, $email, $password) - `, { - $username: username, - $email: email, - $password: password - }); - return result.lastInsertRowid; -}; - -SqlTiddlerDatabase.prototype.getUser = function(userId) { - return this.engine.runStatementGet(` - SELECT * FROM users WHERE user_id = $userId - `, { - $userId: userId - }); -}; - -SqlTiddlerDatabase.prototype.getUserByUsername = function(username) { - return this.engine.runStatementGet(` - SELECT * FROM users WHERE username = $username - `, { - $username: username - }); -}; - -SqlTiddlerDatabase.prototype.getUserByEmail = function(email) { - return this.engine.runStatementGet(` - SELECT * FROM users WHERE email = $email - `, { - $email: email - }); -}; - -SqlTiddlerDatabase.prototype.listUsersByRoleId = function(roleId) { - return this.engine.runStatementGetAll(` - SELECT u.* - FROM users u - JOIN user_roles ur ON u.user_id = ur.user_id - WHERE ur.role_id = $roleId - ORDER BY u.username - `, { - $roleId: roleId - }); -}; - -SqlTiddlerDatabase.prototype.updateUser = function (userId, username, email, roleId) { - const existingUser = this.engine.runStatement(` - SELECT user_id FROM users - WHERE email = $email AND user_id != $userId -`, { - $email: email, - $userId: userId - }); - - if (existingUser.length > 0) { - return { - success: false, - message: "Email address already in use by another user." - }; - } - - try { - this.engine.transaction(() => { - // Update user information - this.engine.runStatement(` - UPDATE users - SET username = $username, email = $email - WHERE user_id = $userId - `, { - $userId: userId, - $username: username, - $email: email - }); - - if (roleId) { - // Remove all existing roles for the user - this.engine.runStatement(` - DELETE FROM user_roles - WHERE user_id = $userId - `, { - $userId: userId - }); - - // Add the new role - this.engine.runStatement(` - INSERT INTO user_roles (user_id, role_id) - VALUES ($userId, $roleId) - `, { - $userId: userId, - $roleId: roleId - }); - } - }); - - return { - success: true, - message: "User profile and role updated successfully." - }; - } catch (error) { - return { - success: false, - message: "Failed to update user profile: " + error.message - }; - } -}; - -SqlTiddlerDatabase.prototype.updateUserPassword = function (userId, newHash) { - try { - this.engine.runStatement(` - UPDATE users - SET password = $newHash - WHERE user_id = $userId - `, { - $userId: userId, - $newHash: newHash, - }); - - return { - success: true, - message: "Password updated successfully." - }; - } catch (error) { - return { - success: false, - message: "Failed to update password: " + error.message - }; - } -}; - -SqlTiddlerDatabase.prototype.deleteUser = function(userId) { - this.engine.runStatement(` - DELETE FROM users WHERE user_id = $userId - `, { - $userId: userId - }); -}; - -SqlTiddlerDatabase.prototype.listUsers = function() { - return this.engine.runStatementGetAll(` - SELECT * FROM users ORDER BY username - `); -}; - -SqlTiddlerDatabase.prototype.createOrUpdateUserSession = function(userId, sessionId) { - const currentTimestamp = new Date().toISOString(); - - // First, try to update an existing session - const updateResult = this.engine.runStatement(` - UPDATE sessions - SET session_id = $sessionId, last_accessed = $timestamp - WHERE user_id = $userId - `, { - $userId: userId, - $sessionId: sessionId, - $timestamp: currentTimestamp - }); - - // If no existing session was updated, create a new one - if (updateResult.changes === 0) { - this.engine.runStatement(` - INSERT INTO sessions (user_id, session_id, created_at, last_accessed) - VALUES ($userId, $sessionId, $timestamp, $timestamp) - `, { - $userId: userId, - $sessionId: sessionId, - $timestamp: currentTimestamp - }); - } - - return sessionId; -}; - -SqlTiddlerDatabase.prototype.createUserSession = function(userId, sessionId) { - const currentTimestamp = new Date().toISOString(); - this.engine.runStatement(` - INSERT INTO sessions (user_id, session_id, created_at, last_accessed) - VALUES ($userId, $sessionId, $timestamp, $timestamp) - `, { - $userId: userId, - $sessionId: sessionId, - $timestamp: currentTimestamp - }); - - return sessionId; -}; - -SqlTiddlerDatabase.prototype.findUserBySessionId = function(sessionId) { - // First, get the user_id from the sessions table - const sessionResult = this.engine.runStatementGet(` - SELECT user_id, last_accessed - FROM sessions - WHERE session_id = $sessionId - `, { - $sessionId: sessionId - }); - - if (!sessionResult) { - return null; // Session not found - } - - const lastAccessed = new Date(sessionResult.last_accessed); - const expirationTime = 24 * 60 * 60 * 1000; // 24 hours in milliseconds - if (new Date() - lastAccessed > expirationTime) { - // Session has expired - this.deleteSession(sessionId); - return null; - } - - // Update the last_accessed timestamp - const currentTimestamp = new Date().toISOString(); - this.engine.runStatement(` - UPDATE sessions - SET last_accessed = $timestamp - WHERE session_id = $sessionId - `, { - $sessionId: sessionId, - $timestamp: currentTimestamp - }); - - const userResult = this.engine.runStatementGet(` - SELECT * - FROM users - WHERE user_id = $userId - `, { - $userId: sessionResult.user_id - }); - - if (!userResult) { - return null; - } - - return userResult; -}; - -SqlTiddlerDatabase.prototype.deleteSession = function(sessionId) { - this.engine.runStatement(` - DELETE FROM sessions - WHERE session_id = $sessionId - `, { - $sessionId: sessionId - }); -}; - -SqlTiddlerDatabase.prototype.deleteUserSessions = function(userId) { - this.engine.runStatement(` - DELETE FROM sessions - WHERE user_id = $userId - `, { - $userId: userId - }); -}; - -// Set the user as an admin -SqlTiddlerDatabase.prototype.setUserAdmin = function(userId) { - var admin = this.getRoleByName("ADMIN"); - if(admin) { - this.addRoleToUser(userId, admin.role_id); - } -}; - -// Group CRUD operations -SqlTiddlerDatabase.prototype.createGroup = function(groupName, description) { - const result = this.engine.runStatement(` - INSERT INTO groups (group_name, description) - VALUES ($groupName, $description) - `, { - $groupName: groupName, - $description: description - }); - return result.lastInsertRowid; -}; - -SqlTiddlerDatabase.prototype.getGroup = function(groupId) { - return this.engine.runStatementGet(` - SELECT * FROM groups WHERE group_id = $groupId - `, { - $groupId: groupId - }); -}; - -SqlTiddlerDatabase.prototype.updateGroup = function(groupId, groupName, description) { - this.engine.runStatement(` - UPDATE groups - SET group_name = $groupName, description = $description - WHERE group_id = $groupId - `, { - $groupId: groupId, - $groupName: groupName, - $description: description - }); -}; - -SqlTiddlerDatabase.prototype.deleteGroup = function(groupId) { - this.engine.runStatement(` - DELETE FROM groups WHERE group_id = $groupId - `, { - $groupId: groupId - }); -}; - -SqlTiddlerDatabase.prototype.listGroups = function() { - return this.engine.runStatementGetAll(` - SELECT * FROM groups ORDER BY group_name - `); -}; - -// Role CRUD operations -SqlTiddlerDatabase.prototype.createRole = function(roleName, description) { - const result = this.engine.runStatement(` - INSERT OR IGNORE INTO roles (role_name, description) - VALUES ($roleName, $description) - `, { - $roleName: roleName, - $description: description - }); - return result.lastInsertRowid; -}; - -SqlTiddlerDatabase.prototype.getRole = function(roleId) { - return this.engine.runStatementGet(` - SELECT * FROM roles WHERE role_id = $roleId - `, { - $roleId: roleId - }); -}; - -SqlTiddlerDatabase.prototype.getRoleByName = function(roleName) { - return this.engine.runStatementGet(` - SELECT * FROM roles WHERE role_name = $roleName - `, { - $roleName: roleName - }); +class SqlTiddlerDatabase { + constructor(engine) { + this.engine = engine; + } + async listBags() { + return await this.engine.bags.findMany({ + select: { bag_name: true, bag_id: true, accesscontrol: true, description: true }, + orderBy: { bag_name: "asc" } + }); + // const rows = await this.engine.runStatementGetAll(` + // SELECT bag_name, bag_id, accesscontrol, description + // FROM bags + // ORDER BY bag_name + // `); + // return rows; + } + /* + Create or update a bag + Returns the bag_id of the bag + */ + async createBag(bag_name, description, accesscontrol = "") { + // ignore if the bag already exists + const bag = await this.engine.bags.upsert({ + create: { bag_name, description, accesscontrol }, + update: { description, accesscontrol }, + where: { bag_name }, + }); + return bag.bag_id; + } + /* + Returns array of {recipe_name:,recipe_id:,description:,bag_names: []} + */ + async listRecipes() { + const rows = await this.engine.recipes.findMany({ + select: { + recipe_name: true, + recipe_id: true, + description: true, + owner_id: true, + recipe_bags: { select: { bag: { select: { bag_name: true } } } } + } + }); + return rows.map(row => { + return { + recipe_name: row.recipe_name, + recipe_id: row.recipe_id, + description: row.description, + owner_id: row.owner_id, + bag_names: row.recipe_bags.map(value => value.bag.bag_name) + }; + }); + // const rows = await this.engine.runStatementGetAll(` + // SELECT r.recipe_name, r.recipe_id, r.description, r.owner_id, b.bag_name, rb.position + // FROM recipes AS r + // JOIN recipe_bags AS rb ON rb.recipe_id = r.recipe_id + // JOIN bags AS b ON rb.bag_id = b.bag_id + // ORDER BY r.recipe_name, rb.position + // `); + // const results = []; + // let currentRecipeName = null, currentRecipeIndex = -1; + // for (const row of rows) { + // if (row.recipe_name !== currentRecipeName) { + // currentRecipeName = row.recipe_name; + // currentRecipeIndex += 1; + // results.push({ + // recipe_name: row.recipe_name, + // recipe_id: row.recipe_id, + // description: row.description, + // owner_id: row.owner_id, + // /** @type {string[]} */ + // bag_names: [] + // }); + // } + // results[currentRecipeIndex].bag_names.push(row.bag_name); + // } + // return results; + } + /* + Create or update a recipe + Returns the recipe_id of the recipe + */ + async createRecipe(recipe_name, bag_names, description) { + // deleting this will also delete the recipe_bags records + await this.engine.recipes.deleteMany({ + where: { recipe_name } + }); + const updateRecipes = await this.engine.recipes.create({ + data: { + recipe_name, + description, + recipe_bags: { + create: bag_names.map((bag_name, position) => ({ + position, + bag: { connect: { bag_name } } + })) + } + } + }); + return updateRecipes.recipe_id; + // // Run the queries + // await this.engine.runStatement(` + // -- Delete existing recipe_bags entries for this recipe + // DELETE FROM recipe_bags WHERE recipe_id = (SELECT recipe_id FROM recipes WHERE recipe_name = $recipe_name) + // `, { + // $recipe_name: recipe_name + // }); + // const updateRecipes = await this.engine.runStatement(` + // -- Create the entry in the recipes table if required + // INSERT OR REPLACE INTO recipes (recipe_name, description) + // VALUES ($recipe_name, $description) + // `, { + // $recipe_name: recipe_name, + // $description: description + // }); + // await this.engine.runStatement(` + // INSERT INTO recipe_bags (recipe_id, bag_id, position) + // SELECT r.recipe_id, b.bag_id, j.key as position + // FROM recipes r + // JOIN bags b + // INNER JOIN json_each($bag_names) AS j ON j.value = b.bag_name + // WHERE r.recipe_name = $recipe_name + // `, { + // $recipe_name: recipe_name, + // $bag_names: JSON.stringify(bag_names) + // }); + // return updateRecipes.lastInsertRowid; + } + /* + Assign a recipe to a user + */ + async assignRecipeToUser(recipe_name, user_id) { + await this.engine.recipes.update({ + where: { recipe_name }, + data: { owner_id: user_id } + }); + // await this.engine.runStatement(` + // UPDATE recipes SET owner_id = $user_id WHERE recipe_name = $recipe_name + // `, { + // $recipe_name: recipe_name, + // $user_id: user_id + // }); + } + /* + Returns {tiddler_id:} + */ + async saveBagTiddler(tiddlerFields, bag_name, attachment_blob) { + // fields are deleted at the same time (cascade) + await this.engine.tiddlers.deleteMany({ + where: { bag: { bag_name }, title: tiddlerFields.title }, + }); + const tiddler = await this.engine.tiddlers.create({ + data: { + // this makes sure the bag exists + bag: { connect: { bag_name } }, + title: tiddlerFields.title, + is_deleted: false, + attachment_blob, + fields: { + create: Object.entries(tiddlerFields).map(([field_name, field_value]) => { + if (field_value === null) + field_value = ""; + if (field_value === undefined) + field_value = ""; + switch (typeof field_value) { + case "string": + break; + case "number": + case "boolean": + case "bigint": + field_value = field_value.toString(); + break; + default: + $tw.utils.error("Invalid field value type: " + typeof field_value); + } + return { field_name, field_value }; + }) + } + } + }); + return { tiddler_id: tiddler.tiddler_id }; + // attachment_blob = attachment_blob || null; + // // Update the tiddlers table + // var info = await this.engine.runStatement(` + // INSERT OR REPLACE INTO tiddlers (bag_id, title, is_deleted, attachment_blob) + // VALUES ( + // (SELECT bag_id FROM bags WHERE bag_name = $bag_name), + // $title, + // FALSE, + // $attachment_blob + // ) + // `, { + // $title: tiddlerFields.title, + // $attachment_blob: attachment_blob, + // $bag_name: bag_name + // }); + // // Update the fields table + // await this.engine.runStatement(` + // INSERT OR REPLACE INTO fields (tiddler_id, field_name, field_value) + // SELECT + // t.tiddler_id, + // 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: bag_name, + // $field_values: JSON.stringify(Object.assign({}, tiddlerFields, { title: undefined })) + // }); + // return { + // tiddler_id: info.lastInsertRowid + // }; + } + /* + Returns {tiddler_id:,bag_name:} or null if the recipe is empty + */ + async saveRecipeTiddler(tiddlerFields, recipe_name, attachment_blob) { + // Find the topmost bag in the recipe + const bag = await this.engine.recipe_bags.findFirst({ + where: { recipe: { recipe_name } }, + select: { bag: { select: { bag_name: true } } }, + orderBy: { position: "desc" } + }); + if (!bag) { + return null; + } + // Save the tiddler to the topmost bag + const info = await this.saveBagTiddler(tiddlerFields, bag.bag.bag_name, attachment_blob); + return { + tiddler_id: info.tiddler_id, + bag_name: bag.bag.bag_name + }; + // // Find the topmost bag in the recipe + // var row = await this.engine.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: recipe_name + // }); + // if (!row) { + // return null; + // } + // // Save the tiddler to the topmost bag + // var info = await this.saveBagTiddler(tiddlerFields, row.bag_name, attachment_blob); + // return { + // tiddler_id: info.tiddler_id, + // bag_name: row.bag_name + // }; + } + /* + Returns {tiddler_id:} of the delete marker + */ + async deleteTiddler(title, bag_name) { + // fields are deleted at the same time (cascade) + // use deleteMany so we can filter by bag_name directly + await this.engine.tiddlers.deleteMany({ + where: { bag: { bag_name }, title }, + }); + // create the delete marker + const rowDeleteMarker = await this.engine.tiddlers.create({ + data: { + is_deleted: true, + attachment_blob: null, + title, + bag: { connect: { bag_name } } + } + }); + return { tiddler_id: rowDeleteMarker.tiddler_id }; + // // Delete the fields of this tiddler + // await this.engine.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: bag_name + // }); + // // Mark the tiddler itself as deleted + // const rowDeleteMarker = await this.engine.runStatement(` + // INSERT OR REPLACE INTO tiddlers (bag_id, title, is_deleted, attachment_blob) + // VALUES ( + // (SELECT bag_id FROM bags WHERE bag_name = $bag_name), + // $title, + // TRUE, + // NULL + // ) + // `, { + // $title: title, + // $bag_name: bag_name + // }); + // return { tiddler_id: rowDeleteMarker.lastInsertRowid }; + } + /* + returns {tiddler_id:,tiddler:,attachment_blob:} + */ + async getBagTiddler(title, bag_name) { + const { bag_id } = await this.engine.bags.findUniqueOrThrow({ + where: { bag_name }, + select: { bag_id: true } + }); + const tiddler = await this.engine.tiddlers.findUnique({ + where: { bag_id_title: { bag_id, title } }, + include: { fields: true } + }); + if (!tiddler) { + return null; + } + return { + tiddler_id: tiddler.tiddler_id, + attachment_blob: tiddler.attachment_blob, + tiddler: tiddler.fields.reduce((accumulator, value) => { + accumulator[value.field_name] = value.field_value; + return accumulator; + }, { title }) + }; + // const rowTiddler = await this.engine.runStatementGet(` + // SELECT t.tiddler_id, t.attachment_blob + // FROM bags AS b + // INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id + // WHERE t.title = $title AND b.bag_name = $bag_name AND t.is_deleted = FALSE + // `, { + // $title: title, + // $bag_name: bag_name + // }); + // if (!rowTiddler) { + // return null; + // } + // const rows = await this.engine.runStatementGetAll(` + // SELECT field_name, field_value, tiddler_id + // FROM fields + // WHERE tiddler_id = $tiddler_id + // `, { + // $tiddler_id: rowTiddler.tiddler_id + // }); + // if (rows.length === 0) { + // return null; + // } else { + // return { + // bag_name: bag_name, + // tiddler_id: rows[0].tiddler_id, + // attachment_blob: rowTiddler.attachment_blob, + // tiddler: rows.reduce((accumulator, value) => { + // accumulator[value["field_name"]] = value.field_value; + // return accumulator; + // }, { title: title }) + // }; + // } + } + /* + Returns {bag_name:, tiddler: {fields}, tiddler_id:, attachment_blob:} + */ + async getRecipeTiddler(title, recipe_name) { + const row = await this.engine.recipe_bags.findFirst({ + // where: the first recipe_bag containing this tiddler, in descending order + where: { recipe: { recipe_name }, bag: { tiddlers: { some: { title } } } }, + orderBy: { position: "desc" }, + // select: the tiddler and its fields + select: { + bag: { + select: { + bag_name: true, + tiddlers: { + where: { title }, + include: { fields: true } + } + } + }, + }, + }); + if (!row) { + return null; + } + const tiddler = row.bag.tiddlers[0]; + return { + bag_name: row.bag.bag_name, + tiddler_id: tiddler.tiddler_id, + attachment_blob: tiddler.attachment_blob, + tiddler: tiddler.fields.reduce((accumulator, value) => { + accumulator[value.field_name] = value.field_value; + return accumulator; + }, { title }) + }; + // const rowTiddlerId = await this.engine.runStatementGet(` + // SELECT t.tiddler_id, t.attachment_blob, b.bag_name + // FROM bags AS b + // INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id + // 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 + // AND t.is_deleted = FALSE + // ORDER BY rb.position DESC + // LIMIT 1 + // `, { + // $title: title, + // $recipe_name: recipe_name + // }); + // if (!rowTiddlerId) { + // return null; + // } + // // Get the fields + // const rows = await this.engine.runStatementGetAll(` + // SELECT field_name, field_value + // FROM fields + // WHERE tiddler_id = $tiddler_id + // `, { + // $tiddler_id: rowTiddlerId.tiddler_id + // }); + // return { + // bag_name: rowTiddlerId.bag_name, + // tiddler_id: rowTiddlerId.tiddler_id, + // attachment_blob: rowTiddlerId.attachment_blob, + // tiddler: rows.reduce((accumulator, value) => { + // accumulator[value["field_name"]] = value.field_value; + // return accumulator; + // }, { title: title }) + // }; + } + /* + Checks if a user has permission to access a recipe + */ + async hasRecipePermission(user_id, recipe_name, permissionName) { + var _a; + const recipe = await this.engine.recipes.findUnique({ + where: { recipe_name }, select: { owner_id: true } + }); + return await this.checkACLPermission(user_id, "recipe", recipe_name, permissionName, (_a = recipe === null || recipe === void 0 ? void 0 : recipe.owner_id) !== null && _a !== void 0 ? _a : undefined); + // try { + // // check if the user is the owner of the entity + // const recipe = await this.engine.runStatementGet(` + // SELECT owner_id + // FROM recipes + // WHERE recipe_name = $recipe_name + // `, { + // $recipe_name: recipeName + // }); + // if (!!recipe?.owner_id && recipe?.owner_id === userId) { + // return true; + // } else { + // var permission = await this.checkACLPermission(userId, "recipe", recipeName, permissionName, recipe?.owner_id); + // return permission; + // } + // } catch (error) { + // console.error(error); + // return false; + // } + } + /* + Checks if a user has permission to access a bag + */ + async hasBagPermission(userId, bagName, permissionName) { + return await this.checkACLPermission(userId, "bag", bagName, permissionName, undefined); + } + /** + * @overload + * @param {string} entityType + * @param {string} entityName + * @param {false} [fetchAll] + * @returns {Promise>} + * + * @overload + * @param {string} entityType + * @param {string} entityName + * @param {true} fetchAll + * @returns {Promise[]>} + */ + async getACLByName(entityType, entityName, fetchAll) { + okEntityType(entityType); + okTypeTruthy(entityName, "string", "No entityName provided"); + // okType(fetchAll, "boolean", "fetchAll must be a boolean"); + return await this.engine.acl.findMany({ + where: { entity_type: entityType, entity_name: entityName }, + include: { permission: true }, + take: fetchAll ? undefined : 1, + }); + // const entityInfo = this.entityTypeToTableMap[entityType]; + // if (!entityInfo) { + // throw new Error("Invalid entity type: " + entityType); + // } + // // First, check if there's an ACL record for the entity and get the permission_id + // var checkACLExistsQuery = ` + // SELECT acl.*, permissions.permission_name + // FROM acl + // LEFT JOIN permissions ON acl.permission_id = permissions.permission_id + // WHERE acl.entity_type = $entity_type + // AND acl.entity_name = $entity_name + // `; + // if (!fetchAll) { + // checkACLExistsQuery += ' LIMIT 1'; + // } + // const aclRecord = await this.engine[fetchAll ? 'runStatementGetAll' : 'runStatementGet'](checkACLExistsQuery, { + // $entity_type: entityType, + // $entity_name: entityName + // }); + // return aclRecord; + } + async checkACLPermission(user_id, entityType, entityName, permissionName, ownerId) { + var _a; + if (user_id === undefined) + return false; + okType(user_id, "number", "No user_id provided"); + okEntityType(entityType); + okTypeTruthy(entityName, "string", "No entityName provided"); + okTypeTruthy(permissionName, "string", "No permissionName provided"); + (0, assert_1.ok)(typeof ownerId === "number" || ownerId === undefined); + // if the entityName starts with "$:/", we'll assume its a system bag/recipe, then grant the user permission + if (entityName.startsWith("$:/")) + return true; + const aclRecords = await this.getACLByName(entityType, entityName, true); + const aclRecord = aclRecords.find(record => { var _a; return ((_a = record.permission) === null || _a === void 0 ? void 0 : _a.permission_name) === permissionName; }); + // If no ACL record exists, return true for hasPermission + if (!aclRecord && !ownerId && aclRecords.length === 0 + || !!aclRecord && !!ownerId && ownerId === user_id) { + return true; + } + if (!((_a = aclRecord === null || aclRecord === void 0 ? void 0 : aclRecord.permission) === null || _a === void 0 ? void 0 : _a.permission_id)) + return false; + const result = await this.engine.users.findUnique({ + where: { + user_id, + user_roles: { + some: { + role: { + acls: { + some: { + entity_type: entityType, + entity_name: entityName, + permission_id: aclRecord.permission.permission_id + } + } + } + } + } + }, + select: { + user_id: true, + } + }); + return !!result; + // // If ACL record exists, check for user permission using the retrieved permission_id + // const checkPermissionQuery = ` + // SELECT * + // FROM users u + // JOIN user_roles ur ON u.user_id = ur.user_id + // JOIN roles r ON ur.role_id = r.role_id + // JOIN acl a ON r.role_id = a.role_id + // WHERE u.user_id = $user_id + // AND a.entity_type = $entity_type + // AND a.entity_name = $entity_name + // AND a.permission_id = $permission_id + // LIMIT 1 + // `; + // const result = await this.engine.runStatementGet(checkPermissionQuery, { + // $user_id: userId, + // $entity_type: entityType, + // $entity_name: entityName, + // $permission_id: aclRecord?.permission_id + // }); + } + /** + * Returns the ACL records for an entity (bag or recipe) + */ + async getEntityAclRecords(entity_name) { + return await this.engine.acl.findMany({ + where: { entity_name }, + }); + // const checkACLExistsQuery = ` + // SELECT * + // FROM acl + // WHERE entity_name = $entity_name + // `; + // const aclRecords = await this.engine.runStatementGetAll(checkACLExistsQuery, { + // $entity_name: entityName + // }); + // return aclRecords; + } + /* + Get the entity by name + */ + async getEntityByName(entityType, entityName) { + okEntityType(entityType); + okTypeTruthy(entityName, "string", "No entityName provided"); + // they have to be separated for typechecking, but the code is the same + switch (entityType) { + case "recipe": return await this.engine.recipes.findUnique({ + where: { recipe_name: entityName }, + }); + case "bag": return await this.engine.bags.findUnique({ + where: { bag_name: entityName }, + }); + } + // const entityTypeToTableMap = { + // bag: { + // table: "bags", + // column: "bag_name" + // }, + // recipe: { + // table: "recipes", + // column: "recipe_name" + // } + // } as const; + // const entityInfo = this.entityTypeToTableMap[entityType]; + // if (entityInfo) { + // return await this.engine.runStatementGet(`SELECT * FROM ${entityInfo.table} WHERE ${entityInfo.column} = $entity_name`, { + // $entity_name: entityName + // }); + // } + // return null; + } + /* + Get the titles of the tiddlers in a bag. Returns an empty array for bags that do not exist + */ + async getBagTiddlers(bag_name) { + const rows = await this.engine.tiddlers.findMany({ + where: { bag: { bag_name }, is_deleted: false }, + select: { title: true, tiddler_id: true }, + orderBy: { title: "asc" } + }); + return rows; + // const rows = await this.engine.runStatementGetAll(` + // SELECT DISTINCT title, tiddler_id + // FROM tiddlers + // WHERE bag_id IN ( + // SELECT bag_id + // FROM bags + // WHERE bag_name = $bag_name + // ) + // AND tiddlers.is_deleted = FALSE + // ORDER BY title ASC + // `, { + // $bag_name: bag_name + // }); + // return rows; + } + /* + Get the tiddler_id of the newest tiddler in a bag. Returns null for bags that do not exist + */ + async getBagLastTiddlerId(bag_name) { + var _a; + const row = await this.engine.tiddlers.findFirst({ + where: { bag: { bag_name } }, + select: { tiddler_id: true }, + orderBy: { tiddler_id: "desc" } + }); + return (_a = row === null || row === void 0 ? void 0 : row.tiddler_id) !== null && _a !== void 0 ? _a : null; + // const row = await this.engine.runStatementGet(` + // SELECT tiddler_id + // FROM tiddlers + // WHERE bag_id IN ( + // SELECT bag_id + // FROM bags + // WHERE bag_name = $bag_name + // ) + // ORDER BY tiddler_id DESC + // LIMIT 1 + // `, { + // $bag_name: bag_name + // }); + // if (row) { + // return row.tiddler_id; + // } else { + // return null; + // } + } + /** + Get the metadata of the tiddlers in a recipe as an array [{title:,tiddler_id:,bag_name:,is_deleted:}], + sorted in ascending order of tiddler_id. + + Options include: + + limit: optional maximum number of results to return + last_known_tiddler_id: tiddler_id of the last known update. Only returns tiddlers that have been created, modified or deleted since + include_deleted: boolean, defaults to false + + Returns null for recipes that do not exist + + @returns {} + */ + async getRecipeTiddlers(recipe_name, options = {}) { + const tiddlers = await this.engine.tiddlers.findMany({ + // all tiddlers in a bag that are in the recipe + where: { + bag: { recipe_bags: { some: { recipe: { recipe_name } } } }, + tiddler_id: options.last_known_tiddler_id ? { + gt: options.last_known_tiddler_id + } : undefined, + is_deleted: options.include_deleted ? undefined : false + }, + select: { + title: true, + tiddler_id: true, + is_deleted: true, + bag: { + select: { + bag_name: true, + recipe_bags: { + select: { position: true, recipe_id: true }, + where: { recipe: { recipe_name } } + } + } + } + } + }); + const tiddlerMap = new Map(); + for (const tiddler of tiddlers) { + if (!tiddler.bag.recipe_bags.length) + $tw.utils.warning(`Tiddler '${tiddler.title}' is not in the recipe '${recipe_name}'???`); + if (tiddler.bag.recipe_bags.length > 1) + $tw.utils.warning(`Tiddler bag '${tiddler.bag.bag_name}' is specified multiple times in the recipe '${recipe_name}'???`); + const { position } = tiddler.bag.recipe_bags.sort((a, b) => b.position - a.position)[0]; + const current = tiddlerMap.get(tiddler.title); + if (current) + if (current.position > position) + continue; + tiddlerMap.set(tiddler.title, { position, tiddler }); + } + return [...tiddlerMap.values()].map(({ tiddler }) => ({ + title: tiddler.title, + tiddler_id: tiddler.tiddler_id, + is_deleted: tiddler.is_deleted, + bag_name: tiddler.bag.bag_name, + })); + // // Get the recipe ID + // const rowsCheckRecipe = await this.engine.runStatementGet(` + // SELECT recipe_id FROM recipes WHERE recipes.recipe_name = $recipe_name + // `, { + // $recipe_name: recipe_name + // }); + // if (!rowsCheckRecipe) { + // return null; + // } + // const recipe_id = rowsCheckRecipe.recipe_id; + // // Compose the query to get the tiddlers + // const params = { + // $recipe_id: recipe_id + // }; + // if (options.limit) { + // params.$limit = options.limit.toString(); + // } + // if (options.last_known_tiddler_id) { + // params.$last_known_tiddler_id = options.last_known_tiddler_id; + // } + // /** * @type {any} */ + // const rows = await this.engine.runStatementGetAll(` + // SELECT title, tiddler_id, is_deleted, bag_name + // FROM ( + // SELECT t.title, t.tiddler_id, t.is_deleted, b.bag_name, MAX(rb.position) AS position + // FROM bags AS b + // INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id + // INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id + // WHERE rb.recipe_id = $recipe_id + // ${options.include_deleted ? "" : "AND t.is_deleted = FALSE"} + // ${options.last_known_tiddler_id ? "AND tiddler_id > $last_known_tiddler_id" : ""} + // GROUP BY t.title + // ORDER BY t.title, tiddler_id DESC + // ${options.limit ? "LIMIT $limit" : ""} + // ) + // `, params); + // return rows; + } + /* + Get the tiddler_id of the newest tiddler in a recipe. Returns null for recipes that do not exist + */ + async getRecipeLastTiddlerId(recipe_name) { + var _a; + // the old code was returning the latest tiddler_id out of every bag in the recipe, + // not just the latest visible tiddler_id, so we'll do the same for now + const row = await this.engine.tiddlers.findFirst({ + where: { bag: { recipe_bags: { some: { recipe: { recipe_name } } } } }, + orderBy: { tiddler_id: "desc" }, + select: { tiddler_id: true } + }); + return (_a = row === null || row === void 0 ? void 0 : row.tiddler_id) !== null && _a !== void 0 ? _a : null; + // const row = await this.engine.runStatementGet(` + // SELECT t.title, t.tiddler_id, b.bag_name, MAX(rb.position) AS position + // FROM bags AS b + // INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id + // 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.tiddler_id DESC + // LIMIT 1 + // `, { + // $recipe_name: recipe_name + // }); + // if (row) { + // return row.tiddler_id; + // } else { + // return null; + // } + } + async deleteAllTiddlersInBag(bag_name) { + const { bag_id } = await this.engine.bags.findUniqueOrThrow({ + where: { bag_name }, + select: { bag_id: true }, + }); + const tiddlers = await this.engine.tiddlers.findMany({ + where: { bag: { bag_name } }, + }); + await this.engine.tiddlers.deleteMany({ + where: { bag: { bag_name } }, + }); + // create the delete marker + await this.engine.tiddlers.createManyAndReturn({ + data: tiddlers.map(({ title }) => ({ + title, + bag_id, + is_deleted: true, + attachment_blob: null, + })), + select: { tiddler_id: true, title: true } + }); + // await this.engine.tiddlers.deleteMany({ + // where: { bag: { bag_name } } + // }); + // await this.engine.tiddlers.createMany({ + // data: [{ title: "$:/status/IsReadOnly", is_deleted: false, bag: { connect: { bag_name } } }] + // }) + // // Delete the fields + // await this.engine.runStatement(` + // DELETE FROM fields + // WHERE tiddler_id IN ( + // SELECT tiddler_id + // FROM tiddlers + // WHERE bag_id = (SELECT bag_id FROM bags WHERE bag_name = $bag_name) + // AND is_deleted = FALSE + // ) + // `, { + // $bag_name: bag_name + // }); + // // Mark the tiddlers as deleted + // await this.engine.runStatement(` + // UPDATE tiddlers + // SET is_deleted = TRUE + // WHERE bag_id = (SELECT bag_id FROM bags WHERE bag_name = $bag_name) + // AND is_deleted = FALSE + // `, { + // $bag_name: bag_name + // }); + } + /* + Get the names of the bags in a recipe. Returns an empty array for recipes that do not exist + */ + async getRecipeBags(recipe_name) { + const rows = await this.engine.recipe_bags.findMany({ + where: { recipe: { recipe_name } }, + select: { bag: { select: { bag_name: true } } }, + orderBy: { position: "asc" } + }); + return rows.map(e => e.bag.bag_name); + // const rows = await this.engine.runStatementGetAll(` + // SELECT bags.bag_name + // FROM bags + // JOIN ( + // SELECT rb.bag_id, rb.position as position + // 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 + // ORDER BY position + // `, { + // $recipe_name: recipe_name + // }); + // return rows.map(value => value.bag_name); + } + /* + Get the attachment value of a bag, if any exist + */ + async getBagTiddlerAttachmentBlob(title, bag_name) { + var _a; + const row = await this.engine.tiddlers.findFirst({ + where: { bag: { bag_name }, title }, + select: { attachment_blob: true } + }); + return (_a = row === null || row === void 0 ? void 0 : row.attachment_blob) !== null && _a !== void 0 ? _a : null; + // const row = await this.engine.runStatementGet(` + // SELECT t.attachment_blob + // FROM bags AS b + // INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id + // WHERE t.title = $title AND b.bag_name = $bag_name AND t.is_deleted = FALSE + // `, { + // $title: title, + // $bag_name: bag_name + // }); + // return row ? row.attachment_blob : null; + } + async getRecipeTiddlerBag({ title, recipe_name }) { + const bag = await this.engine.recipe_bags.findFirst({ + // where: the first recipe_bag containing this tiddler, in descending order + where: { recipe: { recipe_name }, bag: { tiddlers: { some: { title } } } }, + orderBy: { position: "desc" }, + // select: the bag_id + select: { bag_id: true }, + }); + return bag; + } + /* + Get the attachment value of a recipe, if any exist + */ + async getRecipeTiddlerAttachmentBlob(title, recipe_name) { + var _a; + const bag = await this.getRecipeTiddlerBag({ title, recipe_name }); + if (!bag) + return null; + const tiddler = await this.engine.tiddlers.findUnique({ + where: { bag_id_title: { bag_id: bag.bag_id, title } }, + select: { attachment_blob: true } + }); + return (_a = tiddler === null || tiddler === void 0 ? void 0 : tiddler.attachment_blob) !== null && _a !== void 0 ? _a : null; + // const row = await this.engine.runStatementGet(` + // SELECT t.attachment_blob + // FROM bags AS b + // INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id + // 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 AND t.is_deleted = FALSE + // ORDER BY rb.position DESC + // LIMIT 1 + // `, { + // $title: title, + // $recipe_name: recipe_name + // }); + // return row ? row.attachment_blob : null; + } + // User CRUD operations + async createUser(username, email, password) { + const result = await this.engine.users.create({ + data: { + username, + email, + password + }, + select: { + user_id: true + } + }); + return result.user_id; + // const result = await this.engine.runStatement(` + // INSERT INTO users (username, email, password) + // VALUES ($username, $email, $password) + // `, { + // $username: username, + // $email: email, + // $password: password + // }); + // return result.lastInsertRowid; + } + async getUser(user_id) { + okType(user_id, "number", "No userId provided"); + return await this.engine.users.findUnique({ + where: { user_id } + }); + // return await this.engine.runStatementGet(` + // SELECT * FROM users WHERE user_id = $userId + // `, { + // $userId: userId + // }); + } + async getUserByUsername(username) { + return await this.engine.users.findFirst({ + where: { username }, + }); + // return await this.engine.runStatementGet(` + // SELECT * FROM users WHERE username = $username + // `, { + // $username: username + // }); + } + async getUserByEmail(email) { + return await this.engine.users.findFirst({ + where: { email }, + }); + // return await this.engine.runStatementGet(` + // SELECT * FROM users WHERE email = $email + // `, { + // $email: email + // }); + } + async listUsersByRoleId(role_id) { + return await this.engine.users.findMany({ + where: { user_roles: { some: { role_id } } }, + orderBy: { username: "asc" } + }); + // return await this.engine.runStatementGetAll(` + // SELECT u.* + // FROM users u + // JOIN user_roles ur ON u.user_id = ur.user_id + // WHERE ur.role_id = $roleId + // ORDER BY u.username + // `, { + // $roleId: roleId + // }); + } + async updateUser(user_id, username, email, role_id) { + const existingUser = await this.engine.users.findFirst({ + where: { user_id: { not: user_id }, email }, + }); + if (existingUser) { + return { + success: false, + message: "Email address already in use by another user." + }; + } + await this.engine.users.update({ + where: { user_id }, + data: { username, email } + }); + if (typeof role_id === "number") { + await this.engine.user_roles.deleteMany({ + where: { user_id } + }); + await this.engine.user_roles.create({ + data: { user_id, role_id } + }); + } + return { + success: true, + message: "User profile and role updated successfully." + }; + // }); + // const existingUser = await this.engine.runStatementGet(` + // SELECT user_id FROM users + // WHERE email = $email AND user_id != $userId + // `, { + // $email: email, + // $userId: userId + // }); + // if (existingUser) { + // return { + // success: false, + // message: "Email address already in use by another user." + // }; + // } + // try { + // await this.engine.transaction(async () => { + // // Update user information + // await this.engine.runStatement(` + // UPDATE users + // SET username = $username, email = $email + // WHERE user_id = $userId + // `, { + // $userId: userId, + // $username: username, + // $email: email + // }); + // if (roleId) { + // // Remove all existing roles for the user + // await this.engine.runStatement(` + // DELETE FROM user_roles + // WHERE user_id = $userId + // `, { + // $userId: userId + // }); + // // Add the new role + // await this.engine.runStatement(` + // INSERT INTO user_roles (user_id, role_id) + // VALUES ($userId, $roleId) + // `, { + // $userId: userId, + // $roleId: roleId + // }); + // } + // }); + // return { + // success: true, + // message: "User profile and role updated successfully." + // }; + // } catch (error) { + // return { + // success: false, + // message: "Failed to update user profile: " + error.message + // }; + // } + } + async updateUserPassword(user_id, password) { + await this.engine.users.update({ + where: { user_id }, + data: { password } + }); + // try { + // await this.engine.runStatement(` + // UPDATE users + // SET password = $newHash + // WHERE user_id = $userId + // `, { + // $userId: userId, + // $newHash: newHash, + // }); + // return { + // success: true, + // message: "Password updated successfully." + // }; + // } catch (error) { + // return { + // success: false, + // message: "Failed to update password: " + error.message + // }; + // } + } + async deleteUser(user_id) { + await this.engine.users.delete({ + where: { user_id } + }); + // await this.engine.runStatement(` + // DELETE FROM users WHERE user_id = $userId + // `, { + // $userId: userId + // }); + } + async listUsers() { + return await this.engine.users.findMany({ + orderBy: { username: "asc" } + }); + // return await this.engine.runStatementGetAll(` + // SELECT * FROM users ORDER BY username + // `); + } + async createOrUpdateUserSession(user_id, session_id) { + const currentTimestamp = new Date().toISOString(); + // First, try to update an existing session + const updateResult = await this.engine.sessions.updateMany({ + where: { user_id }, + data: { + session_id, + last_accessed: currentTimestamp + }, + }); + // If no existing session was updated, create a new one + if (updateResult.count === 0) { + await this.engine.sessions.create({ + data: { + user_id, + session_id, + created_at: currentTimestamp, + last_accessed: currentTimestamp + } + }); + } + return session_id; + // await this.engine.sessions.upsert({ + // create: { + // user_id, + // session_id, + // created_at: currentTimestamp, + // last_accessed: currentTimestamp + // }, + // update: { + // session_id, + // last_accessed: currentTimestamp + // }, + // where: { user_id }, + // }); + // const currentTimestamp = new Date().toISOString(); + // // First, try to update an existing session + // const updateResult = await this.engine.runStatement(` + // UPDATE sessions + // SET session_id = $sessionId, last_accessed = $timestamp + // WHERE user_id = $userId + // `, { + // $userId: userId, + // $sessionId: sessionId, + // $timestamp: currentTimestamp + // }); + // // If no existing session was updated, create a new one + // if (updateResult.changes === 0) { + // await this.engine.runStatement(` + // INSERT INTO sessions (user_id, session_id, created_at, last_accessed) + // VALUES ($userId, $sessionId, $timestamp, $timestamp) + // `, { + // $userId: userId, + // $sessionId: sessionId, + // $timestamp: currentTimestamp + // }); + // } + // return sessionId; + } + async createUserSession(user_id, session_id) { + const currentTimestamp = new Date().toISOString(); + // throw new Error("how are we even here?"); + await this.engine.sessions.create({ + data: { + user_id, + session_id, + created_at: currentTimestamp, + last_accessed: currentTimestamp + } + }); + // await this.engine.runStatement(` + // INSERT INTO sessions (user_id, session_id, created_at, last_accessed) + // VALUES ($userId, $sessionId, $timestamp, $timestamp) + // `, { + // $userId: userId, + // $sessionId: sessionId, + // $timestamp: currentTimestamp + // }); + return session_id; + } + /** + * @typedef {Object} User + * @property {number} user_id + * @property {string} username + * @property {string} email + * @property {string} [password] + * @property {string} created_at + * @property {string} last_login + */ + async findUserBySessionId(session_id) { + const session = await this.engine.sessions.findUnique({ + where: { session_id }, + select: { last_accessed: true, user: true } + }); + if (!session) + return null; + const lastAccessed = new Date(session.last_accessed); + const expirationTime = 24 * 60 * 60 * 1000; // 24 hours in milliseconds + if ((+new Date() - +lastAccessed) > expirationTime) { + await this.deleteSession(session_id); + return null; + } + // Update the last_accessed timestamp + const currentTimestamp = new Date().toISOString(); + await this.engine.sessions.update({ + where: { session_id }, + data: { last_accessed: currentTimestamp } + }); + return session.user; + // // First, get the user_id from the sessions table + // const sessionResult = await this.engine.runStatementGet(` + // SELECT user_id, last_accessed + // FROM sessions + // WHERE session_id = $sessionId + // `, { + // $sessionId: sessionId + // }); + // if (!sessionResult) { + // return null; // Session not found + // } + // const lastAccessed = new Date(sessionResult.last_accessed); + // const expirationTime = 24 * 60 * 60 * 1000; // 24 hours in milliseconds + // if (new Date() - lastAccessed > expirationTime) { + // // Session has expired + // await this.deleteSession(sessionId); + // return null; + // } + // // Update the last_accessed timestamp + // const currentTimestamp = new Date().toISOString(); + // await this.engine.runStatement(` + // UPDATE sessions + // SET last_accessed = $timestamp + // WHERE session_id = $sessionId + // `, { + // $sessionId: sessionId, + // $timestamp: currentTimestamp + // }); + // /** @type {any} */ + // const userResult = await this.engine.runStatementGet(` + // SELECT * + // FROM users + // WHERE user_id = $userId + // `, { + // $userId: sessionResult.user_id + // }); + // if (!userResult) { + // return null; + // } + // return userResult; + } + async deleteSession(session_id) { + await this.engine.sessions.delete({ + where: { session_id } + }); + // await this.engine.runStatement(` + // DELETE FROM sessions + // WHERE session_id = $sessionId + // `, { + // $sessionId: sessionId + // }); + } + async deleteUserSessions(user_id) { + await this.engine.sessions.deleteMany({ + where: { user_id } + }); + // await this.engine.runStatement(` + // DELETE FROM sessions + // WHERE user_id = $userId + // `, { + // $userId: userId + // }); + } + // Set the user as an admin + async setUserAdmin(user_id) { + const admin = await this.getRoleByName("ADMIN"); + if (admin) { + await this.addRoleToUser(user_id, admin.role_id); + } + // var admin = await this.getRoleByName("ADMIN"); + // if (admin) { + // await this.addRoleToUser(userId, admin.role_id); + // } + } + // Group CRUD operations + async createGroup(group_name, description) { + const result = await this.engine.groups.create({ + data: { + group_name, + description + }, + select: { group_id: true } + }); + return result.group_id; + // const result = await this.engine.runStatement(` + // INSERT INTO groups (group_name, description) + // VALUES ($groupName, $description) + // `, { + // $groupName: groupName, + // $description: description + // }); + // return result.lastInsertRowid; + } + async getGroup(groupId) { + return await this.engine.groups.findUnique({ + where: { group_id: groupId } + }); + // return await this.engine.runStatementGet(` + // SELECT * FROM groups WHERE group_id = $groupId + // `, { + // $groupId: groupId + // }); + } + async updateGroup(groupId, groupName, description) { + await this.engine.groups.update({ + where: { group_id: groupId }, + data: { group_name: groupName, description } + }); + // await this.engine.runStatement(` + // UPDATE groups + // SET group_name = $groupName, description = $description + // WHERE group_id = $groupId + // `, { + // $groupId: groupId, + // $groupName: groupName, + // $description: description + // }); + } + async deleteGroup(groupId) { + return await this.engine.groups.delete({ + where: { group_id: groupId } + }); + // await this.engine.runStatement(` + // DELETE FROM groups WHERE group_id = $groupId + // `, { + // $groupId: groupId + // }); + } + async listGroups() { + return await this.engine.groups.findMany({ + orderBy: { group_name: "asc" } + }); + // return await this.engine.runStatementGetAll(` + // SELECT * FROM groups ORDER BY group_name + // `); + } + // Role CRUD operations + async createRole(roleName, description) { + const result = await this.engine.roles.create({ + data: { + role_name: roleName, + description + }, + select: { role_id: true } + }); + // const result = await this.engine.runStatement(` + // INSERT OR IGNORE INTO roles (role_name, description) + // VALUES ($roleName, $description) + // `, { + // $roleName: roleName, + // $description: description + // }); + // return result.lastInsertRowid; + } + async getRole(roleId) { + return await this.engine.roles.findUnique({ + where: { role_id: roleId } + }); + // return await this.engine.runStatementGet(` + // SELECT * FROM roles WHERE role_id = $roleId + // `, { + // $roleId: roleId + // }); + } + async getRoleByName(roleName) { + return await this.engine.roles.findFirst({ + where: { role_name: roleName } + }); + // return await this.engine.runStatementGet(` + // SELECT * FROM roles WHERE role_name = $roleName + // `, { + // $roleName: roleName + // }); + } + async updateRole(roleId, roleName, description) { + await this.engine.roles.update({ + where: { role_id: roleId }, + data: { role_name: roleName, description } + }); + // await this.engine.runStatement(` + // UPDATE roles + // SET role_name = $roleName, description = $description + // WHERE role_id = $roleId + // `, { + // $roleId: roleId, + // $roleName: roleName, + // $description: description + // }); + } + async deleteRole(roleId) { + await this.engine.roles.delete({ + where: { role_id: roleId } + }); + // await this.engine.runStatement(` + // DELETE FROM roles WHERE role_id = $roleId + // `, { + // $roleId: roleId + // }); + } + async listRoles() { + return await this.engine.roles.findMany({ + orderBy: { role_name: "asc" } + }); + // return await this.engine.runStatementGetAll(` + // SELECT * FROM roles ORDER BY role_name DESC + // `); + } + // Permission CRUD operations + async createPermission(permissionName, description) { + const result = await this.engine.permissions.create({ + data: { + permission_name: permissionName, + description + }, + select: { permission_id: true } + }); + return result.permission_id; + // const result = await this.engine.runStatement(` + // INSERT OR IGNORE INTO permissions (permission_name, description) + // VALUES ($permissionName, $description) + // `, { + // $permissionName: permissionName, + // $description: description + // }); + // return result.lastInsertRowid; + } + async getPermission(permissionId) { + return await this.engine.permissions.findUnique({ + where: { permission_id: permissionId } + }); + // return await this.engine.runStatementGet(` + // SELECT * FROM permissions WHERE permission_id = $permissionId + // `, { + // $permissionId: permissionId + // }); + } + async getPermissionByName(permissionName) { + return await this.engine.permissions.findFirst({ + where: { permission_name: permissionName } + }); + // return await this.engine.runStatementGet(` + // SELECT * FROM permissions WHERE permission_name = $permissionName + // `, { + // $permissionName: permissionName + // }); + } + async updatePermission(permissionId, permissionName, description) { + await this.engine.permissions.update({ + where: { permission_id: permissionId }, + data: { permission_name: permissionName, description } + }); + // await this.engine.runStatement(` + // UPDATE permissions + // SET permission_name = $permissionName, description = $description + // WHERE permission_id = $permissionId + // `, { + // $permissionId: permissionId, + // $permissionName: permissionName, + // $description: description + // }); + } + async deletePermission(permissionId) { + await this.engine.permissions.delete({ + where: { permission_id: permissionId } + }); + // await this.engine.runStatement(` + // DELETE FROM permissions WHERE permission_id = $permissionId + // `, { + // $permissionId: permissionId + // }); + } + async listPermissions() { + return await this.engine.permissions.findMany({ + orderBy: { permission_name: "asc" } + }); + // return await this.engine.runStatementGetAll(` + // SELECT * FROM permissions ORDER BY permission_name + // `); + } + // ACL CRUD operations + async createACL(entityName, entityType, roleId, permissionId) { + if (entityName.startsWith("$:/")) + return; + // No idea why this was insert or replace into because the only thing unique is the acl_id, + // which is not include in this call, so it would never replace. + await this.engine.acl.create({ + data: { + entity_name: entityName, + entity_type: entityType, + role_id: roleId, + permission_id: permissionId + } + }); + // if (!entityName.startsWith("$:/")) { + // const result = await this.engine.runStatement(` + // INSERT OR IGNORE INTO acl (entity_name, entity_type, role_id, permission_id) + // VALUES ($entityName, $entityType, $roleId, $permissionId) + // `, + // { + // $entityName: entityName, + // $entityType: entityType, + // $roleId: roleId, + // $permissionId: permissionId + // }); + // return result.lastInsertRowid; + // } + } + async getACL(aclId) { + return await this.engine.acl.findUnique({ + where: { acl_id: aclId } + }); + // return await this.engine.runStatementGet(` + // SELECT * FROM acl WHERE acl_id = $aclId + // `, { + // $aclId: aclId + // }); + } + async updateACL(aclId, entityId, entityType, roleId, permissionId) { + await this.engine.acl.update({ + where: { acl_id: aclId }, + data: { + entity_name: entityId, + entity_type: entityType, + role_id: roleId, + permission_id: permissionId + } + }); + // await this.engine.runStatement(` + // UPDATE acl + // SET entity_name = $entityId, entity_type = $entityType, + // role_id = $roleId, permission_id = $permissionId + // WHERE acl_id = $aclId + // `, { + // $aclId: aclId, + // $entityId: entityId, + // $entityType: entityType, + // $roleId: roleId, + // $permissionId: permissionId + // }); + } + async deleteACL(aclId) { + await this.engine.acl.delete({ + where: { acl_id: aclId } + }); + // await this.engine.runStatement(` + // DELETE FROM acl WHERE acl_id = $aclId + // `, { + // $aclId: aclId + // }); + } + async listACLs() { + return await this.engine.acl.findMany({ + orderBy: [{ entity_type: "asc" }, { entity_name: "asc" }] + }); + // return await this.engine.runStatementGetAll(` + // SELECT * FROM acl ORDER BY entity_type, entity_name + // `); + } + // Association management functions + async addUserToGroup(userId, groupId) { + await this.engine.user_groups.create({ + data: { + user_id: userId, + group_id: groupId + }, + }); + // await this.engine.runStatement(` + // INSERT OR IGNORE INTO user_groups (user_id, group_id) + // VALUES ($userId, $groupId) + // `, { + // $userId: userId, + // $groupId: groupId + // }); + } + async isUserInGroup(userId, groupId) { + const result = await this.engine.user_groups.findFirst({ + where: { user_id: userId, group_id: groupId } + }); + return !!result; + // const result = await this.engine.runStatementGet(` + // SELECT 1 FROM user_groups + // WHERE user_id = $userId AND group_id = $groupId + // `, { + // $userId: userId, + // $groupId: groupId + // }); + // return result !== undefined; + } + async removeUserFromGroup(userId, groupId) { + await this.engine.user_groups.delete({ + where: { user_id_group_id: { user_id: userId, group_id: groupId } } + }); + // await this.engine.runStatement(` + // DELETE FROM user_groups + // WHERE user_id = $userId AND group_id = $groupId + // `, { + // $userId: userId, + // $groupId: groupId + // }); + } + async addRoleToUser(userId, roleId) { + await this.engine.user_roles.create({ + data: { + user_id: userId, + role_id: roleId + } + }); + // await this.engine.runStatement(` + // INSERT OR IGNORE INTO user_roles (user_id, role_id) + // VALUES ($userId, $roleId) + // `, { + // $userId: userId, + // $roleId: roleId + // }); + } + async removeRoleFromUser(userId, roleId) { + await this.engine.user_roles.delete({ + where: { user_id_role_id: { user_id: userId, role_id: roleId } } + }); + // await this.engine.runStatement(` + // DELETE FROM user_roles + // WHERE user_id = $userId AND role_id = $roleId + // `, { + // $userId: userId, + // $roleId: roleId + // }); + } + async addRoleToGroup(groupId, roleId) { + await this.engine.group_roles.create({ + data: { + group_id: groupId, + role_id: roleId + } + }); + // await this.engine.runStatement(` + // INSERT OR IGNORE INTO group_roles (group_id, role_id) + // VALUES ($groupId, $roleId) + // `, { + // $groupId: groupId, + // $roleId: roleId + // }); + } + async removeRoleFromGroup(groupId, roleId) { + await this.engine.group_roles.delete({ + where: { group_id_role_id: { group_id: groupId, role_id: roleId } } + }); + // await this.engine.runStatement(` + // DELETE FROM group_roles + // WHERE group_id = $groupId AND role_id = $roleId + // `, { + // $groupId: groupId, + // $roleId: roleId + // }); + } + async addPermissionToRole(roleId, permissionId) { + await this.engine.role_permissions.create({ + data: { + role_id: roleId, + permission_id: permissionId + } + }); + // await this.engine.runStatement(` + // INSERT OR IGNORE INTO role_permissions (role_id, permission_id) + // VALUES ($roleId, $permissionId) + // `, { + // $roleId: roleId, + // $permissionId: permissionId + // }); + } + async removePermissionFromRole(roleId, permissionId) { + await this.engine.role_permissions.delete({ + where: { role_id_permission_id: { role_id: roleId, permission_id: permissionId } } + }); + // await this.engine.runStatement(` + // DELETE FROM role_permissions + // WHERE role_id = $roleId AND permission_id = $permissionId + // `, { + // $roleId: roleId, + // $permissionId: permissionId + // }); + } + async getUserRoles(userId) { + return await this.engine.user_roles.findFirst({ + where: { user_id: userId }, + select: { role: { select: { role_name: true } } } + }).then(e => e && ({ role_name: e.role.role_name })); + // const query = ` + // SELECT r.role_id, r.role_name + // FROM user_roles ur + // JOIN roles r ON ur.role_id = r.role_id + // WHERE ur.user_id = $userId + // LIMIT 1 + // `; + // return await this.engine.runStatementGet(query, { $userId: userId }); + } + async deleteUserRolesByRoleId(roleId) { + await this.engine.user_roles.deleteMany({ + where: { role_id: roleId } + }); + // await this.engine.runStatement(` + // DELETE FROM user_roles + // WHERE role_id = $roleId + // `, { + // $roleId: roleId + // }); + } + async deleteUserRolesByUserId(userId) { + await this.engine.user_roles.deleteMany({ + where: { user_id: userId } + }); + // await this.engine.runStatement(` + // DELETE FROM user_roles + // WHERE user_id = $userId + // `, { + // $userId: userId + // }); + } + async isRoleInUse(roleId) { + // Check if the role is assigned to any users + // const userRoleCheck = await this.engine.runStatementGet(` + // SELECT 1 + // FROM user_roles + // WHERE role_id = $roleId + // LIMIT 1 + // `, { + // $roleId: roleId + // }); + const userRoleCheck = await this.engine.user_roles.findFirst({ + where: { role_id: roleId } + }); + if (userRoleCheck) { + return true; + } + // Check if the role is used in any ACLs + // const aclRoleCheck = await this.engine.runStatementGet(` + // SELECT 1 + // FROM acl + // WHERE role_id = $roleId + // LIMIT 1 + // `, { + // $roleId: roleId + // }); + const aclRoleCheck = await this.engine.acl.findFirst({ + where: { role_id: roleId } + }); + if (aclRoleCheck) { + return true; + } + // If we've reached this point, the role is not in use + return false; + } + async getRoleById(roleId) { + return await this.engine.roles.findUnique({ + where: { role_id: roleId } + }); + // const role = await this.engine.runStatementGet(` + // SELECT role_id, role_name, description + // FROM roles + // WHERE role_id = $roleId + // `, { + // $roleId: roleId + // }); + // return role; + } } - -SqlTiddlerDatabase.prototype.updateRole = function(roleId, roleName, description) { - this.engine.runStatement(` - UPDATE roles - SET role_name = $roleName, description = $description - WHERE role_id = $roleId - `, { - $roleId: roleId, - $roleName: roleName, - $description: description - }); -}; - -SqlTiddlerDatabase.prototype.deleteRole = function(roleId) { - this.engine.runStatement(` - DELETE FROM roles WHERE role_id = $roleId - `, { - $roleId: roleId - }); -}; - -SqlTiddlerDatabase.prototype.listRoles = function() { - return this.engine.runStatementGetAll(` - SELECT * FROM roles ORDER BY role_name DESC - `); -}; - -// Permission CRUD operations -SqlTiddlerDatabase.prototype.createPermission = function(permissionName, description) { - const result = this.engine.runStatement(` - INSERT OR IGNORE INTO permissions (permission_name, description) - VALUES ($permissionName, $description) - `, { - $permissionName: permissionName, - $description: description - }); - return result.lastInsertRowid; -}; - -SqlTiddlerDatabase.prototype.getPermission = function(permissionId) { - return this.engine.runStatementGet(` - SELECT * FROM permissions WHERE permission_id = $permissionId - `, { - $permissionId: permissionId - }); -}; - -SqlTiddlerDatabase.prototype.getPermissionByName = function(permissionName) { - return this.engine.runStatementGet(` - SELECT * FROM permissions WHERE permission_name = $permissionName - `, { - $permissionName: permissionName - }); -}; - -SqlTiddlerDatabase.prototype.updatePermission = function(permissionId, permissionName, description) { - this.engine.runStatement(` - UPDATE permissions - SET permission_name = $permissionName, description = $description - WHERE permission_id = $permissionId - `, { - $permissionId: permissionId, - $permissionName: permissionName, - $description: description - }); -}; - -SqlTiddlerDatabase.prototype.deletePermission = function(permissionId) { - this.engine.runStatement(` - DELETE FROM permissions WHERE permission_id = $permissionId - `, { - $permissionId: permissionId - }); -}; - -SqlTiddlerDatabase.prototype.listPermissions = function() { - return this.engine.runStatementGetAll(` - SELECT * FROM permissions ORDER BY permission_name - `); -}; - -// ACL CRUD operations -SqlTiddlerDatabase.prototype.createACL = function(entityName, entityType, roleId, permissionId) { - if(!entityName.startsWith("$:/")) { - const result = this.engine.runStatement(` - INSERT OR IGNORE INTO acl (entity_name, entity_type, role_id, permission_id) - VALUES ($entityName, $entityType, $roleId, $permissionId) - `, - { - $entityName: entityName, - $entityType: entityType, - $roleId: roleId, - $permissionId: permissionId - }); - return result.lastInsertRowid; - } -}; - -SqlTiddlerDatabase.prototype.getACL = function(aclId) { - return this.engine.runStatementGet(` - SELECT * FROM acl WHERE acl_id = $aclId - `, { - $aclId: aclId - }); -}; - -SqlTiddlerDatabase.prototype.updateACL = function(aclId, entityId, entityType, roleId, permissionId) { - this.engine.runStatement(` - UPDATE acl - SET entity_name = $entityId, entity_type = $entityType, - role_id = $roleId, permission_id = $permissionId - WHERE acl_id = $aclId - `, { - $aclId: aclId, - $entityId: entityId, - $entityType: entityType, - $roleId: roleId, - $permissionId: permissionId - }); -}; - -SqlTiddlerDatabase.prototype.deleteACL = function(aclId) { - this.engine.runStatement(` - DELETE FROM acl WHERE acl_id = $aclId - `, { - $aclId: aclId - }); -}; - -SqlTiddlerDatabase.prototype.listACLs = function() { - return this.engine.runStatementGetAll(` - SELECT * FROM acl ORDER BY entity_type, entity_name - `); -}; - -// Association management functions -SqlTiddlerDatabase.prototype.addUserToGroup = function(userId, groupId) { - this.engine.runStatement(` - INSERT OR IGNORE INTO user_groups (user_id, group_id) - VALUES ($userId, $groupId) - `, { - $userId: userId, - $groupId: groupId - }); -}; - -SqlTiddlerDatabase.prototype.isUserInGroup = function(userId, groupId) { - const result = this.engine.runStatementGet(` - SELECT 1 FROM user_groups - WHERE user_id = $userId AND group_id = $groupId - `, { - $userId: userId, - $groupId: groupId - }); - return result !== undefined; -}; - -SqlTiddlerDatabase.prototype.removeUserFromGroup = function(userId, groupId) { - this.engine.runStatement(` - DELETE FROM user_groups - WHERE user_id = $userId AND group_id = $groupId - `, { - $userId: userId, - $groupId: groupId - }); -}; - -SqlTiddlerDatabase.prototype.addRoleToUser = function(userId, roleId) { - this.engine.runStatement(` - INSERT OR IGNORE INTO user_roles (user_id, role_id) - VALUES ($userId, $roleId) - `, { - $userId: userId, - $roleId: roleId - }); -}; - -SqlTiddlerDatabase.prototype.removeRoleFromUser = function(userId, roleId) { - this.engine.runStatement(` - DELETE FROM user_roles - WHERE user_id = $userId AND role_id = $roleId - `, { - $userId: userId, - $roleId: roleId - }); -}; - -SqlTiddlerDatabase.prototype.addRoleToGroup = function(groupId, roleId) { - this.engine.runStatement(` - INSERT OR IGNORE INTO group_roles (group_id, role_id) - VALUES ($groupId, $roleId) - `, { - $groupId: groupId, - $roleId: roleId - }); -}; - -SqlTiddlerDatabase.prototype.removeRoleFromGroup = function(groupId, roleId) { - this.engine.runStatement(` - DELETE FROM group_roles - WHERE group_id = $groupId AND role_id = $roleId - `, { - $groupId: groupId, - $roleId: roleId - }); -}; - -SqlTiddlerDatabase.prototype.addPermissionToRole = function(roleId, permissionId) { - this.engine.runStatement(` - INSERT OR IGNORE INTO role_permissions (role_id, permission_id) - VALUES ($roleId, $permissionId) - `, { - $roleId: roleId, - $permissionId: permissionId - }); -}; - -SqlTiddlerDatabase.prototype.removePermissionFromRole = function(roleId, permissionId) { - this.engine.runStatement(` - DELETE FROM role_permissions - WHERE role_id = $roleId AND permission_id = $permissionId - `, { - $roleId: roleId, - $permissionId: permissionId - }); -}; - -SqlTiddlerDatabase.prototype.getUserRoles = function(userId) { - const query = ` - SELECT r.role_id, r.role_name - FROM user_roles ur - JOIN roles r ON ur.role_id = r.role_id - WHERE ur.user_id = $userId - LIMIT 1 - `; - - return this.engine.runStatementGet(query, { $userId: userId }); -}; - -SqlTiddlerDatabase.prototype.deleteUserRolesByRoleId = function(roleId) { - this.engine.runStatement(` - DELETE FROM user_roles - WHERE role_id = $roleId - `, { - $roleId: roleId - }); -}; - -SqlTiddlerDatabase.prototype.deleteUserRolesByUserId = function(userId) { - this.engine.runStatement(` - DELETE FROM user_roles - WHERE user_id = $userId - `, { - $userId: userId - }); -}; - -SqlTiddlerDatabase.prototype.isRoleInUse = function(roleId) { - // Check if the role is assigned to any users - const userRoleCheck = this.engine.runStatementGet(` - SELECT 1 - FROM user_roles - WHERE role_id = $roleId - LIMIT 1 - `, { - $roleId: roleId - }); - - if(userRoleCheck) { - return true; - } - - // Check if the role is used in any ACLs - const aclRoleCheck = this.engine.runStatementGet(` - SELECT 1 - FROM acl - WHERE role_id = $roleId - LIMIT 1 - `, { - $roleId: roleId - }); - - if(aclRoleCheck) { - return true; - } - - // If we've reached this point, the role is not in use - return false; -}; - -SqlTiddlerDatabase.prototype.getRoleById = function(roleId) { - const role = this.engine.runStatementGet(` - SELECT role_id, role_name, description - FROM roles - WHERE role_id = $roleId - `, { - $roleId: roleId - }); - - return role; -}; - exports.SqlTiddlerDatabase = SqlTiddlerDatabase; - -})(); +//# sourceMappingURL=sql-tiddler-database.js.map \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js.map b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js.map new file mode 100644 index 00000000000..effeffef0e0 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js.map @@ -0,0 +1 @@ +{"version":3,"file":"sql-tiddler-database.js","sourceRoot":"","sources":["../../src/store/sql-tiddler-database.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js index bb32eba18d3..397f9e1d8b6 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js @@ -12,427 +12,395 @@ This class is largely a wrapper for the sql-tiddler-database.js class, adding th * Handling large tiddlers as attachments \*/ - -(function() { - +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.SqlTiddlerStore = void 0; +const sql_tiddler_database_1 = require("./sql-tiddler-database"); +const path = require("path"); +class Eventer { + constructor(skipDuplicate) { + this.skipDuplicate = skipDuplicate; + this.eventListeners = {}; + this.eventOutstanding = {}; + } + addEventListener(type, listener) { + this.eventListeners[type] = this.eventListeners[type] || []; + this.eventListeners[type].push(listener); + } + removeEventListener(type, listener) { + const listeners = this.eventListeners[type]; + if (listeners) { + var p = listeners.indexOf(listener); + if (p !== -1) { + listeners.splice(p, 1); + } + } + } + dispatchEvent(type, ...args) { + const self = this; + if (!this.eventOutstanding[type] || !this.skipDuplicate) { + $tw.utils.nextTick(function () { + self.eventOutstanding[type] = false; + const listeners = self.eventListeners[type]; + if (listeners) { + for (var p = 0; p < listeners.length; p++) { + var listener = listeners[p]; + listener.apply(listener, args); + } + } + }); + this.eventOutstanding[type] = true; + } + } +} /* Create a tiddler store. Options include: - + databasePath - path to the database file (can be ":memory:" to get a temporary database) adminWiki - reference to $tw.Wiki object used for configuration attachmentStore - reference to associated attachment store engine - wasm | better */ -function SqlTiddlerStore(options) { - options = options || {}; - this.attachmentStore = options.attachmentStore; - this.adminWiki = options.adminWiki || $tw.wiki; - this.eventListeners = {}; // Hashmap by type of array of event listener functions - this.eventOutstanding = {}; // Hashmap by type of boolean true of outstanding events - // Create the database - this.databasePath = options.databasePath || ":memory:"; - var SqlTiddlerDatabase = require("$:/plugins/tiddlywiki/multiwikiserver/store/sql-tiddler-database.js").SqlTiddlerDatabase; - this.sqlTiddlerDatabase = new SqlTiddlerDatabase({ - databasePath: this.databasePath, - engine: options.engine - }); - this.sqlTiddlerDatabase.createTables(); -} - -SqlTiddlerStore.prototype.addEventListener = function(type,listener) { - this.eventListeners[type] = this.eventListeners[type] || []; - this.eventListeners[type].push(listener); -}; - -SqlTiddlerStore.prototype.removeEventListener = function(type,listener) { - const listeners = this.eventListeners[type]; - if(listeners) { - var p = listeners.indexOf(listener); - if(p !== -1) { - listeners.splice(p,1); - } - } -}; - -SqlTiddlerStore.prototype.dispatchEvent = function(type /*, args */) { - const self = this; - if(!this.eventOutstanding[type]) { - $tw.utils.nextTick(function() { - self.eventOutstanding[type] = false; - const args = Array.prototype.slice.call(arguments,1), - listeners = self.eventListeners[type]; - if(listeners) { - for(var p=0; p 256) { - return "Too long"; - } - // Removed ~ from this list temporarily - if(allowPrivilegedCharacters) { - if(!(/^[^\s\u00A0\x00-\x1F\x7F`!@#%^&*()+={}\[\];\'\"<>,\\\?]+$/g.test(name))) { - return "Invalid character(s)"; - } - } else { - if(!(/^[^\s\u00A0\x00-\x1F\x7F`!@#$%^&*()+={}\[\];:\'\"<>.,\/\\\?]+$/g.test(name))) { - return "Invalid character(s)"; - } - } - return null; -}; - -/* -Returns null if the argument is an array of valid bag/recipe names, or a string error message if not -*/ -SqlTiddlerStore.prototype.validateItemNames = function(names,allowPrivilegedCharacters) { - if(!$tw.utils.isArray(names)) { - return "Not a valid array"; - } - var errors = []; - for(const name of names) { - const result = this.validateItemName(name,allowPrivilegedCharacters); - if(result && errors.indexOf(result) === -1) { - errors.push(result); - } - } - if(errors.length === 0) { - return null; - } else { - return errors.join("\n"); - } -}; - -SqlTiddlerStore.prototype.close = function() { - this.sqlTiddlerDatabase.close(); - this.sqlTiddlerDatabase = undefined; -}; - -/* -Given tiddler fields, tiddler_id and a bag_name, return the tiddler fields after the following process: -- Apply the tiddler_id as the revision field -- Apply the bag_name as the bag field -*/ -SqlTiddlerStore.prototype.processOutgoingTiddler = function(tiddlerFields,tiddler_id,bag_name,attachment_blob) { - if(attachment_blob !== null) { - return $tw.utils.extend( - {}, - tiddlerFields, - { - text: undefined, - _canonical_uri: `/bags/${$tw.utils.encodeURIComponentExtended(bag_name)}/tiddlers/${$tw.utils.encodeURIComponentExtended(tiddlerFields.title)}/blob` - } - ); - } else { - return tiddlerFields; - } -}; - -/* -*/ -SqlTiddlerStore.prototype.processIncomingTiddler = function(tiddlerFields, existing_attachment_blob, existing_canonical_uri) { - let attachmentSizeLimit = $tw.utils.parseNumber(this.adminWiki.getTiddlerText("$:/config/MultiWikiServer/AttachmentSizeLimit")); - if(attachmentSizeLimit < 100 * 1024) { - attachmentSizeLimit = 100 * 1024; - } - const attachmentsEnabled = this.adminWiki.getTiddlerText("$:/config/MultiWikiServer/EnableAttachments", "yes") === "yes"; - const contentTypeInfo = $tw.config.contentTypeInfo[tiddlerFields.type || "text/vnd.tiddlywiki"]; - const isBinary = !!contentTypeInfo && contentTypeInfo.encoding === "base64"; - - let shouldProcessAttachment = tiddlerFields.text && tiddlerFields.text.length > attachmentSizeLimit; - - if(existing_attachment_blob) { - const fileSize = this.attachmentStore.getAttachmentFileSize(existing_attachment_blob); - if(fileSize <= attachmentSizeLimit) { - const existingAttachmentMeta = this.attachmentStore.getAttachmentMetadata(existing_attachment_blob); - const hasCanonicalField = !!tiddlerFields._canonical_uri; - const skipAttachment = hasCanonicalField && (tiddlerFields._canonical_uri === (existingAttachmentMeta ? existingAttachmentMeta._canonical_uri : existing_canonical_uri)); - shouldProcessAttachment = !skipAttachment; - } else { - shouldProcessAttachment = false; +class SqlTiddlerStore { + constructor(options = {}) { + this.transactionType = "DEFERRED"; + this.eventer = new Eventer(true); + this.addEventListener = this.eventer.addEventListener.bind(this.eventer); + this.removeEventListener = this.eventer.removeEventListener.bind(this.eventer); + this.dispatchEvent = this.eventer.dispatchEvent.bind(this.eventer); + if (!(options === null || options === void 0 ? void 0 : options.attachmentStore)) { + throw new Error("SqlTiddlerStore requires an attachment store"); + } + this.attachmentStore = options.attachmentStore; + this.adminWiki = options.adminWiki || $tw.wiki; + this.sql = new sql_tiddler_database_1.SqlTiddlerDatabase(options.prisma); } - } - - if(attachmentsEnabled && isBinary && shouldProcessAttachment) { - const attachment_blob = existing_attachment_blob || this.attachmentStore.saveAttachment({ - text: tiddlerFields.text, - type: tiddlerFields.type, - reference: tiddlerFields.title, - _canonical_uri: tiddlerFields._canonical_uri - }); - - if(tiddlerFields && tiddlerFields._canonical_uri) { - delete tiddlerFields._canonical_uri; + /* + Returns null if a bag/recipe name is valid, or a string error message if not + */ + validateItemName(name, allowPrivilegedCharacters = false) { + if (typeof name !== "string") { + return "Not a valid string"; + } + if (name.length > 256) { + return "Too long"; + } + // Removed ~ from this list temporarily + if (allowPrivilegedCharacters) { + if (!(/^[^\s\u00A0\x00-\x1F\x7F`!@#%^&*()+={}\[\];\'\"<>,\\\?]+$/g.test(name))) { + return "Invalid character(s)"; + } + } + else { + if (!(/^[^\s\u00A0\x00-\x1F\x7F`!@#$%^&*()+={}\[\];:\'\"<>.,\/\\\?]+$/g.test(name))) { + return "Invalid character(s)"; + } + } + return null; } - - return { - tiddlerFields: Object.assign({}, tiddlerFields, { text: undefined }), - attachment_blob: attachment_blob - }; - } else { - return { - tiddlerFields: tiddlerFields, - attachment_blob: existing_attachment_blob - }; - } -}; - -SqlTiddlerStore.prototype.saveTiddlersFromPath = function(tiddler_files_path,bag_name) { - var self = this; - this.sqlTiddlerDatabase.transaction(function() { - // Clear out the bag - self.deleteAllTiddlersInBag(bag_name); - // Get the tiddlers - var path = require("path"); - var tiddlersFromPath = $tw.loadTiddlersFromPath(path.resolve($tw.boot.corePath,$tw.config.editionsPath,tiddler_files_path)); - // Save the tiddlers - for(const tiddlersFromFile of tiddlersFromPath) { - for(const tiddler of tiddlersFromFile.tiddlers) { - self.saveBagTiddler(tiddler,bag_name,null); - } - } - }); - self.dispatchEvent("change"); -}; - -SqlTiddlerStore.prototype.listBags = function() { - return this.sqlTiddlerDatabase.listBags(); -}; - -/* -Options include: - -allowPrivilegedCharacters - allows "$", ":" and "/" to appear in recipe name -*/ -SqlTiddlerStore.prototype.createBag = function(bag_name,description,options) { - options = options || {}; - var self = this; - return this.sqlTiddlerDatabase.transaction(function() { - const validationBagName = self.validateItemName(bag_name,options.allowPrivilegedCharacters); - if(validationBagName) { - return {message: validationBagName}; - } - self.sqlTiddlerDatabase.createBag(bag_name,description); - self.dispatchEvent("change"); - return null; - }); -}; - -SqlTiddlerStore.prototype.listRecipes = function() { - return this.sqlTiddlerDatabase.listRecipes(); -}; - -/* -Returns null on success, or {message:} on error - -Options include: - -allowPrivilegedCharacters - allows "$", ":" and "/" to appear in recipe name -*/ -SqlTiddlerStore.prototype.createRecipe = function(recipe_name,bag_names,description,options) { - bag_names = bag_names || []; - description = description || ""; - options = options || {}; - const validationRecipeName = this.validateItemName(recipe_name,options.allowPrivilegedCharacters); - if(validationRecipeName) { - return {message: validationRecipeName}; - } - if(bag_names.length === 0) { - return {message: "Recipes must contain at least one bag"}; - } - var self = this; - return this.sqlTiddlerDatabase.transaction(function() { - self.sqlTiddlerDatabase.createRecipe(recipe_name,bag_names,description); - self.dispatchEvent("change"); - return null; - }); -}; - -/* -Returns {tiddler_id:} -*/ -SqlTiddlerStore.prototype.saveBagTiddler = function(incomingTiddlerFields,bag_name) { - let _canonical_uri; - const existing_attachment_blob = this.sqlTiddlerDatabase.getBagTiddlerAttachmentBlob(incomingTiddlerFields.title,bag_name) - if(existing_attachment_blob) { - _canonical_uri = `/bags/${$tw.utils.encodeURIComponentExtended(bag_name)}/tiddlers/${$tw.utils.encodeURIComponentExtended(incomingTiddlerFields.title)}/blob` - } - const {tiddlerFields, attachment_blob} = this.processIncomingTiddler(incomingTiddlerFields,existing_attachment_blob,_canonical_uri); - const result = this.sqlTiddlerDatabase.saveBagTiddler(tiddlerFields,bag_name,attachment_blob); - this.dispatchEvent("change"); - return result; -}; - -/* -Create a tiddler in a bag adopting the specified file as the attachment. The attachment file must be on the same disk as the attachment store -Options include: - -filepath - filepath to the attachment file -hash - string hash of the attachment file -type - content type of file as uploaded - -Returns {tiddler_id:} -*/ -SqlTiddlerStore.prototype.saveBagTiddlerWithAttachment = function(incomingTiddlerFields,bag_name,options) { - const attachment_blob = this.attachmentStore.adoptAttachment(options.filepath,options.type,options.hash,options._canonical_uri); - if(attachment_blob) { - const result = this.sqlTiddlerDatabase.saveBagTiddler(incomingTiddlerFields,bag_name,attachment_blob); - this.dispatchEvent("change"); - return result; - } else { - return null; - } -}; - -/* -Returns {tiddler_id:,bag_name:} -*/ -SqlTiddlerStore.prototype.saveRecipeTiddler = function(incomingTiddlerFields,recipe_name) { - const existing_attachment_blob = this.sqlTiddlerDatabase.getRecipeTiddlerAttachmentBlob(incomingTiddlerFields.title,recipe_name) - const {tiddlerFields, attachment_blob} = this.processIncomingTiddler(incomingTiddlerFields,existing_attachment_blob,incomingTiddlerFields._canonical_uri); - const result = this.sqlTiddlerDatabase.saveRecipeTiddler(tiddlerFields,recipe_name,attachment_blob); - this.dispatchEvent("change"); - return result; -}; - -SqlTiddlerStore.prototype.deleteTiddler = function(title,bag_name) { - const result = this.sqlTiddlerDatabase.deleteTiddler(title,bag_name); - this.dispatchEvent("change"); - return result; -}; - -/* -returns {tiddler_id:,tiddler:} -*/ -SqlTiddlerStore.prototype.getBagTiddler = function(title,bag_name) { - var tiddlerInfo = this.sqlTiddlerDatabase.getBagTiddler(title,bag_name); - if(tiddlerInfo) { - return Object.assign( - {}, - tiddlerInfo, - { - tiddler: this.processOutgoingTiddler(tiddlerInfo.tiddler,tiddlerInfo.tiddler_id,bag_name,tiddlerInfo.attachment_blob) - }); - } else { - return null; - } -}; - -/* -Get an attachment ready to stream. Returns null if there is an error or: -tiddler_id: revision of tiddler -stream: stream of file -type: type of file -Returns {tiddler_id:,bag_name:} -*/ -SqlTiddlerStore.prototype.getBagTiddlerStream = function(title,bag_name) { - const tiddlerInfo = this.sqlTiddlerDatabase.getBagTiddler(title,bag_name); - if(tiddlerInfo) { - if(tiddlerInfo.attachment_blob) { - return $tw.utils.extend( - {}, - this.attachmentStore.getAttachmentStream(tiddlerInfo.attachment_blob), - { - tiddler_id: tiddlerInfo.tiddler_id, - bag_name: bag_name - } - ); - } else { - const { Readable } = require('stream'); - const stream = new Readable(); - stream._read = function() { - // Push data - const type = tiddlerInfo.tiddler.type || "text/plain"; - stream.push(tiddlerInfo.tiddler.text || "",($tw.config.contentTypeInfo[type] ||{encoding: "utf8"}).encoding); - // Push null to indicate the end of the stream - stream.push(null); - }; - return { - tiddler_id: tiddlerInfo.tiddler_id, - bag_name: bag_name, - stream: stream, - type: tiddlerInfo.tiddler.type || "text/plain" - } - } - } else { - return null; - } -}; - -/* -Returns {bag_name:, tiddler: {fields}, tiddler_id:} -*/ -SqlTiddlerStore.prototype.getRecipeTiddler = function(title,recipe_name) { - var tiddlerInfo = this.sqlTiddlerDatabase.getRecipeTiddler(title,recipe_name); - if(tiddlerInfo) { - return Object.assign( - {}, - tiddlerInfo, - { - tiddler: this.processOutgoingTiddler(tiddlerInfo.tiddler,tiddlerInfo.tiddler_id,tiddlerInfo.bag_name,tiddlerInfo.attachment_blob) - }); - } else { - return null; - } -}; - -/* -Get the titles of the tiddlers in a bag. Returns an empty array for bags that do not exist -*/ -SqlTiddlerStore.prototype.getBagTiddlers = function(bag_name) { - return this.sqlTiddlerDatabase.getBagTiddlers(bag_name); -}; - -/* -Get the tiddler_id of the newest tiddler in a bag. Returns null for bags that do not exist -*/ -SqlTiddlerStore.prototype.getBagLastTiddlerId = function(bag_name) { - return this.sqlTiddlerDatabase.getBagLastTiddlerId(bag_name); -}; - -/* -Get the titles of the tiddlers in a recipe as {title:,bag_name:}. Returns null for recipes that do not exist -*/ -SqlTiddlerStore.prototype.getRecipeTiddlers = function(recipe_name,options) { - return this.sqlTiddlerDatabase.getRecipeTiddlers(recipe_name,options); -}; - -/* -Get the tiddler_id of the newest tiddler in a recipe. Returns null for recipes that do not exist -*/ -SqlTiddlerStore.prototype.getRecipeLastTiddlerId = function(recipe_name) { - return this.sqlTiddlerDatabase.getRecipeLastTiddlerId(recipe_name); -}; - -SqlTiddlerStore.prototype.deleteAllTiddlersInBag = function(bag_name) { - var self = this; - return this.sqlTiddlerDatabase.transaction(function() { - const result = self.sqlTiddlerDatabase.deleteAllTiddlersInBag(bag_name); - self.dispatchEvent("change"); - return result; - }); -}; - -/* -Get the names of the bags in a recipe. Returns an empty array for recipes that do not exist -*/ -SqlTiddlerStore.prototype.getRecipeBags = function(recipe_name) { - return this.sqlTiddlerDatabase.getRecipeBags(recipe_name); -}; - + /* + Returns null if the argument is an array of valid bag/recipe names, or a string error message if not + */ + validateItemNames(names, allowPrivilegedCharacters) { + if (!$tw.utils.isArray(names)) { + return "Not a valid array"; + } + var errors = []; + for (const name of names) { + const result = this.validateItemName(name, allowPrivilegedCharacters); + if (result && errors.indexOf(result) === -1) { + errors.push(result); + } + } + if (errors.length === 0) { + return null; + } + else { + return errors.join("\n"); + } + } + /* + Given tiddler fields, tiddler_id and a bag_name, return the tiddler fields after the following process: + - Apply the tiddler_id as the revision field + - Apply the bag_name as the bag field + */ + processOutgoingTiddler(tiddlerFields, tiddler_id, bag_name, attachment_blob) { + if (attachment_blob !== null) { + const bagStr = $tw.utils.encodeURIComponentExtended(bag_name); + const titleStr = $tw.utils.encodeURIComponentExtended(tiddlerFields.title); + return $tw.utils.extend({}, tiddlerFields, { + text: undefined, + _canonical_uri: `/bags/${bagStr}/tiddlers/${titleStr}/blob` + }); + } + else { + return tiddlerFields; + } + } + /* + */ + processIncomingTiddler(tiddlerFields, existing_attachment_blob, existing_canonical_uri) { + let attachmentSizeLimit = $tw.utils.parseNumber(this.adminWiki.getTiddlerText("$:/config/MultiWikiServer/AttachmentSizeLimit")); + if (attachmentSizeLimit < 100 * 1024) { + attachmentSizeLimit = 100 * 1024; + } + const attachmentsEnabled = this.adminWiki.getTiddlerText("$:/config/MultiWikiServer/EnableAttachments", "yes") === "yes"; + const contentTypeInfo = $tw.config.contentTypeInfo[tiddlerFields.type || "text/vnd.tiddlywiki"]; + const isBinary = !!contentTypeInfo && contentTypeInfo.encoding === "base64"; + let shouldProcessAttachment = tiddlerFields.text && tiddlerFields.text.length > attachmentSizeLimit; + if (existing_attachment_blob) { + const fileSize = this.attachmentStore.getAttachmentFileSize(existing_attachment_blob); + if (fileSize && (fileSize <= attachmentSizeLimit)) { + const existingAttachmentMeta = this.attachmentStore.getAttachmentMetadata(existing_attachment_blob); + const hasCanonicalField = !!tiddlerFields._canonical_uri; + const skipAttachment = hasCanonicalField && (tiddlerFields._canonical_uri === (existingAttachmentMeta ? existingAttachmentMeta._canonical_uri : existing_canonical_uri)); + shouldProcessAttachment = !skipAttachment; + } + else { + shouldProcessAttachment = false; + } + } + if (attachmentsEnabled && isBinary && shouldProcessAttachment) { + const attachment_blob = existing_attachment_blob || this.attachmentStore.saveAttachment({ + text: tiddlerFields.text, + type: tiddlerFields.type, + reference: tiddlerFields.title, + _canonical_uri: tiddlerFields._canonical_uri + }); + if (tiddlerFields && tiddlerFields._canonical_uri) { + delete tiddlerFields._canonical_uri; + } + return { + tiddlerFields: Object.assign({}, tiddlerFields, { text: undefined }), + attachment_blob: attachment_blob + }; + } + else { + return { + tiddlerFields: tiddlerFields, + attachment_blob: existing_attachment_blob + }; + } + } + /** + * + * @param this Requires a transaction with at least IMMEDIATE isolation + * @param tiddler_files_path `resolve($tw.boot.corePath, $tw.config.editionsPath, tiddler_files_path)` + * @param bag_name + */ + async saveTiddlersFromPath(tiddler_files_path, bag_name) { + // Clear out the bag + await this.deleteAllTiddlersInBag(bag_name); + // Get the tiddlers + var tiddlersFromPath = $tw.loadTiddlersFromPath(path.resolve($tw.boot.corePath, $tw.config.editionsPath, tiddler_files_path)); + // Save the tiddlers + for (const tiddlersFromFile of tiddlersFromPath) { + for (const tiddler of tiddlersFromFile.tiddlers) { + await this.saveBagTiddler(tiddler, bag_name); + } + } + this.dispatchEvent("change"); + } + async listBags() { + return await this.sql.listBags(); + } + /* + Options include: + + allowPrivilegedCharacters - allows "$", ":" and "/" to appear in recipe name + */ + async createBag(bag_name, description, options) { + options = options || {}; + var self = this; + const validationBagName = self.validateItemName(bag_name, options.allowPrivilegedCharacters); + if (validationBagName) { + return { message: validationBagName }; + } + await self.sql.createBag(bag_name, description); + self.dispatchEvent("change"); + return null; + } + async listRecipes() { + return await this.sql.listRecipes(); + } + /* + Returns null on success, or {message:} on error + + Options include: + + allowPrivilegedCharacters - allows "$", ":" and "/" to appear in recipe name + */ + async createRecipe(recipe_name, bag_names, description, options) { + bag_names = bag_names || []; + description = description || ""; + options = options || {}; + const validationRecipeName = this.validateItemName(recipe_name, options.allowPrivilegedCharacters); + if (validationRecipeName) { + return { message: validationRecipeName }; + } + if (bag_names.length === 0) { + return { message: "Recipes must contain at least one bag" }; + } + // return await this.sql.transaction(async function () { + await this.sql.createRecipe(recipe_name, bag_names, description); + this.dispatchEvent("change"); + return null; + // }); + } + /* + Returns {tiddler_id:} + */ + async saveBagTiddler(incomingTiddlerFields, bag_name) { + let _canonical_uri; + const existing_attachment_blob = await this.sql.getBagTiddlerAttachmentBlob(incomingTiddlerFields.title, bag_name); + if (existing_attachment_blob) { + _canonical_uri = `/bags/${$tw.utils.encodeURIComponentExtended(bag_name)}/tiddlers/${$tw.utils.encodeURIComponentExtended(incomingTiddlerFields.title)}/blob`; + } + const { tiddlerFields, attachment_blob } = this.processIncomingTiddler(incomingTiddlerFields, existing_attachment_blob, _canonical_uri); + const result = await this.sql.saveBagTiddler(tiddlerFields, bag_name, attachment_blob); + this.dispatchEvent("change"); + return result; + } + /* + Create a tiddler in a bag adopting the specified file as the attachment. The attachment file must be on the same disk as the attachment store + Options include: + + filepath - filepath to the attachment file + hash - string hash of the attachment file + type - content type of file as uploaded + + Returns {tiddler_id:} + */ + async saveBagTiddlerWithAttachment(incomingTiddlerFields, bag_name, options) { + const attachment_blob = this.attachmentStore.adoptAttachment(options.filepath, options.type, options.hash, options._canonical_uri); + if (attachment_blob) { + const result = await this.sql.saveBagTiddler(incomingTiddlerFields, bag_name, attachment_blob); + this.dispatchEvent("change"); + return result; + } + else { + return null; + } + } + /* + Returns {tiddler_id:,bag_name:} + */ + async saveRecipeTiddler(incomingTiddlerFields, recipe_name) { + const existing_attachment_blob = await this.sql.getRecipeTiddlerAttachmentBlob(incomingTiddlerFields.title, recipe_name); + const { tiddlerFields, attachment_blob } = this.processIncomingTiddler(incomingTiddlerFields, existing_attachment_blob, incomingTiddlerFields._canonical_uri); + const result = await this.sql.saveRecipeTiddler(tiddlerFields, recipe_name, attachment_blob); + this.dispatchEvent("change"); + return result; + } + async deleteTiddler(title, bag_name) { + const result = await this.sql.deleteTiddler(title, bag_name); + this.dispatchEvent("change"); + return result; + } + /* + returns {tiddler_id:,tiddler:} + */ + async getBagTiddler(title, bag_name) { + var tiddlerInfo = await this.sql.getBagTiddler(title, bag_name); + if (tiddlerInfo) { + return await Object.assign({}, tiddlerInfo, { + tiddler: this.processOutgoingTiddler(tiddlerInfo.tiddler, tiddlerInfo.tiddler_id, bag_name, tiddlerInfo.attachment_blob) + }); + } + else { + return null; + } + } + /* + Get an attachment ready to stream. Returns null if there is an error or: + tiddler_id: revision of tiddler + stream: stream of file + type: type of file + Returns {tiddler_id:,bag_name:} + */ + async getBagTiddlerStream(title, bag_name) { + const tiddlerInfo = await this.sql.getBagTiddler(title, bag_name); + if (tiddlerInfo) { + if (tiddlerInfo.attachment_blob) { + return $tw.utils.extend({}, this.attachmentStore.getAttachmentStream(tiddlerInfo.attachment_blob), { + tiddler_id: tiddlerInfo.tiddler_id, + bag_name: bag_name + }); + } + else { + const { Readable } = require('stream'); + const stream = new Readable(); + stream._read = function () { + // Push data + const type = tiddlerInfo.tiddler.type || "text/plain"; + stream.push(tiddlerInfo.tiddler.text || "", ($tw.config.contentTypeInfo[type] || { encoding: "utf8" }).encoding); + // Push null to indicate the end of the stream + stream.push(null); + }; + return { + tiddler_id: tiddlerInfo.tiddler_id, + bag_name: bag_name, + stream: stream, + type: tiddlerInfo.tiddler.type || "text/plain" + }; + } + } + else { + return null; + } + } + /* + Returns {bag_name:, tiddler: {fields}, tiddler_id:} + */ + async getRecipeTiddler(title, recipe_name) { + var tiddlerInfo = await this.sql.getRecipeTiddler(title, recipe_name); + if (tiddlerInfo) { + return Object.assign({}, tiddlerInfo, { + tiddler: this.processOutgoingTiddler(tiddlerInfo.tiddler, tiddlerInfo.tiddler_id, tiddlerInfo.bag_name, tiddlerInfo.attachment_blob) + }); + } + else { + return null; + } + } + /* + Get the titles of the tiddlers in a bag. Returns an empty array for bags that do not exist + */ + async getBagTiddlers(bag_name) { + return await this.sql.getBagTiddlers(bag_name); + } + /* + Get the tiddler_id of the newest tiddler in a bag. Returns null for bags that do not exist + */ + async getBagLastTiddlerId(bag_name) { + return await this.sql.getBagLastTiddlerId(bag_name); + } + /* + Get the titles of the tiddlers in a recipe as {title:,bag_name:}. Returns null for recipes that do not exist + */ + async getRecipeTiddlers(recipe_name, options = {}) { + return await this.sql.getRecipeTiddlers(recipe_name, options); + } + /* + Get the tiddler_id of the newest tiddler in a recipe. Returns null for recipes that do not exist + */ + async getRecipeLastTiddlerId(recipe_name) { + return await this.sql.getRecipeLastTiddlerId(recipe_name); + } + async deleteAllTiddlersInBag(bag_name) { + var self = this; + // return await this.sql.transaction(async function () { + const result = await self.sql.deleteAllTiddlersInBag(bag_name); + self.dispatchEvent("change"); + return result; + // }); + } + /* + Get the names of the bags in a recipe. Returns an empty array for recipes that do not exist + */ + async getRecipeBags(recipe_name) { + return await this.sql.getRecipeBags(recipe_name); + } +} exports.SqlTiddlerStore = SqlTiddlerStore; - -})(); \ No newline at end of file +//# sourceMappingURL=sql-tiddler-store.js.map \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js.map b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js.map new file mode 100644 index 00000000000..96422a85a48 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js.map @@ -0,0 +1 @@ +{"version":3,"file":"sql-tiddler-store.js","sourceRoot":"","sources":["../../src/store/sql-tiddler-store.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/tests-sql-tiddler-database.js b/plugins/tiddlywiki/multiwikiserver/modules/store/tests-sql-tiddler-database.js index 563f86bf517..948dd7bb778 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/tests-sql-tiddler-database.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/tests-sql-tiddler-database.js @@ -6,6 +6,7 @@ tags: [[$:/tags/test-spec]] Tests the SQL tiddler database layer \*/ +/// if($tw.node) { (function(){ @@ -13,16 +14,17 @@ if($tw.node) { /*global $tw: false */ "use strict"; -describe("SQL tiddler database with node built-in sqlite", function() { - runSqlDatabaseTests("node"); +describe("SQL tiddler database with node built-in sqlite", function () { + // try {require("node:sqlite");} catch(e) {return;} + void runSqlDatabaseTests("node") }); -describe("SQL tiddler database with node-sqlite3-wasm", function() { - runSqlDatabaseTests("wasm"); +describe("SQL tiddler database with node-sqlite3-wasm", function () { + void runSqlDatabaseTests("wasm") }); -describe("SQL tiddler database with better-sqlite3", function() { - runSqlDatabaseTests("better"); +describe("SQL tiddler database with better-sqlite3", function () { + void runSqlDatabaseTests("better") }); function runSqlDatabaseTests(engine) { @@ -31,92 +33,95 @@ function runSqlDatabaseTests(engine) { const sqlTiddlerDatabase = new SqlTiddlerDatabase({ engine: engine }); - sqlTiddlerDatabase.createTables(); + + beforeAll(async () => { + await sqlTiddlerDatabase.createTables(); + }); // Tear down - afterAll(function() { + afterAll(async function() { // Close the database - sqlTiddlerDatabase.close(); + await sqlTiddlerDatabase.close(); }); // Run tests - it("should save and retrieve tiddlers using engine: " + engine, function() { + it("should save and retrieve tiddlers using engine: " + engine, async function() { // Create bags and recipes - expect(sqlTiddlerDatabase.createBag("bag-alpha","Bag alpha")).toEqual(1); - expect(sqlTiddlerDatabase.createBag("bag-beta","Bag beta")).toEqual(2); - expect(sqlTiddlerDatabase.createBag("bag-gamma","Bag gamma")).toEqual(3); - expect(sqlTiddlerDatabase.listBags()).toEqual([ + expect(await sqlTiddlerDatabase.createBag("bag-alpha","Bag alpha")).toEqual(1); + expect(await sqlTiddlerDatabase.createBag("bag-beta","Bag beta")).toEqual(2); + expect(await sqlTiddlerDatabase.createBag("bag-gamma","Bag gamma")).toEqual(3); + expect(await sqlTiddlerDatabase.listBags()).toEqual([ { bag_name: 'bag-alpha', bag_id: 1, accesscontrol: '', description: "Bag alpha" }, { bag_name: 'bag-beta', bag_id: 2, accesscontrol: '', description: "Bag beta" }, { bag_name: 'bag-gamma', bag_id: 3, accesscontrol: '', description: "Bag gamma" } ]); - expect(sqlTiddlerDatabase.createRecipe("recipe-rho",["bag-alpha","bag-beta"],"Recipe rho")).toEqual(1); - expect(sqlTiddlerDatabase.createRecipe("recipe-sigma",["bag-alpha","bag-gamma"],"Recipe sigma")).toEqual(2); - expect(sqlTiddlerDatabase.createRecipe("recipe-tau",["bag-alpha"],"Recipe tau")).toEqual(3); - expect(sqlTiddlerDatabase.createRecipe("recipe-upsilon",["bag-alpha","bag-gamma","bag-beta"],"Recipe upsilon")).toEqual(4); - expect(sqlTiddlerDatabase.listRecipes()).toEqual([ + expect(await sqlTiddlerDatabase.createRecipe("recipe-rho",["bag-alpha","bag-beta"],"Recipe rho")).toEqual(1); + expect(await sqlTiddlerDatabase.createRecipe("recipe-sigma",["bag-alpha","bag-gamma"],"Recipe sigma")).toEqual(2); + expect(await sqlTiddlerDatabase.createRecipe("recipe-tau",["bag-alpha"],"Recipe tau")).toEqual(3); + expect(await sqlTiddlerDatabase.createRecipe("recipe-upsilon",["bag-alpha","bag-gamma","bag-beta"],"Recipe upsilon")).toEqual(4); + expect(await sqlTiddlerDatabase.listRecipes()).toEqual([ { recipe_name: 'recipe-rho', recipe_id: 1, bag_names: ["bag-alpha","bag-beta"], description: "Recipe rho", owner_id: null }, { recipe_name: 'recipe-sigma', recipe_id: 2, bag_names: ["bag-alpha","bag-gamma"], description: "Recipe sigma", owner_id: null }, { recipe_name: 'recipe-tau', recipe_id: 3, bag_names: ["bag-alpha"], description: "Recipe tau", owner_id: null }, { recipe_name: 'recipe-upsilon', recipe_id: 4, bag_names: ["bag-alpha","bag-gamma","bag-beta"], description: "Recipe upsilon", owner_id: null } ]); - expect(sqlTiddlerDatabase.getRecipeBags("recipe-rho")).toEqual(["bag-alpha","bag-beta"]); - expect(sqlTiddlerDatabase.getRecipeBags("recipe-sigma")).toEqual(["bag-alpha","bag-gamma"]); - expect(sqlTiddlerDatabase.getRecipeBags("recipe-tau")).toEqual(["bag-alpha"]); - expect(sqlTiddlerDatabase.getRecipeBags("recipe-upsilon")).toEqual(["bag-alpha","bag-gamma","bag-beta"]); + expect(await sqlTiddlerDatabase.getRecipeBags("recipe-rho")).toEqual(["bag-alpha","bag-beta"]); + expect(await sqlTiddlerDatabase.getRecipeBags("recipe-sigma")).toEqual(["bag-alpha","bag-gamma"]); + expect(await sqlTiddlerDatabase.getRecipeBags("recipe-tau")).toEqual(["bag-alpha"]); + expect(await sqlTiddlerDatabase.getRecipeBags("recipe-upsilon")).toEqual(["bag-alpha","bag-gamma","bag-beta"]); // Save tiddlers - expect(sqlTiddlerDatabase.saveBagTiddler({title: "Another Tiddler",text: "I'm in alpha",tags: "one two three"},"bag-alpha")).toEqual({ + expect(await sqlTiddlerDatabase.saveBagTiddler({title: "Another Tiddler",text: "I'm in alpha",tags: "one two three"},"bag-alpha")).toEqual({ tiddler_id: 1 }); - expect(sqlTiddlerDatabase.saveBagTiddler({title: "Hello There",text: "I'm in alpha as well",tags: "one two three"},"bag-alpha")).toEqual({ + expect(await sqlTiddlerDatabase.saveBagTiddler({title: "Hello There",text: "I'm in alpha as well",tags: "one two three"},"bag-alpha")).toEqual({ tiddler_id: 2 }); - expect(sqlTiddlerDatabase.saveBagTiddler({title: "Hello There",text: "I'm in beta",tags: "four five six"},"bag-beta")).toEqual({ + expect(await sqlTiddlerDatabase.saveBagTiddler({title: "Hello There",text: "I'm in beta",tags: "four five six"},"bag-beta")).toEqual({ tiddler_id: 3 }); - expect(sqlTiddlerDatabase.saveBagTiddler({title: "Hello There",text: "I'm in gamma",tags: "seven eight nine"},"bag-gamma")).toEqual({ + expect(await sqlTiddlerDatabase.saveBagTiddler({title: "Hello There",text: "I'm in gamma",tags: "seven eight nine"},"bag-gamma")).toEqual({ tiddler_id: 4 }); // Verify what we've got - expect(sqlTiddlerDatabase.getRecipeTiddlers("recipe-rho")).toEqual([ + expect(await sqlTiddlerDatabase.getRecipeTiddlers("recipe-rho")).toEqual([ { title: 'Another Tiddler', tiddler_id: 1, bag_name: 'bag-alpha', is_deleted: 0 }, { title: 'Hello There', tiddler_id: 3, bag_name: 'bag-beta', is_deleted: 0 } ]); - expect(sqlTiddlerDatabase.getRecipeTiddlers("recipe-sigma")).toEqual([ + expect(await sqlTiddlerDatabase.getRecipeTiddlers("recipe-sigma")).toEqual([ { title: 'Another Tiddler', tiddler_id: 1, bag_name: 'bag-alpha', is_deleted: 0 }, { title: 'Hello There', tiddler_id: 4, bag_name: 'bag-gamma', is_deleted: 0 } ]); - 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"}); + expect((await sqlTiddlerDatabase.getRecipeTiddler("Hello There","recipe-rho"))?.tiddler).toEqual({ title: "Hello There", text: "I'm in beta", tags: "four five six" }); + expect(await sqlTiddlerDatabase.getRecipeTiddler("Missing Tiddler","recipe-rho")).toEqual(null); + expect((await sqlTiddlerDatabase.getRecipeTiddler("Another Tiddler","recipe-rho"))?.tiddler).toEqual({ title: "Another Tiddler", text: "I'm in alpha", tags: "one two three" }); + expect((await sqlTiddlerDatabase.getRecipeTiddler("Hello There","recipe-sigma"))?.tiddler).toEqual({ title: "Hello There", text: "I'm in gamma", tags: "seven eight nine" }); + expect((await sqlTiddlerDatabase.getRecipeTiddler("Another Tiddler","recipe-sigma"))?.tiddler).toEqual({ title: "Another Tiddler", text: "I'm in alpha", tags: "one two three" }); + expect((await sqlTiddlerDatabase.getRecipeTiddler("Hello There","recipe-upsilon"))?.tiddler).toEqual({title: "Hello There",text: "I'm in beta",tags: "four five six"}); // Delete a tiddlers to ensure the underlying tiddler in the recipe shows through - sqlTiddlerDatabase.deleteTiddler("Hello There","bag-beta"); - expect(sqlTiddlerDatabase.getRecipeTiddlers("recipe-rho")).toEqual([ + await sqlTiddlerDatabase.deleteTiddler("Hello There","bag-beta"); + expect(await sqlTiddlerDatabase.getRecipeTiddlers("recipe-rho")).toEqual([ { title: 'Another Tiddler', tiddler_id: 1, bag_name: 'bag-alpha', is_deleted: 0 }, { title: 'Hello There', tiddler_id: 2, bag_name: 'bag-alpha', is_deleted: 0 } ]); - expect(sqlTiddlerDatabase.getRecipeTiddlers("recipe-sigma")).toEqual([ + expect(await sqlTiddlerDatabase.getRecipeTiddlers("recipe-sigma")).toEqual([ { title: 'Another Tiddler', tiddler_id: 1, bag_name: 'bag-alpha', is_deleted: 0 }, { title: 'Hello There', tiddler_id: 4, bag_name: 'bag-gamma', is_deleted: 0 } ]); - expect(sqlTiddlerDatabase.getRecipeTiddler("Hello There","recipe-beta")).toEqual(null); - sqlTiddlerDatabase.deleteTiddler("Another Tiddler","bag-alpha"); - expect(sqlTiddlerDatabase.getRecipeTiddlers("recipe-rho")).toEqual([ { title: 'Hello There', tiddler_id: 2, bag_name: 'bag-alpha', is_deleted: 0 } ]); - expect(sqlTiddlerDatabase.getRecipeTiddlers("recipe-sigma")).toEqual([ { title: 'Hello There', tiddler_id: 4, bag_name: 'bag-gamma', is_deleted: 0 } ]); + expect(await sqlTiddlerDatabase.getRecipeTiddler("Hello There","recipe-beta")).toEqual(null); + await sqlTiddlerDatabase.deleteTiddler("Another Tiddler","bag-alpha"); + expect(await sqlTiddlerDatabase.getRecipeTiddlers("recipe-rho")).toEqual([ { title: "Hello There", tiddler_id: 2, bag_name: "bag-alpha", is_deleted: 0 } ]); + expect(await sqlTiddlerDatabase.getRecipeTiddlers("recipe-sigma")).toEqual([ { title: "Hello There", tiddler_id: 4, bag_name: "bag-gamma", is_deleted: 0 } ]); // Save a recipe tiddler - expect(sqlTiddlerDatabase.saveRecipeTiddler({title: "More", text: "None"},"recipe-rho")).toEqual({tiddler_id: 7, bag_name: 'bag-beta'}); - expect(sqlTiddlerDatabase.getRecipeTiddler("More","recipe-rho").tiddler).toEqual({title: "More", text: "None"}); + expect(await sqlTiddlerDatabase.saveRecipeTiddler({title: "More", text: "None"},"recipe-rho")).toEqual({tiddler_id: 7, bag_name: "bag-beta"}); + expect((await sqlTiddlerDatabase.getRecipeTiddler("More","recipe-rho"))?.tiddler).toEqual({title: "More", text: "None"}); }); - it("should manage users correctly", function() { + it("should manage users correctly", async function() { console.log("should manage users correctly") // Create users - const userId1 = sqlTiddlerDatabase.createUser("john_doe", "john@example.com", "pass123"); - const userId2 = sqlTiddlerDatabase.createUser("jane_doe", "jane@example.com", "pass123"); + const userId1 = await sqlTiddlerDatabase.createUser("john_doe", "john@example.com", "pass123"); + const userId2 = await sqlTiddlerDatabase.createUser("jane_doe", "jane@example.com", "pass123"); // Retrieve users - const user1 = sqlTiddlerDatabase.getUser(userId1); + const user1 = await sqlTiddlerDatabase.getUser(userId1); expect(user1.user_id).toBe(userId1); expect(user1.username).toBe("john_doe"); expect(user1.email).toBe("john@example.com"); @@ -124,107 +129,107 @@ function runSqlDatabaseTests(engine) { expect(user1.last_login).toBeNull(); // Update user - sqlTiddlerDatabase.updateUser(userId1, "john_updated", "john_updated@example.com"); - expect(sqlTiddlerDatabase.getUser(userId1).username).toBe("john_updated"); - expect(sqlTiddlerDatabase.getUser(userId1).email).toBe("john_updated@example.com"); + await sqlTiddlerDatabase.updateUser(userId1, "john_updated", "john_updated@example.com"); + expect((await sqlTiddlerDatabase.getUser(userId1)).username).toBe("john_updated"); + expect((await sqlTiddlerDatabase.getUser(userId1)).email).toBe("john_updated@example.com"); // List users - const users = sqlTiddlerDatabase.listUsers(); + const users = await sqlTiddlerDatabase.listUsers(); expect(users.length).toBe(2); expect(users[0].username).toBe("jane_doe"); expect(users[1].username).toBe("john_updated"); // Delete user - sqlTiddlerDatabase.deleteUser(userId2); - // expect(sqlTiddlerDatabase.getUser(userId2)).toBe(null || undefined); + await sqlTiddlerDatabase.deleteUser(userId2); + // expect(await sqlTiddlerDatabase.getUser(userId2)).toBe(null || undefined); }); - it("should manage groups correctly", function() { + it("should manage groups correctly", async function() { console.log("should manage groups correctly") // Create groups - const groupId1 = sqlTiddlerDatabase.createGroup("Editors", "Can edit content"); - const groupId2 = sqlTiddlerDatabase.createGroup("Viewers", "Can view content"); + const groupId1 = await sqlTiddlerDatabase.createGroup("Editors", "Can edit content"); + const groupId2 = await sqlTiddlerDatabase.createGroup("Viewers", "Can view content"); // Retrieve groups - expect(sqlTiddlerDatabase.getGroup(groupId1)).toEqual({ + expect(await sqlTiddlerDatabase.getGroup(groupId1)).toEqual({ group_id: groupId1, group_name: "Editors", description: "Can edit content" }); // Update group - sqlTiddlerDatabase.updateGroup(groupId1, "Super Editors", "Can edit all content"); - expect(sqlTiddlerDatabase.getGroup(groupId1).group_name).toBe("Super Editors"); - expect(sqlTiddlerDatabase.getGroup(groupId1).description).toBe("Can edit all content"); + await sqlTiddlerDatabase.updateGroup(groupId1, "Super Editors", "Can edit all content"); + expect((await sqlTiddlerDatabase.getGroup(groupId1)).group_name).toBe("Super Editors"); + expect((await sqlTiddlerDatabase.getGroup(groupId1)).description).toBe("Can edit all content"); // List groups - const groups = sqlTiddlerDatabase.listGroups(); + const groups = await sqlTiddlerDatabase.listGroups(); expect(groups.length).toBe(2); expect(groups[0].group_name).toBe("Super Editors"); expect(groups[1].group_name).toBe("Viewers"); // Delete group - sqlTiddlerDatabase.deleteGroup(groupId2); - // expect(sqlTiddlerDatabase.getGroup(groupId2)).toBe(null || undefined); + await sqlTiddlerDatabase.deleteGroup(groupId2); + // expect(await sqlTiddlerDatabase.getGroup(groupId2)).toBe(null || undefined); }); - it("should manage roles correctly", function() { + it("should manage roles correctly", async function() { console.log("should manage roles correctly") // Create roles - const roleId1 = sqlTiddlerDatabase.createRole("Admin" + Date.now(), "Full access"); - const roleId2 = sqlTiddlerDatabase.createRole("Editor" + Date.now(), "Can edit content"); + const roleId1 = await sqlTiddlerDatabase.createRole("Admin" + Date.now(), "Full access"); + const roleId2 = await sqlTiddlerDatabase.createRole("Editor" + Date.now(), "Can edit content"); // Retrieve roles - expect(sqlTiddlerDatabase.getRole(roleId1)).toEqual({ + expect(await sqlTiddlerDatabase.getRole(roleId1)).toEqual({ role_id: roleId1, role_name: jasmine.stringMatching(/^Admin\d+$/), description: "Full access" }); // Update role - sqlTiddlerDatabase.updateRole(roleId1, "Super Admin" + Date.now(), "God-like powers"); - expect(sqlTiddlerDatabase.getRole(roleId1).role_name).toMatch(/^Super Admin\d+$/); - expect(sqlTiddlerDatabase.getRole(roleId1).description).toBe("God-like powers"); + await sqlTiddlerDatabase.updateRole(roleId1, "Super Admin" + Date.now(), "God-like powers"); + expect((await sqlTiddlerDatabase.getRole(roleId1)).role_name).toMatch(/^Super Admin\d+$/); + expect((await sqlTiddlerDatabase.getRole(roleId1)).description).toBe("God-like powers"); // List roles - const roles = sqlTiddlerDatabase.listRoles(); + const roles = await sqlTiddlerDatabase.listRoles(); expect(roles.length).toBeGreaterThan(0); // expect(roles[0].role_name).toMatch(/^Editor\d+$/); // expect(roles[1].role_name).toMatch(/^Super Admin\d+$/); // Delete role - sqlTiddlerDatabase.deleteRole(roleId2); - // expect(sqlTiddlerDatabase.getRole(roleId2)).toBeUndefined(); + await sqlTiddlerDatabase.deleteRole(roleId2); + // expect(await sqlTiddlerDatabase.getRole(roleId2)).toBeUndefined(); }); - it("should manage permissions correctly", function() { + it("should manage permissions correctly", async function() { console.log("should manage permissions correctly") // Create permissions - const permissionId1 = sqlTiddlerDatabase.createPermission("read_tiddlers" + Date.now(), "Can read tiddlers"); - const permissionId2 = sqlTiddlerDatabase.createPermission("write_tiddlers" + Date.now(), "Can write tiddlers"); + const permissionId1 = await sqlTiddlerDatabase.createPermission("read_tiddlers" + Date.now(), "Can read tiddlers"); + const permissionId2 = await sqlTiddlerDatabase.createPermission("write_tiddlers" + Date.now(), "Can write tiddlers"); // Retrieve permissions - expect(sqlTiddlerDatabase.getPermission(permissionId1)).toEqual({ + expect(await sqlTiddlerDatabase.getPermission(permissionId1)).toEqual({ permission_id: permissionId1, permission_name: jasmine.stringMatching(/^read_tiddlers\d+$/), description: "Can read tiddlers" }); // Update permission - sqlTiddlerDatabase.updatePermission(permissionId1, "read_all_tiddlers" + Date.now(), "Can read all tiddlers"); - expect(sqlTiddlerDatabase.getPermission(permissionId1).permission_name).toMatch(/^read_all_tiddlers\d+$/); - expect(sqlTiddlerDatabase.getPermission(permissionId1).description).toBe("Can read all tiddlers"); + await sqlTiddlerDatabase.updatePermission(permissionId1, "read_all_tiddlers" + Date.now(), "Can read all tiddlers"); + expect((await sqlTiddlerDatabase.getPermission(permissionId1)).permission_name).toMatch(/^read_all_tiddlers\d+$/); + expect((await sqlTiddlerDatabase.getPermission(permissionId1)).description).toBe("Can read all tiddlers"); // List permissions - const permissions = sqlTiddlerDatabase.listPermissions(); + const permissions = await sqlTiddlerDatabase.listPermissions(); expect(permissions.length).toBeGreaterThan(0); expect(permissions[0].permission_name).toMatch(/^read_all_tiddlers\d+$/); expect(permissions[1].permission_name).toMatch(/^write_tiddlers\d+$/); // Delete permission - sqlTiddlerDatabase.deletePermission(permissionId2); - // expect(sqlTiddlerDatabase.getPermission(permissionId2)).toBeUndefined(); + await sqlTiddlerDatabase.deletePermission(permissionId2); + // expect(await sqlTiddlerDatabase.getPermission(permissionId2)).toBeUndefined(); }); } diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/tests-sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/store/tests-sql-tiddler-store.js index c5888b2ce16..5fe904079ef 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/tests-sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/tests-sql-tiddler-store.js @@ -1,9 +1,9 @@ /*\ -title: $:/plugins/tiddlywiki/multiwikiserver/store/tests-sql-tiddler-store.js +title: $:/plugins/tiddlywiki/multiwikiserver/await store/tests-sql-tiddler-await store.js type: application/javascript tags: [[$:/tags/test-spec]] -Tests the SQL tiddler store layer +Tests the SQL tiddler await store layer \*/ if($tw.node) { @@ -13,11 +13,11 @@ if($tw.node) { /*global $tw: false */ "use strict"; -describe("SQL tiddler store with node-sqlite3-wasm", function() { +describe("SQL tiddler await store with node-sqlite3-wasm", function() { runSqlStoreTests("wasm"); }); -describe("SQL tiddler store with better-sqlite3", function() { +describe("SQL tiddler await store with better-sqlite3", function() { runSqlStoreTests("better"); }); @@ -26,26 +26,28 @@ function runSqlStoreTests(engine) { var store; - beforeEach(function() { + beforeEach(async function() { store = new SqlTiddlerStore({ databasePath: ":memory:", - engine: engine + engine: engine, + attachmentStore: {} }); + await store.init(); }); - afterEach(function() { - store.close(); + afterEach(async function() { + await store.close(); store = null; }); - it("should return empty results without failure on an empty store", function() { - expect(store.listBags()).toEqual([]); - expect(store.listRecipes()).toEqual([]); + it("should return empty results without failure on an empty await store", async function() { + expect(await store.listBags()).toEqual([]); + expect(await store.listRecipes()).toEqual([]); }); - it("should return a single bag after creating a bag", function() { - expect(store.createBag("bag-alpha", "Bag alpha")).toEqual(null); - expect(store.listBags()).toEqual([{ + it("should return a single bag after creating a bag", async function() { + expect(await store.createBag("bag-alpha", "Bag alpha")).toEqual(null); + expect(await store.listBags()).toEqual([{ bag_name: "bag-alpha", bag_id: 1, accesscontrol: "", @@ -53,17 +55,17 @@ function runSqlStoreTests(engine) { }]); }); - it("should return empty results after failing to create a bag with an invalid name", function() { - expect(store.createBag("bag alpha", "Bag alpha")).toEqual({ + it("should return empty results after failing to create a bag with an invalid name", async function() { + expect(await store.createBag("bag alpha", "Bag alpha")).toEqual({ message: "Invalid character(s)" }); - expect(store.listBags()).toEqual([]); + expect(await store.listBags()).toEqual([]); }); - it("should return a bag with new description after re-creating", function() { - expect(store.createBag("bag-alpha", "Bag alpha")).toEqual(null); - expect(store.createBag("bag-alpha", "Different description")).toEqual(null); - expect(store.listBags()).toEqual([{ + it("should return a bag with new description after re-creating", async function() { + expect(await store.createBag("bag-alpha", "Bag alpha")).toEqual(null); + expect(await store.createBag("bag-alpha", "Different description")).toEqual(null); + expect(await store.listBags()).toEqual([{ bag_name: "bag-alpha", bag_id: 1, accesscontrol: "", @@ -71,9 +73,9 @@ function runSqlStoreTests(engine) { }]); }); - it("should return a saved tiddler within a bag", function() { - expect(store.createBag("bag-alpha", "Bag alpha")).toEqual(null); - var saveBagResult = store.saveBagTiddler({ + it("should return a saved tiddler within a bag", async function() { + expect(await store.createBag("bag-alpha", "Bag alpha")).toEqual(null); + var saveBagResult = await store.saveBagTiddler({ title: "Another Tiddler", text: "I'm in alpha", tags: "one two three" @@ -82,38 +84,38 @@ function runSqlStoreTests(engine) { expect(new Set(Object.keys(saveBagResult))).toEqual(new Set(["tiddler_id"])); expect(typeof(saveBagResult.tiddler_id)).toBe("number"); - expect(store.getBagTiddlers("bag-alpha")).toEqual([{title: "Another Tiddler", tiddler_id: 1}]); + expect(await store.getBagTiddlers("bag-alpha")).toEqual([{title: "Another Tiddler", tiddler_id: 1}]); - var getBagTiddlerResult = store.getBagTiddler("Another Tiddler","bag-alpha"); + var getBagTiddlerResult = await store.getBagTiddler("Another Tiddler","bag-alpha"); expect(typeof(getBagTiddlerResult.tiddler_id)).toBe("number"); delete getBagTiddlerResult.tiddler_id; expect(getBagTiddlerResult).toEqual({ attachment_blob: null, tiddler: {title: "Another Tiddler", text: "I'm in alpha", tags: "one two three"} }); }); - it("should return a single recipe after creating that recipe", function() { - expect(store.createBag("bag-alpha","Bag alpha")).toEqual(null); - expect(store.createBag("bag-beta","Bag beta")).toEqual(null); - expect(store.createRecipe("recipe-rho",["bag-alpha","bag-beta"],"Recipe rho")).toEqual(null); + it("should return a single recipe after creating that recipe", async function() { + expect(await store.createBag("bag-alpha","Bag alpha")).toEqual(null); + expect(await store.createBag("bag-beta","Bag beta")).toEqual(null); + expect(await store.createRecipe("recipe-rho",["bag-alpha","bag-beta"],"Recipe rho")).toEqual(null); - expect(store.listRecipes()).toEqual([ + expect(await store.listRecipes()).toEqual([ { recipe_name: "recipe-rho", recipe_id: 1, bag_names: ["bag-alpha","bag-beta"], description: "Recipe rho", owner_id: null } ]); }); - it("should return a recipe's bags after creating that recipe", function() { - expect(store.createBag("bag-alpha","Bag alpha")).toEqual(null); - expect(store.createBag("bag-beta","Bag beta")).toEqual(null); - expect(store.createRecipe("recipe-rho",["bag-alpha","bag-beta"],"Recipe rho")).toEqual(null); + it("should return a recipe's bags after creating that recipe", async function() { + expect(await store.createBag("bag-alpha","Bag alpha")).toEqual(null); + expect(await store.createBag("bag-beta","Bag beta")).toEqual(null); + expect(await store.createRecipe("recipe-rho",["bag-alpha","bag-beta"],"Recipe rho")).toEqual(null); - expect(store.getRecipeBags("recipe-rho")).toEqual(["bag-alpha","bag-beta"]); + expect(await store.getRecipeBags("recipe-rho")).toEqual(["bag-alpha","bag-beta"]); }); - it("should return a saved tiddler within a recipe", function() { - expect(store.createBag("bag-alpha","Bag alpha")).toEqual(null); - expect(store.createBag("bag-beta","Bag beta")).toEqual(null); - expect(store.createRecipe("recipe-rho",["bag-alpha","bag-beta"],"Recipe rho")).toEqual(null); + it("should return a saved tiddler within a recipe", async function() { + expect(await store.createBag("bag-alpha","Bag alpha")).toEqual(null); + expect(await store.createBag("bag-beta","Bag beta")).toEqual(null); + expect(await store.createRecipe("recipe-rho",["bag-alpha","bag-beta"],"Recipe rho")).toEqual(null); - var saveRecipeResult = store.saveRecipeTiddler({ + var saveRecipeResult = await store.saveRecipeTiddler({ title: "Another Tiddler", text: "I'm in rho" },"recipe-rho"); @@ -122,25 +124,25 @@ function runSqlStoreTests(engine) { expect(typeof(saveRecipeResult.tiddler_id)).toBe("number"); expect(saveRecipeResult.bag_name).toBe("bag-beta"); - expect(store.getRecipeTiddlers("recipe-rho")).toEqual([{title: "Another Tiddler", tiddler_id: 1, bag_name: "bag-beta", is_deleted: 0 }]); + expect(await store.getRecipeTiddlers("recipe-rho")).toEqual([{title: "Another Tiddler", tiddler_id: 1, bag_name: "bag-beta", is_deleted: 0 }]); - var getRecipeTiddlerResult = store.getRecipeTiddler("Another Tiddler","recipe-rho"); + var getRecipeTiddlerResult = await store.getRecipeTiddler("Another Tiddler","recipe-rho"); expect(typeof(getRecipeTiddlerResult.tiddler_id)).toBe("number"); delete getRecipeTiddlerResult.tiddler_id; expect(getRecipeTiddlerResult).toEqual({ attachment_blob: null, bag_name: "bag-beta", tiddler: {title: "Another Tiddler", text: "I'm in rho"} }); }); - it("should return no tiddlers after the only one has been deleted", function() { - expect(store.createBag("bag-alpha","Bag alpha")).toEqual(null); + it("should return no tiddlers after the only one has been deleted", async function() { + expect(await store.createBag("bag-alpha","Bag alpha")).toEqual(null); - store.saveBagTiddler({ + await store.saveBagTiddler({ title: "Another Tiddler", text: "I'm in alpha", tags: "one two three" }, "bag-alpha"); - store.deleteTiddler("Another Tiddler","bag-alpha"); - expect(store.getBagTiddlers("bag-alpha")).toEqual([]); + await store.deleteTiddler("Another Tiddler","bag-alpha"); + expect(await store.getBagTiddlers("bag-alpha")).toEqual([]); }); } diff --git a/plugins/tiddlywiki/multiwikiserver/modules/tests/test-attachment.js b/plugins/tiddlywiki/multiwikiserver/modules/tests/test-attachment.js index 9298c43f8a3..1fa84f7eee9 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/tests/test-attachment.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/tests/test-attachment.js @@ -6,30 +6,30 @@ tags: [[$:/tags/test-spec]] Tests attachments. \*/ -if(typeof window === 'undefined' && typeof process !== 'undefined' && process.versions && process.versions.node) { +if(typeof window === "undefined" && typeof process !== "undefined" && process.versions && process.versions.node) { (function(){ - var fs = require('fs'); - var path = require('path'); - var assert = require('assert'); - var AttachmentStore = require('$:/plugins/tiddlywiki/multiwikiserver/store/attachments.js').AttachmentStore; - const {Buffer} = require('buffer'); + var fs = require("fs"); + var path = require("path"); + var assert = require("assert"); + var AttachmentStore = require("$:/plugins/tiddlywiki/multiwikiserver/store/attachments.js").AttachmentStore; + const{Buffer} = require("buffer"); - function generateFileWithSize(filePath, sizeInBytes) { - return new Promise((resolve, reject) => { + async function generateFileWithSize(filePath, sizeInBytes) { + return await new Promise((resolve, reject) => { var buffer = Buffer.alloc(sizeInBytes); for(var i = 0; i < sizeInBytes; i++) { buffer[i] = Math.floor(Math.random() * 256); } - fs.writeFile(filePath, buffer, (err) => { + fs.writeFile(filePath, buffer, err => { if(err) { - console.error('Error writing file:', err); + console.error("Error writing file:", err); reject(err); } else { - console.log('File '+filePath+' generated with size '+sizeInBytes+' bytes'); + console.log("File "+filePath+" generated with size "+sizeInBytes+" bytes"); fs.readFile(filePath, (err, data) => { if(err) { - console.error('Error reading file:', err); + console.error("Error reading file:", err); reject(err); } else { resolve(data); @@ -41,10 +41,10 @@ if(typeof window === 'undefined' && typeof process !== 'undefined' && process.ve } (function() { - 'use strict'; + "use strict"; if($tw.node) { - describe('AttachmentStore', function() { - var storePath = './editions/test/test-store'; + describe("AttachmentStore", function() { + var storePath = "./editions/test/test-store"; var attachmentStore = new AttachmentStore({ storePath: storePath }); var originalTimeout; @@ -69,111 +69,111 @@ if(typeof window === 'undefined' && typeof process !== 'undefined' && process.ve }); }); - it('isValidAttachmentName', function() { - expect(attachmentStore.isValidAttachmentName('abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890')).toBe(true); - expect(attachmentStore.isValidAttachmentName('invalid-name')).toBe(false); + it("isValidAttachmentName", function() { + expect(attachmentStore.isValidAttachmentName("abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890")).toBe(true); + expect(attachmentStore.isValidAttachmentName("invalid-name")).toBe(false); }); - it('saveAttachment', function() { + it("saveAttachment", function() { var options = { - text: 'Hello, World!', - type: 'text/plain', - reference: 'test-reference', + text: "Hello, World!", + type: "text/plain", + reference: "test-reference", }; var contentHash = attachmentStore.saveAttachment(options); assert.strictEqual(contentHash.length, 64); - assert.strictEqual(fs.existsSync(path.resolve(storePath, 'files', contentHash)), true); + assert.strictEqual(fs.existsSync(path.resolve(storePath, "files", contentHash)), true); }); - it('adoptAttachment', function() { - var incomingFilepath = path.resolve(storePath, 'incoming-file.txt'); - fs.writeFileSync(incomingFilepath, 'Hello, World!'); - var type = 'text/plain'; - var hash = 'abcdef0123456789abcdef0123456789'; - var _canonical_uri = 'test-canonical-uri'; + it("adoptAttachment", function() { + var incomingFilepath = path.resolve(storePath, "incoming-file.txt"); + fs.writeFileSync(incomingFilepath, "Hello, World!"); + var type = "text/plain"; + var hash = "abcdef0123456789abcdef0123456789"; + var _canonical_uri = "test-canonical-uri"; attachmentStore.adoptAttachment(incomingFilepath, type, hash, _canonical_uri); - expect(fs.existsSync(path.resolve(storePath, 'files', hash))).toBe(true); + expect(fs.existsSync(path.resolve(storePath, "files", hash))).toBe(true); }); - it('getAttachmentStream', function() { + it("getAttachmentStream", function() { var options = { - text: 'Hello, World!', - type: 'text/plain', - filename: 'data.txt', + text: "Hello, World!", + type: "text/plain", + filename: "data.txt", }; var contentHash = attachmentStore.saveAttachment(options); var stream = attachmentStore.getAttachmentStream(contentHash); expect(stream).not.toBeNull(); - expect(stream.type).toBe('text/plain'); + expect(stream?.type).toBe("text/plain"); }); - it('getAttachmentFileSize', function() { + it("getAttachmentFileSize", function() { var options = { - text: 'Hello, World!', - type: 'text/plain', - reference: 'test-reference', + text: "Hello, World!", + type: "text/plain", + reference: "test-reference", }; var contentHash = attachmentStore.saveAttachment(options); var fileSize = attachmentStore.getAttachmentFileSize(contentHash); expect(fileSize).toBe(13); }); - it('getAttachmentMetadata', function() { + it("getAttachmentMetadata", function() { var options = { - text: 'Hello, World!', - type: 'text/plain', - filename: 'data.txt', + text: "Hello, World!", + type: "text/plain", + filename: "data.txt", }; var contentHash = attachmentStore.saveAttachment(options); var metadata = attachmentStore.getAttachmentMetadata(contentHash); expect(metadata).not.toBeNull(); - expect(metadata.type).toBe('text/plain'); - expect(metadata.filename).toBe('data.txt'); + expect(metadata.type).toBe("text/plain"); + expect(metadata.filename).toBe("data.txt"); }); - it('saveAttachment large file', async function() { + it("saveAttachment large file", async function() { var sizeInMB = 10 - const file = await generateFileWithSize('./editions/test/test-store/large-file.txt', 1024 * 1024 * sizeInMB) + const file = await generateFileWithSize("./editions/test/test-store/large-file.txt", 1024 * 1024 * sizeInMB) var options = { text: file, - type: 'application/octet-stream', - reference: 'test-reference', + type: "application/octet-stream", + reference: "test-reference", }; var contentHash = attachmentStore.saveAttachment(options); assert.strictEqual(contentHash.length, 64); - assert.strictEqual(fs.existsSync(path.resolve(storePath, 'files', contentHash)), true); + assert.strictEqual(fs.existsSync(path.resolve(storePath, "files", contentHash)), true); }); - it('saveAttachment multiple large files', async function() { + it("saveAttachment multiple large files", async function() { var sizeInMB = 10; var numFiles = 5; - for (var i = 0; i < numFiles; i++) { + for(var i = 0; i < numFiles; i++) { const file = await generateFileWithSize(`./editions/test/test-store/large-file-${i}.txt`, 1024 * 1024 * sizeInMB); var options = { text: file, - type: 'application/octet-stream', + type: "application/octet-stream", reference: `test-reference-${i}`, }; var contentHash = attachmentStore.saveAttachment(options); assert.strictEqual(contentHash.length, 64); - assert.strictEqual(fs.existsSync(path.resolve(storePath, 'files', contentHash)), true); + assert.strictEqual(fs.existsSync(path.resolve(storePath, "files", contentHash)), true); } }); - it('getAttachmentStream multiple large files', async function() { + it("getAttachmentStream multiple large files", async function() { var sizeInMB = 10; var numFiles = 5; - for (var i = 0; i < numFiles; i++) { + for(var i = 0; i < numFiles; i++) { const file = await generateFileWithSize(`./editions/test/test-store/large-file-${i}.txt`, 1024 * 1024 * sizeInMB); var options = { text: file, - type: 'application/octet-stream', + type: "application/octet-stream", reference: `test-reference-${i}`, }; var contentHash = attachmentStore.saveAttachment(options); var stream = attachmentStore.getAttachmentStream(contentHash); assert.notStrictEqual(stream, null); - assert.strictEqual(stream.type, 'application/octet-stream'); + assert.strictEqual(stream?.type, "application/octet-stream"); } }); }); diff --git a/plugins/tiddlywiki/multiwikiserver/prisma/schema.prisma b/plugins/tiddlywiki/multiwikiserver/prisma/schema.prisma new file mode 100644 index 00000000000..634e44b118e --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/prisma/schema.prisma @@ -0,0 +1,161 @@ +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} +generator client { + provider = "prisma-client-js" + binaryTargets = ["native"] +} +model acl { + acl_id Int @id @default(autoincrement()) + entity_name String + entity_type String + role_id Int + permission_id Int + permission permissions? @relation(fields: [permission_id], references: [permission_id], onDelete: NoAction, onUpdate: NoAction) + role roles? @relation(fields: [role_id], references: [role_id], onDelete: NoAction, onUpdate: NoAction) + + @@index([entity_name], map: "idx_acl_entity_id") +} + +model bags { + bag_id Int @id @default(autoincrement()) + bag_name String @unique(map: "sqlite_autoindex_bags_1") + accesscontrol String + description String + recipe_bags recipe_bags[] + tiddlers tiddlers[] +} + +model group_roles { + group_id Int + role_id Int + role roles @relation(fields: [role_id], references: [role_id], onDelete: NoAction, onUpdate: NoAction) + group groups @relation(fields: [group_id], references: [group_id], onDelete: NoAction, onUpdate: NoAction) + + @@id([group_id, role_id]) +} + +model groups { + group_id Int @id @default(autoincrement()) + group_name String @unique(map: "sqlite_autoindex_groups_1") + description String? + group_roles group_roles[] + user_groups user_groups[] +} + +model permissions { + permission_id Int @id @default(autoincrement()) + permission_name String @unique(map: "sqlite_autoindex_permissions_1") + description String? + acl acl[] + role_permissions role_permissions[] +} + +model recipe_bags { + recipe_id Int + bag_id Int + position Int + bag bags @relation(fields: [bag_id], references: [bag_id], onDelete: Cascade) + recipe recipes @relation(fields: [recipe_id], references: [recipe_id], onDelete: Cascade) + + @@unique([recipe_id, bag_id], map: "sqlite_autoindex_recipe_bags_1") + @@index([recipe_id], map: "idx_recipe_bags_recipe_id") +} + +model recipes { + recipe_id Int @id @default(autoincrement()) + recipe_name String @unique(map: "sqlite_autoindex_recipes_1") + description String + recipe_bags recipe_bags[] + owner_id Int? + user users? @relation(fields: [owner_id], references: [user_id], onDelete: NoAction, onUpdate: NoAction) +} + +/// The underlying table does not contain a valid unique identifier and can therefore currently not be handled by Prisma Client. +model role_permissions { + role_id Int + permission_id Int + permission permissions @relation(fields: [permission_id], references: [permission_id], onDelete: NoAction, onUpdate: NoAction) + role roles @relation(fields: [role_id], references: [role_id], onDelete: NoAction, onUpdate: NoAction) + + @@id([role_id, permission_id]) +} + +model roles { + role_id Int @id @default(autoincrement()) + role_name String @unique(map: "sqlite_autoindex_roles_1") + description String? + acls acl[] + group_roles group_roles[] + role_permissions role_permissions[] + user_roles user_roles[] +} + +model sessions { + session_id String @id + created_at String + last_accessed String + user_id Int + user users @relation(fields: [user_id], references: [user_id], onDelete: NoAction, onUpdate: NoAction) +} + +model tiddlers { + tiddler_id Int @id @default(autoincrement()) + bag_id Int + title String + is_deleted Boolean + attachment_blob String? + fields fields[] + // Deleting a referenced record (bag_id) will trigger the deletion of referencing record (tiddler). + bag bags @relation(fields: [bag_id], references: [bag_id], onDelete: Cascade) + + @@unique([bag_id, title], map: "sqlite_autoindex_tiddlers_1") + @@index([bag_id], map: "idx_tiddlers_bag_id") +} + +model fields { + tiddler_id Int + field_name String + field_value String + // Deleting a referenced record (tiddler_id) will trigger the deletion of referencing record (field). + tiddler tiddlers? @relation(fields: [tiddler_id], references: [tiddler_id], onDelete: Cascade) + + @@id([tiddler_id, field_name]) + @@index([tiddler_id], map: "idx_fields_tiddler_id") +} + +model user_groups { + user_id Int + group_id Int + group groups @relation(fields: [group_id], references: [group_id], onDelete: NoAction, onUpdate: NoAction) + user users @relation(fields: [user_id], references: [user_id], onDelete: NoAction, onUpdate: NoAction) + + @@id([user_id, group_id]) + +} + +/// The underlying table does not contain a valid unique identifier and can therefore currently not be handled by Prisma Client. +model user_roles { + user_id Int + role_id Int + role roles @relation(fields: [role_id], references: [role_id], onDelete: NoAction, onUpdate: NoAction) + user users @relation(fields: [user_id], references: [user_id], onDelete: NoAction, onUpdate: NoAction) + + @@id([user_id, role_id]) + +} + +model users { + user_id Int @id @default(autoincrement()) + username String @unique(map: "sqlite_autoindex_users_1") + email String @unique(map: "sqlite_autoindex_users_2") + password String + created_at String @default("datetime('now')") + last_login String? + recipes recipes[] + sessions sessions[] + user_groups user_groups[] + user_roles user_roles[] +} + diff --git a/plugins/tiddlywiki/multiwikiserver/prisma/schema.prisma.meta b/plugins/tiddlywiki/multiwikiserver/prisma/schema.prisma.meta new file mode 100644 index 00000000000..ed59128f5b0 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/prisma/schema.prisma.meta @@ -0,0 +1,2 @@ +title: $:/plugins/tiddlywiki/multiwikiserver/prisma/schema.prisma +type: text/plain \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/prisma/schema.prisma.sql b/plugins/tiddlywiki/multiwikiserver/prisma/schema.prisma.sql new file mode 100644 index 00000000000..0b83fa9a110 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/prisma/schema.prisma.sql @@ -0,0 +1,194 @@ +-- CreateTable +CREATE TABLE "acl" ( + "acl_id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "entity_name" TEXT NOT NULL, + "entity_type" TEXT NOT NULL, + "role_id" INTEGER, + "permission_id" INTEGER, + CONSTRAINT "acl_permission_id_fkey" FOREIGN KEY ("permission_id") REFERENCES "permissions" ("permission_id") ON DELETE NO ACTION ON UPDATE NO ACTION, + CONSTRAINT "acl_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "roles" ("role_id") ON DELETE NO ACTION ON UPDATE NO ACTION +); + +-- CreateTable +CREATE TABLE "bags" ( + "bag_id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "bag_name" TEXT NOT NULL, + "accesscontrol" TEXT NOT NULL, + "description" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "fields" ( + "tiddler_id" INTEGER NOT NULL, + "field_name" TEXT NOT NULL, + "field_value" TEXT NOT NULL, + + PRIMARY KEY ("tiddler_id", "field_name"), + CONSTRAINT "fields_tiddler_id_fkey" FOREIGN KEY ("tiddler_id") REFERENCES "tiddlers" ("tiddler_id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "group_roles" ( + "group_id" INTEGER NOT NULL, + "role_id" INTEGER NOT NULL, + + PRIMARY KEY ("group_id", "role_id"), + CONSTRAINT "group_roles_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "roles" ("role_id") ON DELETE NO ACTION ON UPDATE NO ACTION, + CONSTRAINT "group_roles_group_id_fkey" FOREIGN KEY ("group_id") REFERENCES "groups" ("group_id") ON DELETE NO ACTION ON UPDATE NO ACTION +); + +-- CreateTable +CREATE TABLE "groups" ( + "group_id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "group_name" TEXT NOT NULL, + "description" TEXT +); + +-- CreateTable +CREATE TABLE "permissions" ( + "permission_id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "permission_name" TEXT NOT NULL, + "description" TEXT +); + +-- CreateTable +CREATE TABLE "recipe_bags" ( + "recipe_id" INTEGER NOT NULL, + "bag_id" INTEGER NOT NULL, + "position" INTEGER NOT NULL, + CONSTRAINT "recipe_bags_bag_id_fkey" FOREIGN KEY ("bag_id") REFERENCES "bags" ("bag_id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "recipe_bags_recipe_id_fkey" FOREIGN KEY ("recipe_id") REFERENCES "recipes" ("recipe_id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "recipes" ( + "recipe_id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "recipe_name" TEXT NOT NULL, + "description" TEXT NOT NULL, + "owner_id" INTEGER, + CONSTRAINT "recipes_owner_id_fkey" FOREIGN KEY ("owner_id") REFERENCES "users" ("user_id") ON DELETE NO ACTION ON UPDATE NO ACTION +); + +-- CreateTable +CREATE TABLE "role_permissions" ( + "role_id" INTEGER NOT NULL, + "permission_id" INTEGER NOT NULL, + + PRIMARY KEY ("role_id", "permission_id"), + CONSTRAINT "role_permissions_permission_id_fkey" FOREIGN KEY ("permission_id") REFERENCES "permissions" ("permission_id") ON DELETE NO ACTION ON UPDATE NO ACTION, + CONSTRAINT "role_permissions_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "roles" ("role_id") ON DELETE NO ACTION ON UPDATE NO ACTION +); + +-- CreateTable +CREATE TABLE "roles" ( + "role_id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "role_name" TEXT NOT NULL, + "description" TEXT +); + +-- CreateTable +CREATE TABLE "sessions" ( + "user_id" INTEGER NOT NULL, + "session_id" TEXT NOT NULL PRIMARY KEY, + "created_at" TEXT NOT NULL, + "last_accessed" TEXT NOT NULL, + CONSTRAINT "sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("user_id") ON DELETE NO ACTION ON UPDATE NO ACTION +); + +-- CreateTable +CREATE TABLE "tiddlers" ( + "tiddler_id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "bag_id" INTEGER NOT NULL, + "title" TEXT NOT NULL, + "is_deleted" BOOLEAN NOT NULL, + "attachment_blob" TEXT, + CONSTRAINT "tiddlers_bag_id_fkey" FOREIGN KEY ("bag_id") REFERENCES "bags" ("bag_id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "user_groups" ( + "user_id" INTEGER NOT NULL, + "group_id" INTEGER NOT NULL, + + PRIMARY KEY ("user_id", "group_id"), + CONSTRAINT "user_groups_group_id_fkey" FOREIGN KEY ("group_id") REFERENCES "groups" ("group_id") ON DELETE NO ACTION ON UPDATE NO ACTION, + CONSTRAINT "user_groups_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("user_id") ON DELETE NO ACTION ON UPDATE NO ACTION +); + +-- CreateTable +CREATE TABLE "user_roles" ( + "user_id" INTEGER NOT NULL, + "role_id" INTEGER NOT NULL, + + PRIMARY KEY ("user_id", "role_id"), + CONSTRAINT "user_roles_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "roles" ("role_id") ON DELETE NO ACTION ON UPDATE NO ACTION, + CONSTRAINT "user_roles_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("user_id") ON DELETE NO ACTION ON UPDATE NO ACTION +); + +-- CreateTable +CREATE TABLE "users" ( + "user_id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "username" TEXT NOT NULL, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + "created_at" TEXT DEFAULT 'datetime(''now'')', + "last_login" TEXT +); + +-- CreateIndex +CREATE INDEX "idx_acl_entity_id" ON "acl"("entity_name"); + +-- CreateIndex +Pragma writable_schema=1; +CREATE UNIQUE INDEX "sqlite_autoindex_bags_1" ON "bags"("bag_name"); +Pragma writable_schema=0; + +-- CreateIndex +CREATE INDEX "idx_fields_tiddler_id" ON "fields"("tiddler_id"); + +-- CreateIndex +Pragma writable_schema=1; +CREATE UNIQUE INDEX "sqlite_autoindex_groups_1" ON "groups"("group_name"); +Pragma writable_schema=0; + +-- CreateIndex +Pragma writable_schema=1; +CREATE UNIQUE INDEX "sqlite_autoindex_permissions_1" ON "permissions"("permission_name"); +Pragma writable_schema=0; + +-- CreateIndex +CREATE INDEX "idx_recipe_bags_recipe_id" ON "recipe_bags"("recipe_id"); + +-- CreateIndex +Pragma writable_schema=1; +CREATE UNIQUE INDEX "sqlite_autoindex_recipe_bags_1" ON "recipe_bags"("recipe_id", "bag_id"); +Pragma writable_schema=0; + +-- CreateIndex +Pragma writable_schema=1; +CREATE UNIQUE INDEX "sqlite_autoindex_recipes_1" ON "recipes"("recipe_name"); +Pragma writable_schema=0; + +-- CreateIndex +Pragma writable_schema=1; +CREATE UNIQUE INDEX "sqlite_autoindex_roles_1" ON "roles"("role_name"); +Pragma writable_schema=0; + +-- CreateIndex +CREATE INDEX "idx_tiddlers_bag_id" ON "tiddlers"("bag_id"); + +-- CreateIndex +Pragma writable_schema=1; +CREATE UNIQUE INDEX "sqlite_autoindex_tiddlers_1" ON "tiddlers"("bag_id", "title"); +Pragma writable_schema=0; + +-- CreateIndex +Pragma writable_schema=1; +CREATE UNIQUE INDEX "sqlite_autoindex_users_1" ON "users"("username"); +Pragma writable_schema=0; + +-- CreateIndex +Pragma writable_schema=1; +CREATE UNIQUE INDEX "sqlite_autoindex_users_2" ON "users"("email"); +Pragma writable_schema=0; + diff --git a/plugins/tiddlywiki/multiwikiserver/prisma/schema.prisma.sql.meta b/plugins/tiddlywiki/multiwikiserver/prisma/schema.prisma.sql.meta new file mode 100644 index 00000000000..110fd08da90 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/prisma/schema.prisma.sql.meta @@ -0,0 +1,2 @@ +title: $:/plugins/tiddlywiki/multiwikiserver/prisma/schema.prisma.sql +type: application/sql \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/src/globals.d.ts b/plugins/tiddlywiki/multiwikiserver/src/globals.d.ts new file mode 100644 index 00000000000..487437dd69e --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/src/globals.d.ts @@ -0,0 +1,112 @@ +import { IncomingMessage as HTTPIncomingMessage, ServerResponse as HTTPServerResponse } from "http"; +import { Server as _Server, ServerManager } from "./server"; +import { Router } from "./router"; +import { Prisma, PrismaClient } from "@prisma/client"; +import { DefaultArgs } from "@prisma/client/runtime/library"; +import "./src/startup"; +import { SqlTiddlerStore } from "./store/sql-tiddler-store"; + +declare global { + const $tw: $TW; + + interface Wiki extends Record { + + } + interface Boot extends Record { + + } + interface Tiddler extends Record { + + } + + + interface $TW { + mws: { + store: SqlTiddlerStore; + serverManager: ServerManager; + router: Router; + connection: PrismaClient; + databasePath: string; + transaction: (type: T, callback: (store: SqlTiddlerStore) => Promise) => Promise; + } + } + + interface $TW { + loadTiddlersFromPath: any; + loadPluginFolder: any; + getLibraryItemSearchPaths: any; + wiki: Wiki; + utils: { + [x: string]: any; + decodeURIComponentSafe(str: string): string; + each(object: T[], callback: (value: T, index: number, object: T[]) => void): void; + each(object: Record, callback: (value: T, key: string, object: Record) => void): void; + parseJSONSafe(str: string, defaultJSON?: any): any; + parseNumber(string: string): number; + parseNumber(string: string | null): number | null; + }; + modules: { + [x: string]: any; + forEachModuleOfType: (moduleType: string, callback: (title: string, module: any) => void) => void; + } + boot: any; + config: any; + node: any; + hooks: any; + sjcl: any; + Wiki: { new(): Wiki }; + Tiddler: { new(fields: Record): Tiddler }; + + } + + + + + type HTTPVerb = "GET" | "OPTIONS" | "HEAD" | "PUT" | "POST" | "DELETE"; + + interface IncomingMessage extends HTTPIncomingMessage { + url: string; + method: string; + headers: { + //@ts-ignore + "set-cookie"?: string[]; + [x: string]: string | undefined; + } + } + + interface ServerResponse extends HTTPServerResponse { } + + type InnerState = Awaited>; + interface ServerState

extends InnerState { + params: string[] & { length: P }; + data: + F extends "json" ? any : + F extends "www-form-urlencoded" ? URLSearchParams : + F extends "buffer" ? Buffer : + F extends "stream" ? undefined : + string; + store: SqlTiddlerStore; + } + + type PrismaTxnClient = Omit, "$connect" | "$disconnect" | "$on" | "$transaction" | "$use" | "$extends"> + interface ServerRoute { + path: RegExp; + handler: ServerRouteHandler; + method?: string; + useACL?: boolean; + /** this is required if useACL is true */ + entityName?: string; + csrfDisable?: boolean; + bodyFormat?: ServerRouteBodyFormat; + } + type ServerRouteBodyFormat = "string" | "www-form-urlencoded" | "buffer" | "stream"; + interface ServerRouteHandler

{ + (this: ServerRoute, req: IncomingMessage, res: ServerResponse, state: ServerState): Promise; + } + +} + + +export { }; + + diff --git a/plugins/tiddlywiki/multiwikiserver/src/router.ts b/plugins/tiddlywiki/multiwikiserver/src/router.ts new file mode 100644 index 00000000000..8cccd4172d2 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/src/router.ts @@ -0,0 +1,692 @@ +/*\ +title: $:/plugins/tiddlywiki/multiwikiserver/router.js +type: application/javascript +module-type: library + +Serve tiddlers over http + +\*/ +"use strict"; +import { parse as parseQueryString } from "querystring"; + +import { ok } from "assert"; +import { deflateSync, gzipSync } from "zlib"; +import { createHash } from "crypto"; +import { SqlTiddlerStore } from "./store/sql-tiddler-store"; +import { defaultVariables, Server, ServerVariables } from "./server"; + +const acl: { + middleware: ( + request: IncomingMessage, + response: ServerResponse, + state: ServerState, + entityType: string, + permissionName: string + ) => Promise; +} = require("$:/plugins/tiddlywiki/multiwikiserver/routes/helpers/acl-middleware.js"); + +const FinishedResponse: unique symbol = Symbol("FinishedResponse"); +type FinishedResponse = { [FinishedResponse]: boolean } + +type RequestState = Awaited>; + +export class Router { + variables: ServerVariables; + requiredPlugins: string[]; + csrfDisable: boolean; + enableGzip: boolean; + enableBrowserCache: boolean; + store: SqlTiddlerStore; + wiki: Wiki; + authorizations: { + readers: string[]; + writers: string[]; + admin: string[] | undefined; + }; + routes: ServerRoute[] = []; + authenticators: any[] = []; + constructor(options: { + variables: Partial + wiki: any, + store: SqlTiddlerStore + }) { + this.store = options.store; + this.wiki = options.wiki; + + // delete falsy keys so that we can use the default values + for (const key in options.variables) { + if (!(options.variables as any)[key]) { + delete (options.variables as any)[key]; + } + } + // Initialise the variables (Object.assign scoops peroperties up from left to right) + this.variables = Object.assign({}, defaultVariables, options.variables ?? {}); + // Setup the default required plugins + this.requiredPlugins = this.get("required-plugins").split(','); + // Initialise CSRF + this.csrfDisable = this.get("csrf-disable") === "yes"; + // Initialize Gzip compression + this.enableGzip = this.get("gzip") === "yes"; + // Initialize browser-caching + this.enableBrowserCache = this.get("use-browser-cache") === "yes"; + + // Warn if required plugins are missing + const missing = this.requiredPlugins.filter(title => { + return !$tw.wiki.getTiddler(title); + }); + + if (missing.length > 0) { + const error = "Warning: Plugin(s) required for client-server operation are missing.\n" + + "\"" + missing.join("\", \"") + "\""; + $tw.utils.warning(error); + } + + // Initialise authorization + let authorizedUserName; + if (this.get("username") && this.get("password")) { + authorizedUserName = this.get("username") || ""; //redundant for type checker + } else if (this.get("credentials")) { + authorizedUserName = "(authenticated)"; + } else { + authorizedUserName = "(anon)"; + } + this.authorizations = { + readers: (this.get("readers") || authorizedUserName).split(",").map($tw.utils.trim), + writers: (this.get("writers") || authorizedUserName).split(",").map($tw.utils.trim), + /** @type {string[] | undefined} */ + admin: undefined + } + if (this.get("admin") || authorizedUserName !== "(anon)") { + this.authorizations["admin"] = (this.get("admin") || authorizedUserName).split(',').map($tw.utils.trim) + } + // Load and initialise authenticators + $tw.modules.forEachModuleOfType("authenticator", (title, authenticatorDefinition) => { + this.addAuthenticator(authenticatorDefinition.AuthenticatorClass, title); + }); + // Load route handlers + $tw.modules.forEachModuleOfType("mws-route", (title, routeDefinition) => { + this.addRoute(routeDefinition, title); + }); + + } + + get(key: K) { + return this.variables[key]; + } + + addRoute(route: any, title: string) { + if (!route) throw new Error(`Route ${title} is not defined`); + + if (this.methodMappings[route.method as keyof Router["methodMappings"]] === undefined) { + throw new Error(`Route ${title} does not have a valid method, expected one of ${Object.keys(this.methodMappings).join(", ")}`); + } + + if (!route.path) { + throw new Error(`Route ${title} does not have a path`); + } + + if (route.useACL) { + + const permissionName = this.methodACLPermMappings[route.method as keyof Router["methodACLPermMappings"]]; + + if (!route.entityName) { + throw new Error(`Route ${title} is configured to use ACL middleware but does not specify an entityName`); + } + + if (!permissionName) { + throw new Error(`Route ${title} is configured to use ACL middleware but the route method does not support ACL`); + } + + } + + this.routes.push(route); + + } + + addAuthenticator(AuthenticatorClass: any, title: string) { + // Instantiate and initialise the authenticator + var authenticator = new AuthenticatorClass(this), + result = authenticator.init(); + if (typeof result === "string") { + $tw.utils.error("Error: " + result); + } else if (result) { + // Only use the authenticator if it initialised successfully + this.authenticators.push(authenticator); + } + } + + serverManagerRequestHandler( + server: Server, + request: IncomingMessage, + response: ServerResponse + ) { + + // $tw.mws.connection.$transaction(async (prisma) => { + // the database transaction gets committed when the promise resolves + // and rolled back if it rejects + this.routeRequest(server, request, response).catch(console.error); + // },{maxWait: 30000, timeout: 20000}).catch(console.error); + + } + + async routeRequest( + server: Server, + request: IncomingMessage, + response: ServerResponse, + options?: any, + ): Promise { + // returning the response object in order to make sure we call response.end() because the route + // handler should never resolve the promise until it finishes writing the response. + // After that, the database connection gets closed or returned to the pool. + // An error should be thrown if the connection has unfinished business when end is called, + // because that probably means we forgot to await something. + + if (this.get("debug-level") !== "none") { + var start = $tw.utils.timer(); + response.on("finish", function () { + console.log("Response time:", request.method, request.url, $tw.utils.timer() - start); + }); + } + + // Compose the state object + const state = await this.makeRequestState(server, request, response, options); + + // Authorize with the authenticated username + if (!this.isAuthorized(state.authorizationType, state.authenticatedUsername)) { + if (!response.headersSent) + response.writeHead(403, "'" + state.authenticatedUsername + "' is not authorized"); + return state.end(); + } + + // Find the route that matches this path + var route = this.findMatchingRoute(request, state); + + // Return a 404 if we didn't find a route + if (!route) { + if (!response.headersSent) + response.writeHead(404); + return state.end(); + } + + // If the route is configured to use ACL middleware, check that the user has permission + if (route.useACL) { + ok(route.entityName); + const permissionName = this.methodACLPermMappings[route.method as keyof Router["methodACLPermMappings"]]; + await acl.middleware(request, response, state, route.entityName, permissionName) + } + + // Optionally output debug info + if (this.get("debug-level") !== "none") { + console.log("Request path:", JSON.stringify(state.urlInfo)); + console.log("Request headers:", JSON.stringify(request.headers)); + console.log("authenticatedUsername:", state.authenticatedUsername); + } + + // If this is a write, check for the CSRF header unless globally disabled, or disabled for this route + if (!this.csrfDisable + && !route.csrfDisable + && state.authorizationType === "writers" + && request.headers["x-requested-with"] !== "TiddlyWiki" + && !response.headersSent + ) { + if (!response.headersSent) + response.writeHead(403, "'X-Requested-With' header required to login"); + return state.end(); + } + // if we've sent headers already, it was a denied request + if (response.headersSent) return state.end(); + // Receive the request body if necessary and hand off to the route handler + if (route.bodyFormat === "stream" || request.method === "GET" || request.method === "HEAD") { + // Let the route handle the request stream itself + await route.handler(request, response, state); + return state.end(); + } else if (route.bodyFormat === "string" + || route.bodyFormat === "www-form-urlencoded" + || !route.bodyFormat) { + // Set the encoding for the incoming request + request.setEncoding("utf8"); + await this.readBodyString(request, route, state); + await route.handler(request, response, state); + return state.end(); + } else if (route.bodyFormat === "buffer") { + await this.readBodyBuffer(request, state); + await route.handler(request, response, state); + return state.end(); + } else { + $tw.utils.warning(`Invalid bodyFormat ${route.bodyFormat} in route ${route.method} ${route.path.source}`); + response.writeHead(500); + return state.end(); + } + + } + + findMatchingRoute(request: IncomingMessage, state: RequestState) { + for (var t = 0; t < this.routes.length; t++) { + var potentialRoute = this.routes[t], + pathRegExp = potentialRoute.path, + pathname = state.urlInfo.pathname, + match; + if (state.pathPrefix) { + if (pathname.substr(0, state.pathPrefix.length) === state.pathPrefix) { + pathname = pathname.substr(state.pathPrefix.length) || "/"; + match = pathRegExp.exec(pathname); + } else { + match = null; + } + } else { + match = pathRegExp.exec(pathname); + } + // Allow POST as a synonym for PUT because HTML doesn't allow PUT forms + if (match && (request.method === potentialRoute.method || (request.method === "POST" && potentialRoute.method === "PUT"))) { + for (var p = 1; p < match.length; p++) { + state.params.push(match[p]); + } + return potentialRoute; + } + } + return null; + }; + + private async readBodyString(request: IncomingMessage, route: ServerRoute, state: RequestState) { + await (new Promise((resolve) => { + let data = ""; + request.on("data", function (chunk) { + data += chunk.toString(); + }); + request.on("end", function () { + if (route.bodyFormat === "www-form-urlencoded") { + state.data = new URLSearchParams(data); + } else { + state.data = data; + } + resolve(); + }); + })); + } + + private async readBodyBuffer(request: IncomingMessage, state: RequestState) { + await (new Promise((resolve) => { + const data: Buffer[] = []; + request.on("data", function (chunk) { + data.push(chunk); + }); + request.on("end", function () { + state.data = Buffer.concat(data); + resolve(); + }); + })); + } + + async authenticateUser(request: IncomingMessage, response: ServerResponse) { + const { session: session_id } = this.parseCookieString(request.headers.cookie); + if (!session_id) { + return null; + } + // get user info + const user = await this.store.sql.findUserBySessionId(session_id); + if (!user) { + return null; + } + //@ts-expect-error because password is not optional + delete user.password; + const userRole = await this.store.sql.getUserRoles(user.user_id); + + return { + ...user, + isAdmin: userRole?.role_name?.toLowerCase() === 'admin', + sessionId: session_id, + password: undefined, // for typing + }; + }; + + isAuthorized(authorizationType: RequestState["authorizationType"], username: string | undefined) { + var principals = this.authorizations[authorizationType] || []; + return principals.indexOf("(anon)") !== -1 + || (username && (principals.indexOf("(authenticated)") !== -1 + || principals.indexOf(username) !== -1)); + } + + requestAuthentication(response: ServerResponse) { + if (!response.headersSent) { + response.writeHead(401, { + 'WWW-Authenticate': 'Basic realm="Secure Area"' + }); + response.end('Authentication required.'); + } + }; + + // Check if the anonymous IO configuration is set to allow both reads and writes + getAnonymousAccessConfig() { + const allowReadsTiddler = this.store.adminWiki.getTiddlerText("$:/config/MultiWikiServer/AllowAnonymousReads", "undefined"); + const allowWritesTiddler = this.store.adminWiki.getTiddlerText("$:/config/MultiWikiServer/AllowAnonymousWrites", "undefined"); + const showAnonymousAccessModal = this.store.adminWiki.getTiddlerText("$:/config/MultiWikiServer/ShowAnonymousAccessModal", "undefined"); + + return { + allowReads: allowReadsTiddler === "yes", + allowWrites: allowWritesTiddler === "yes", + isEnabled: allowReadsTiddler !== "undefined" && allowWritesTiddler !== "undefined", + showAnonConfig: showAnonymousAccessModal === "yes" + }; + } + + parseCookieString(cookieString: string | null | undefined) { + const cookies: Record = {}; + if (typeof cookieString !== 'string') return cookies; + + cookieString.split(';').forEach(cookie => { + const parts = cookie.split('='); + if (parts.length >= 2) { + const key = parts[0].trim(); + const value = parts.slice(1).join('=').trim(); + cookies[key] = decodeURIComponent(value); + } + }); + + return cookies; + } + + async makeRequestState(server: Server, request: IncomingMessage, response: ServerResponse, options: { authorizationType?: any; pathPrefix?: any; } = {}) { + + // Authenticate the user + const authenticatedUser = await this.authenticateUser(request, response); + const authenticatedUsername = authenticatedUser?.username; + + // Get the principals authorized to access this resource + const authorizationType: "readers" | "writers" = options.authorizationType + || this.methodMappings[request.method as keyof Router["methodMappings"]] + || "readers"; + + var { allowReads, allowWrites, isEnabled, showAnonConfig } = this.getAnonymousAccessConfig(); + + // // this is slightly hacky, but we're sending the connection off through the various channels. + // // I mean, we could just pass the connection to the handler, but that would be too easy. + // const { prisma, finish } = await new Promise<{ + // prisma: PrismaTxnClient, + // finish: (value: void | PromiseLike) => void + // }>((resolveInit) => { + // $tw.mws.connection.$transaction(async (prisma) => { + // await new Promise((finish) => { + // resolveInit({ prisma, finish }); + // }); + // }); + // }); + + const urlInfo = new URL(request.url, server.origin()!); + return { + authorizationType, + pathPrefix: options.pathPrefix || server.pathPrefix || "", + store: new SqlTiddlerStore({ + attachmentStore: this.store.attachmentStore, + adminWiki: this.store.adminWiki, + prisma: $tw.mws.connection, + }), + urlInfo, + queryParameters: urlInfo.searchParams, + sendResponse: this.sendResponse.bind(this, request, response), + redirect: this.redirect.bind(this, request, response), + streamMultipartData: this.streamMultipartData.bind(this, request), + makeTiddlerEtag: this.makeTiddlerEtag.bind(this), + authenticatedUser, + authenticatedUsername, + anonAccessConfigured: isEnabled, + allowAnon: isEnabled && (request.method === 'GET' ? allowReads : allowWrites), + allowAnonReads: allowReads, + allowAnonWrites: allowWrites, + showAnonConfig: !!authenticatedUser?.isAdmin && showAnonConfig, + firstGuestUser: !authenticatedUser && (await this.store.sql.listUsers()).length === 0, + data: undefined as any, + params: [] as string[], + end: () => { + // if no response was sent, send a 500 + if (!response.headersSent) { + response.writeHead(500); + response.end("Internal server error"); + $tw.utils.error("Response not sent " + request.method + " " + request.url); + } else { + response.write = () => { + throw new Error("Cannot write to response after it has been ended"); + } + response.end(); + } + // + return { [FinishedResponse]: true }; + } + }; + + } + methodMappings = { + "GET": "readers", + "OPTIONS": "readers", + "HEAD": "readers", + "PUT": "writers", + "POST": "writers", + "DELETE": "writers" + }; + + methodACLPermMappings = { + "GET": "READ", + "OPTIONS": "READ", + "HEAD": "READ", + "PUT": "WRITE", + "POST": "WRITE", + "DELETE": "WRITE" + } + + + /** + * 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 + * response will be transmitted and the browser will use the cached data. + * Only requests with status code 200 are considdered for caching. + * request: request instance passed to the handler + * response: response instance passed to the handler + * statusCode: stauts code to send to the browser + * headers: response headers (they will be augmented with an `Etag` header) + * data: the data to send (passed to the end method of the response instance) + * encoding: the encoding of the data to send (passed to the end method of the response instance) + * @this {Server} + * @param {IncomingMessage} request + * @param {ServerResponse} response + * @param {number} statusCode + * @param {Record} headers + * @param {string | Buffer} data + * @param {BufferEncoding} encoding + */ + sendResponse(request: IncomingMessage, response: ServerResponse, statusCode: number, headers: Record, data?: Buffer): void; + sendResponse(request: IncomingMessage, response: ServerResponse, statusCode: number, headers: Record, data: string, encoding: BufferEncoding): void; + sendResponse(request: IncomingMessage, response: ServerResponse, statusCode: number, headers: Record, data?: string | Buffer, encoding?: BufferEncoding) { + if (typeof data === "string" && encoding === undefined) { + $tw.utils.error("Missing encoding for string data, we assume utf8"); + encoding = "utf8"; + } + if (this.enableBrowserCache && (statusCode == 200)) { + var hash = createHash('md5'); + // Put everything into the hash that could change and invalidate the data that + // the browser already stored. The headers the data and the encoding. + if (data !== undefined) hash.update(data); + hash.update(JSON.stringify(headers)); + if (encoding) { + hash.update(encoding); + } + var contentDigest = hash.digest("hex"); + // RFC 7232 section 2.3 mandates for the etag to be enclosed in quotes + headers["Etag"] = '"' + contentDigest + '"'; + headers["Cache-Control"] = "max-age=0, must-revalidate"; + // Check if any of the hashes contained within the if-none-match header + // matches the current hash. + // If one matches, do not send the data but tell the browser to use the + // cached data. + // We do not implement "*" as it makes no sense here. + var ifNoneMatch = request.headers["if-none-match"]; + if (ifNoneMatch) { + var matchParts = ifNoneMatch.split(",").map(function (/** @type {string} */ etag: string) { + return etag.replace(/^[ "]+|[ "]+$/g, ""); + }); + if (matchParts.indexOf(contentDigest) != -1) { + response.writeHead(304, headers); + response.end(); + return; + } + } + } + /* + If the gzip=yes is set, check if the user agent permits compression. If so, + compress our response if the raw data is bigger than 2k. Compressing less + data is inefficient. Note that we use the synchronous functions from zlib + to stay in the imperative style. The current `Server` doesn't depend on + this, and we may just as well use the async versions. + */ + if (this.enableGzip && data && (data.length > 2048)) { + var acceptEncoding = request.headers["accept-encoding"] || ""; + if (/\bdeflate\b/.test(acceptEncoding)) { + headers["Content-Encoding"] = "deflate"; + data = deflateSync(data); + } else if (/\bgzip\b/.test(acceptEncoding)) { + headers["Content-Encoding"] = "gzip"; + data = gzipSync(data); + } + } + if (!response.headersSent) { + response.writeHead(statusCode, headers); + if (typeof data === "string") + response.end(data, encoding ?? "utf8"); + else + response.end(data); + } + } + /** + * @this {Server} + * @param {IncomingMessage} request + * @param {ServerResponse} response + * @param {number} statusCode + * @param {string} location + */ + redirect(request: IncomingMessage, response: ServerResponse, statusCode: number, location: string) { + response.setHeader("Location", location); + response.statusCode = statusCode; + response.end() + } + + /* + Options include: + cbPartStart(headers,name,filename) - invoked when a file starts being received + cbPartChunk(chunk) - invoked when a chunk of a file is received + cbPartEnd() - invoked when a file finishes being received + cbFinished(err) - invoked when the all the form data has been processed + */ + /** + * + * @param {import("http").IncomingMessage} request + * @param {Object} options + * @param {(headers: Object, name: string | null, filename: string | null) => void} options.cbPartStart + * @param {(chunk: Buffer) => void} options.cbPartChunk + * @param {() => void} options.cbPartEnd + * @param {(err: string | null) => void} options.cbFinished + */ + streamMultipartData(request: import("http").IncomingMessage, options: { cbPartStart: (headers: object, name: string | null, filename: string | null) => void; cbPartChunk: (chunk: Buffer) => void; cbPartEnd: () => void; cbFinished: (err: string | null) => void; }) { + // Check that the Content-Type is multipart/form-data + const contentType = request.headers['content-type']; + if (!contentType?.startsWith("multipart/form-data")) { + return options.cbFinished("Expected multipart/form-data content type"); + } + // Extract the boundary string from the Content-Type header + const boundaryMatch = contentType.match(/boundary=(.+)$/); + if (!boundaryMatch) { + return options.cbFinished("Missing boundary in multipart/form-data"); + } + const boundary = boundaryMatch[1]; + const boundaryBuffer = Buffer.from("--" + boundary); + // Initialise + let buffer = Buffer.alloc(0); + let processingPart = false; + // Process incoming chunks + request.on("data", (chunk) => { + // Accumulate the incoming data + buffer = Buffer.concat([buffer, chunk]); + // Loop through any parts within the current buffer + while (true) { + if (!processingPart) { + // If we're not processing a part then we try to find a boundary marker + const boundaryIndex = buffer.indexOf(boundaryBuffer); + if (boundaryIndex === -1) { + // Haven't reached the boundary marker yet, so we should wait for more data + break; + } + // Look for the end of the headers + const endOfHeaders = buffer.indexOf("\r\n\r\n", boundaryIndex + boundaryBuffer.length); + if (endOfHeaders === -1) { + // Haven't reached the end of the headers, so we should wait for more data + break; + } + // Extract and parse headers + const headersPart = Uint8Array.prototype.slice.call(buffer, boundaryIndex + boundaryBuffer.length, endOfHeaders).toString(); + /** @type {Record} */ + const currentHeaders: Record = {}; + headersPart.split("\r\n").forEach(headerLine => { + const [key, value] = headerLine.split(": "); + currentHeaders[key.toLowerCase()] = value; + }); + // Parse the content disposition header + const contentDisposition = { + name: null as string | null, + filename: null as string | null + }; + if (currentHeaders["content-disposition"]) { + // Split the content-disposition header into semicolon-delimited parts + const parts = currentHeaders["content-disposition"].split(";").map(part => part.trim()); + // Iterate over each part to extract name and filename if they exist + parts.forEach(part => { + if (part.startsWith("name=")) { + // Remove "name=" and trim quotes + contentDisposition.name = part.substring(6, part.length - 1); + } else if (part.startsWith("filename=")) { + // Remove "filename=" and trim quotes + contentDisposition.filename = part.substring(10, part.length - 1); + } + }); + } + processingPart = true; + options.cbPartStart(currentHeaders, contentDisposition.name, contentDisposition.filename); + // Slice the buffer to the next part + buffer = Buffer.from(buffer.slice(endOfHeaders + 4)); + } else { + const boundaryIndex = buffer.indexOf(boundaryBuffer); + if (boundaryIndex >= 0) { + // Return the part up to the boundary minus the terminating LF CR + options.cbPartChunk(Buffer.from(buffer.slice(0, boundaryIndex - 2))); + options.cbPartEnd(); + processingPart = false; + buffer = Buffer.from(buffer.slice(boundaryIndex)); + } else { + // Return the rest of the buffer + options.cbPartChunk(buffer); + // Reset the buffer and wait for more data + buffer = Buffer.alloc(0); + break; + } + } + } + }); + // All done + request.on("end", () => { + options.cbFinished(null); + }); + } + + /** + Make an etag. + @param {Object} options + @param {string} options.bag_name + @param {string} options.tiddler_id + */ + makeTiddlerEtag(options: { bag_name: string; tiddler_id: number; }) { + if (options.bag_name || options.tiddler_id) { + return "\"tiddler:" + options.bag_name + "/" + options.tiddler_id + "\""; + } else { + throw "Missing bag_name or tiddler_id"; + } + } + + +} + diff --git a/plugins/tiddlywiki/multiwikiserver/src/server.ts b/plugins/tiddlywiki/multiwikiserver/src/server.ts new file mode 100644 index 00000000000..0b78ee2faa5 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/src/server.ts @@ -0,0 +1,238 @@ +/*\ +title: $:/plugins/tiddlywiki/multiwikiserver/server.js +type: application/javascript +module-type: library + +Serve tiddlers over http + +\*/ +"use strict"; +import { ok } from "assert"; +import { readFileSync } from "fs"; +import * as http from "http"; +import * as https from "https"; +import * as net from "net"; +import { Router } from "./router"; + + + +declare global { + + interface IncomingMessage extends http.IncomingMessage { + url: string; + method: string; + } + interface ServerResponse extends http.ServerResponse { } +} + + +export interface ServerVariables { + + port: string + host: string + "required-plugins": string + "root-tiddler": string + "root-render-type": string + "root-serve-type": string + "tiddler-render-type": string + "tiddler-render-template": string + "system-tiddler-render-type": string + "system-tiddler-render-template": string + "use-browser-cache"?: "yes" + "debug-level": "none" + "gzip"?: "yes" + "csrf-disable"?: "yes" + "path-prefix"?: string + /** TLS Private Key file path */ + "tls-key-file"?: string + /** TLS Public Cert file path */ + "tls-cert-file"?: string + /** TLS Private Key passphrase */ + "tls-passphrase"?: string + /** Server name, mostly for 403 errors */ + "server-name"?: string + /** the expected origin header */ + "origin"?: string + /** a single username */ + username?: string + /** a single password */ + password?: string + /** */ + credentials?: string + /** comma-delimited list of reader usernames */ + readers?: string + /** comma-delimited list of writer usernames */ + writers?: string + /** comma-delimited list of admin usernames */ + admin?: string +} + + +export const defaultVariables = { + port: "8080", + host: "127.0.0.1", + "required-plugins": "$:/plugins/tiddlywiki/filesystem,$:/plugins/tiddlywiki/tiddlyweb", + "root-tiddler": "$:/core/save/all", + "root-render-type": "text/plain", + "root-serve-type": "text/html", + "tiddler-render-type": "text/html", + "tiddler-render-template": "$:/core/templates/server/static.tiddler.html", + "system-tiddler-render-type": "text/plain", + "system-tiddler-render-template": "$:/core/templates/wikified-tiddler", + "debug-level": "none", +} as const; + + +interface ServerDefinition { + address: string, + port: number, + path: string, + /** Path to the key file on the file system */ + tlsKeyFile?: string; + /** Path to the cert file on the file system */ + tlsCertFile?: string; + tlsPass?: string; +} + + +export class ServerManager { + servers: Map = new Map(); + constructor() { + // Stop listening when we get the "th-quit" hook + $tw.hooks.addHook("th-quit", () => { + this.isClosing = true; + this.servers.forEach(server => server.close()); + }); + } + + isClosing = false; + + onListen = (server: Server) => { + // Log listening details + $tw.utils.log("Serving on " + server.origin(), "brown/orange"); + }; + onError = (server: Server, err: Error) => { + $tw.utils.warning(`Error serving on ${server.origin()}: ${err.message}`); + }; + onClose = (server: Server) => { + if (this.isClosing) { + $tw.utils.log(`Server closed: ${server.origin()}`, "green"); + } else { + $tw.utils.warning("Server closed unexpectedly: " + server.origin(), "red"); + server.listen(); // Attempt to restart the listener + } + }; + onRequest = (server: Server, request: IncomingMessage, response: ServerResponse) => { + $tw.utils.warning(new Error("The mws-listen command has not been run yet.")); + response.writeHead(500).end("Server is not started yet."); + }; + /** require and create the router, attaching it to the server manager */ + createRouter(params: ServerVariables) { + $tw.mws.router = new Router({ wiki: $tw.wiki, variables: params, store: $tw.mws.store }); + this.onRequest = $tw.mws.router.serverManagerRequestHandler.bind($tw.mws.router); + } + createServer(options: ServerDefinition, listening?: () => void) { + const server2 = this.mapServer(options, listening); + this.servers.set(options, server2); + server2.listen(); + return server2; + } + listenCommand(params: Partial, listening?: () => void) { + // Handle defaults for port and host + return this.createServer({ + address: params.host || defaultVariables.host, + port: +(params.port ?? "") || +(process.env.PORT ?? "") || +defaultVariables.port, + path: params["path-prefix"] || "", + tlsKeyFile: params["tls-key-file"], + tlsCertFile: params["tls-cert-file"], + tlsPass: params["tls-passphrase"], + }, listening) + } + + private mapServer(server: ServerDefinition, listening?: () => void) { + const { address, port, path, tlsKeyFile, tlsCertFile, tlsPass } = server; + + if (tlsKeyFile || tlsCertFile || tlsPass) { + if ((!tlsKeyFile || !tlsCertFile)) { + throw new Error("TLS Key and Cert must be provided together, TLS Passphrase is optional, " + + "but if it is provided then TLS Key and Cert are also required."); + } + const server2 = new Server( + https.createServer({ + key: readFileSync(tlsKeyFile), + cert: readFileSync(tlsCertFile), + passphrase: tlsPass, + }), + "https", address, port, path, + () => { this.onListen(server2); listening?.(); }, + this.onError, + this.onClose, + this.onRequest, + ); + return server2; + } else { + const server2 = new Server( + http.createServer(), + "http", address, port, path, + () => { this.onListen(server2); listening?.(); }, + this.onError, + this.onClose, + this.onRequest, + ); + return server2; + } + } +} + +export class Server { + constructor( + public readonly server: S, + public readonly protocol: "http" | "https", + public readonly host: string, + public readonly port: number, + public readonly pathPrefix: string, + onListen: (server: Server) => void, + onError: (server: Server, err: Error) => void, + onClose: (server: Server) => void, + onRequest: (server: Server, req: IncomingMessage, res: ServerResponse) => void, + ) { + ok(this.protocol === "http" || this.protocol === "https", + "Expected protocol to be http or https"); + ok(typeof this.host === "string", + "Expected host to be a string"); + ok(typeof this.port === "number" && this.port > 0, + "Expected port to be defined"); + ok(!this.pathPrefix || this.pathPrefix.startsWith("/") && !this.pathPrefix.endsWith("/"), + "Expected pathPrefix to start with a / but NOT end with one"); + + this.server.on("listening", onListen.bind(null, this)); + this.server.on("error", onError.bind(null, this)); + this.server.on("close", onClose.bind(null, this)); + this.server.on("request", onRequest.bind(null, this)); + } + address() { + const address = this.server.address(); + if (!address) { return null; } + if (typeof address === "string") { + throw new Error("Expected server.address() to return an object"); + } + return { + protocol: this.protocol, + family: address.family, + address: address.address, + port: address.port, + } + } + origin() { + const address = this.address(); + if (!address) { return null; } + const host = address.family === "IPv6" ? "[" + address.address + "]" : address.address + return `${address.protocol}://${host}:${address.port}`; + } + listen(): void { + this.server.listen(this.port, this.host); + } + close() { + this.server.close(); + } +} diff --git a/plugins/tiddlywiki/multiwikiserver/src/store/attachments.ts b/plugins/tiddlywiki/multiwikiserver/src/store/attachments.ts new file mode 100644 index 00000000000..bced5a884b6 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/src/store/attachments.ts @@ -0,0 +1,176 @@ +/*\ +title: $:/plugins/tiddlywiki/multiwikiserver/store/attachments.js +type: application/javascript +module-type: library + +Class to handle the attachments in the filing system + +The store folder looks like this: + +store/ + inbox/ - files that are in the process of being uploaded via a multipart form upload + 202402282125432742/ + 0 + 1 + ... + ... + files/ - files that are the text content of large tiddlers + b7def178-79c4-4d88-b7a4-39763014a58b/ + data.jpg - the extension is provided for convenience when directly inspecting the file system + meta.json - contains: + { + "filename": "data.jpg", + "type": "video/mp4", + "uploaded": "2024021821224823" + } + database.sql - The database file (managed by sql-tiddler-database.js) + +\*/ + + +"use strict"; +/* +Class to handle an attachment store. Options include: + +storePath - path to the store +*/ +export class AttachmentStore { + storePath: string; + constructor(options: { storePath: string; }) { + this.storePath = options.storePath; + } + + /* + Check if an attachment name is valid + */ + isValidAttachmentName(attachment_name: string) { + const re = new RegExp("^[a-f0-9]{64}$"); + return re.test(attachment_name); + } + + /* + Saves an attachment to a file. Options include: + + text: text content (may be binary) + type: MIME type of content + reference: reference to use for debugging + _canonical_uri: canonical uri of the content + */ + saveAttachment(options: { text: string | Buffer; type: string; reference?: string; _canonical_uri: string; }) { + const path = require("path"), fs = require("fs"); + // Compute the content hash for naming the attachment + const contentHash = $tw.sjcl.codec.hex.fromBits($tw.sjcl.hash.sha256.hash(options.text)).slice(0, 64).toString(); + // Choose the best file extension for the attachment given its type + const contentTypeInfo = $tw.config.contentTypeInfo[options.type] || $tw.config.contentTypeInfo["application/octet-stream"]; + // Creat the attachment directory + const attachmentPath = path.resolve(this.storePath, "files", contentHash); + $tw.utils.createDirectory(attachmentPath); + // Save the data file + const dataFilename = "data" + contentTypeInfo.extension; + fs.writeFileSync(path.resolve(attachmentPath, dataFilename), options.text, contentTypeInfo.encoding); + // Save the meta.json file + fs.writeFileSync(path.resolve(attachmentPath, "meta.json"), JSON.stringify({ + _canonical_uri: options._canonical_uri, + created: $tw.utils.stringifyDate(new Date()), + modified: $tw.utils.stringifyDate(new Date()), + contentHash: contentHash, + filename: dataFilename, + type: options.type + }, null, 4)); + return contentHash; + } + + /* + Adopts an attachment file into the store + */ + adoptAttachment(incomingFilepath: string, type: string, hash: string, _canonical_uri: string) { + const path = require("path"), fs = require("fs"); + // Choose the best file extension for the attachment given its type + const contentTypeInfo = $tw.config.contentTypeInfo[type] || $tw.config.contentTypeInfo["application/octet-stream"]; + // Creat the attachment directory + const attachmentPath = path.resolve(this.storePath, "files", hash); + $tw.utils.createDirectory(attachmentPath); + // Rename the data file + const dataFilename = "data" + contentTypeInfo.extension, + dataFilepath = path.resolve(attachmentPath, dataFilename); + fs.renameSync(incomingFilepath, dataFilepath); + // Save the meta.json file + fs.writeFileSync(path.resolve(attachmentPath, "meta.json"), JSON.stringify({ + _canonical_uri: _canonical_uri, + created: $tw.utils.stringifyDate(new Date()), + modified: $tw.utils.stringifyDate(new Date()), + contentHash: hash, + filename: dataFilename, + type: type + }, null, 4)); + return hash; + } + + /* + Get an attachment ready to stream. Returns null if there is an error or: + stream: filestream of file + type: type of file + */ + getAttachmentStream(attachment_name: string) { + const path = require("path"), fs = require("fs"); + // Check the attachment name + if (this.isValidAttachmentName(attachment_name)) { + // Construct the path to the attachment directory + const attachmentPath = path.resolve(this.storePath, "files", attachment_name); + // Read the meta.json file + const metaJsonPath = path.resolve(attachmentPath, "meta.json"); + if (fs.existsSync(metaJsonPath) && fs.statSync(metaJsonPath).isFile()) { + const meta = $tw.utils.parseJSONSafe(fs.readFileSync(metaJsonPath, "utf8"), function () { return null; }); + if (meta) { + const dataFilepath = path.resolve(attachmentPath, meta.filename); + // Check if the data file exists + if (fs.existsSync(dataFilepath) && fs.statSync(dataFilepath).isFile()) { + // Stream the file + return { + stream: fs.createReadStream(dataFilepath), + type: meta.type + }; + } + } + } + } + // An error occured + return null; + } + + /* + Get the size of an attachment file given the contentHash. + Returns the size in bytes, or null if the file doesn't exist. + */ + getAttachmentFileSize(contentHash: string) { + const path = require("path"), fs = require("fs"); + // Construct the path to the attachment directory + const attachmentPath = path.resolve(this.storePath, "files", contentHash); + // Read the meta.json file + const metaJsonPath = path.resolve(attachmentPath, "meta.json"); + if (fs.existsSync(metaJsonPath) && fs.statSync(metaJsonPath).isFile()) { + const meta = $tw.utils.parseJSONSafe(fs.readFileSync(metaJsonPath, "utf8"), function () { return null; }); + if (meta) { + const dataFilepath = path.resolve(attachmentPath, meta.filename); + // Check if the data file exists and return its size + if (fs.existsSync(dataFilepath) && fs.statSync(dataFilepath).isFile()) { + return fs.statSync(dataFilepath).size; + } + } + } + // Return null if the file doesn't exist or there was an error + return null; + } + + getAttachmentMetadata(attachmentBlob: string) { + const path = require("path"), fs = require("fs"); + const attachmentPath = path.resolve(this.storePath, "files", attachmentBlob); + const metaJsonPath = path.resolve(attachmentPath, "meta.json"); + if (fs.existsSync(metaJsonPath)) { + const metadata = JSON.parse(fs.readFileSync(metaJsonPath, "utf8")); + return metadata; + } + return null; + } +} + diff --git a/plugins/tiddlywiki/multiwikiserver/src/store/sql-tiddler-database.ts b/plugins/tiddlywiki/multiwikiserver/src/store/sql-tiddler-database.ts new file mode 100644 index 00000000000..534768dd1cc --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/src/store/sql-tiddler-database.ts @@ -0,0 +1,1940 @@ +/*\ +title: $:/plugins/tiddlywiki/multiwikiserver/store/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. +Validation is for the most part left to the caller + +\*/ +"use strict"; +// import { SqlEngine } from "$:/plugins/tiddlywiki/multiwikiserver/store/sql-engine"; +import { PrismaClient } from "@prisma/client"; +import { ok } from "assert"; +const TYPEOF_ENUM = typeof ""; +type TYPEOF_ENUM = typeof TYPEOF_ENUM | "null" | "nully"; +export function okType(value: any, type: "string", msg?: string): asserts value is string; +export function okType(value: any, type: "number", msg?: string): asserts value is number; +export function okType(value: any, type: "bigint", msg?: string): asserts value is bigint; +export function okType(value: any, type: "boolean", msg?: string): asserts value is boolean; +export function okType(value: any, type: "symbol", msg?: string): asserts value is symbol; +export function okType(value: any, type: "object", msg?: string): asserts value is object; +export function okType(value: any, type: "function", msg?: string): asserts value is Function; +export function okType(value: any, type: "null", msg?: string): asserts value is null; +export function okType(value: any, type: "nully", msg?: string): asserts value is null | undefined; +export function okType(value: any, type: "undefined", msg?: string): asserts value is undefined; +export function okType(value: any, type: TYPEOF_ENUM, msg?: string) { + switch (type) { + case "null": ok(value === null, msg); break; + case "nully": ok(value === null || value === undefined, msg); break; + default: ok(typeof value === type, msg); + } +} +export function okTypeTruthy(value: any, type: "string", msg?: string): asserts value is string & {}; +export function okTypeTruthy(value: any, type: "number", msg?: string): asserts value is number & {}; +export function okTypeTruthy(value: any, type: "bigint", msg?: string): asserts value is bigint & {}; +export function okTypeTruthy(value: any, type: "boolean", msg?: string): asserts value is boolean & {}; +export function okTypeTruthy(value: any, type: "symbol", msg?: string): asserts value is symbol & {}; +export function okTypeTruthy(value: any, type: "object", msg?: string): asserts value is object & {}; +export function okTypeTruthy(value: any, type: "function", msg?: string): asserts value is Function & {}; +export function okTypeTruthy(value: any, type: TYPEOF_ENUM, msg?: string) { + switch (type) { + case "null": ok(value === null, msg); break; + case "nully": ok(value === null || value === undefined, msg); break; + default: ok(typeof value === type, msg); + } +} + +export function okEntityType(value: any): asserts value is EntityType { + ok(value === "bag" || value === "recipe", "Invalid entity type: " + value); +} + +type EntityType = "bag" | "recipe"; +type TiddlerFields = Record +/* +Create a tiddler store. Options include: + +databasePath - path to the database file (can be ":memory:" to get a temporary database) +engine - wasm | better +*/ +export class SqlTiddlerDatabase { + + constructor(public engine: PrismaTxnClient) { + + } + + async listBags() { + return await this.engine.bags.findMany({ + select: { bag_name: true, bag_id: true, accesscontrol: true, description: true }, + orderBy: { bag_name: "asc" } + }); + // const rows = await this.engine.runStatementGetAll(` + // SELECT bag_name, bag_id, accesscontrol, description + // FROM bags + // ORDER BY bag_name + // `); + // return rows; + } + /* + Create or update a bag + Returns the bag_id of the bag + */ + async createBag(bag_name: string, description: string, accesscontrol = "") { + // ignore if the bag already exists + const bag = await this.engine.bags.upsert({ + create: { bag_name, description, accesscontrol }, + update: { description, accesscontrol }, + where: { bag_name }, + }); + return bag.bag_id; + } + /* + Returns array of {recipe_name:,recipe_id:,description:,bag_names: []} + */ + async listRecipes() { + const rows = await this.engine.recipes.findMany({ + select: { + recipe_name: true, + recipe_id: true, + description: true, + owner_id: true, + recipe_bags: { select: { bag: { select: { bag_name: true } } } } + } + }); + return rows.map(row => { + return { + recipe_name: row.recipe_name, + recipe_id: row.recipe_id, + description: row.description, + owner_id: row.owner_id, + bag_names: row.recipe_bags.map(value => value.bag.bag_name) + }; + }); + + // const rows = await this.engine.runStatementGetAll(` + // SELECT r.recipe_name, r.recipe_id, r.description, r.owner_id, b.bag_name, rb.position + // FROM recipes AS r + // JOIN recipe_bags AS rb ON rb.recipe_id = r.recipe_id + // JOIN bags AS b ON rb.bag_id = b.bag_id + // ORDER BY r.recipe_name, rb.position + // `); + // const results = []; + // let currentRecipeName = null, currentRecipeIndex = -1; + // for (const row of rows) { + // if (row.recipe_name !== currentRecipeName) { + // currentRecipeName = row.recipe_name; + // currentRecipeIndex += 1; + // results.push({ + // recipe_name: row.recipe_name, + // recipe_id: row.recipe_id, + // description: row.description, + // owner_id: row.owner_id, + // /** @type {string[]} */ + // bag_names: [] + // }); + // } + // results[currentRecipeIndex].bag_names.push(row.bag_name); + // } + // return results; + } + /* + Create or update a recipe + Returns the recipe_id of the recipe + */ + async createRecipe(recipe_name: string, bag_names: string[], description: string) { + // deleting this will also delete the recipe_bags records + await this.engine.recipes.deleteMany({ + where: { recipe_name } + }); + const updateRecipes = await this.engine.recipes.create({ + data: { + recipe_name, + description, + recipe_bags: { + create: bag_names.map((bag_name, position) => ({ + position, + bag: { connect: { bag_name } } + })) + } + } + }); + return updateRecipes.recipe_id; + + // // Run the queries + // await this.engine.runStatement(` + // -- Delete existing recipe_bags entries for this recipe + // DELETE FROM recipe_bags WHERE recipe_id = (SELECT recipe_id FROM recipes WHERE recipe_name = $recipe_name) + // `, { + // $recipe_name: recipe_name + // }); + // const updateRecipes = await this.engine.runStatement(` + // -- Create the entry in the recipes table if required + // INSERT OR REPLACE INTO recipes (recipe_name, description) + // VALUES ($recipe_name, $description) + // `, { + // $recipe_name: recipe_name, + // $description: description + // }); + // await this.engine.runStatement(` + // INSERT INTO recipe_bags (recipe_id, bag_id, position) + // SELECT r.recipe_id, b.bag_id, j.key as position + // FROM recipes r + // JOIN bags b + // INNER JOIN json_each($bag_names) AS j ON j.value = b.bag_name + // WHERE r.recipe_name = $recipe_name + // `, { + // $recipe_name: recipe_name, + // $bag_names: JSON.stringify(bag_names) + // }); + // return updateRecipes.lastInsertRowid; + } + /* + Assign a recipe to a user + */ + async assignRecipeToUser(recipe_name: string, user_id: number) { + await this.engine.recipes.update({ + where: { recipe_name }, + data: { owner_id: user_id } + }); + // await this.engine.runStatement(` + // UPDATE recipes SET owner_id = $user_id WHERE recipe_name = $recipe_name + // `, { + // $recipe_name: recipe_name, + // $user_id: user_id + // }); + } + /* + Returns {tiddler_id:} + */ + async saveBagTiddler(tiddlerFields: { [s: string]: string; }, bag_name: string, attachment_blob: string) { + // fields are deleted at the same time (cascade) + await this.engine.tiddlers.deleteMany({ + where: { bag: { bag_name }, title: tiddlerFields.title }, + }); + const tiddler = await this.engine.tiddlers.create({ + data: { + // this makes sure the bag exists + bag: { connect: { bag_name } }, + title: tiddlerFields.title, + is_deleted: false, + attachment_blob, + fields: { + create: Object.entries(tiddlerFields).map(([field_name, field_value]) => { + if (field_value === null) field_value = ""; + if (field_value === undefined) field_value = ""; + + switch (typeof field_value) { + case "string": + break; + case "number": + case "boolean": + case "bigint": + field_value = (field_value as any).toString(); + break; + default: + $tw.utils.error("Invalid field value type: " + typeof field_value); + } + return { field_name, field_value }; + }) + } + } + }); + return { tiddler_id: tiddler.tiddler_id }; + // attachment_blob = attachment_blob || null; + // // Update the tiddlers table + // var info = await this.engine.runStatement(` + // INSERT OR REPLACE INTO tiddlers (bag_id, title, is_deleted, attachment_blob) + // VALUES ( + // (SELECT bag_id FROM bags WHERE bag_name = $bag_name), + // $title, + // FALSE, + // $attachment_blob + // ) + // `, { + // $title: tiddlerFields.title, + // $attachment_blob: attachment_blob, + // $bag_name: bag_name + // }); + // // Update the fields table + // await this.engine.runStatement(` + // INSERT OR REPLACE INTO fields (tiddler_id, field_name, field_value) + // SELECT + // t.tiddler_id, + // 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: bag_name, + // $field_values: JSON.stringify(Object.assign({}, tiddlerFields, { title: undefined })) + // }); + // return { + // tiddler_id: info.lastInsertRowid + // }; + } + /* + Returns {tiddler_id:,bag_name:} or null if the recipe is empty + */ + async saveRecipeTiddler(tiddlerFields: TiddlerFields, recipe_name: string, attachment_blob: string) { + // Find the topmost bag in the recipe + const bag = await this.engine.recipe_bags.findFirst({ + where: { recipe: { recipe_name } }, + select: { bag: { select: { bag_name: true } } }, + orderBy: { position: "desc" } + }); + if (!bag) { + return null; + } + // Save the tiddler to the topmost bag + const info = await this.saveBagTiddler(tiddlerFields, bag.bag.bag_name, attachment_blob); + return { + tiddler_id: info.tiddler_id, + bag_name: bag.bag.bag_name + }; + // // Find the topmost bag in the recipe + // var row = await this.engine.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: recipe_name + // }); + // if (!row) { + // return null; + // } + // // Save the tiddler to the topmost bag + // var info = await this.saveBagTiddler(tiddlerFields, row.bag_name, attachment_blob); + // return { + // tiddler_id: info.tiddler_id, + // bag_name: row.bag_name + // }; + } + /* + Returns {tiddler_id:} of the delete marker + */ + async deleteTiddler(title: string, bag_name: string) { + + // fields are deleted at the same time (cascade) + // use deleteMany so we can filter by bag_name directly + await this.engine.tiddlers.deleteMany({ + where: { bag: { bag_name }, title }, + }) + // create the delete marker + const rowDeleteMarker = await this.engine.tiddlers.create({ + data: { + is_deleted: true, + attachment_blob: null, + title, + bag: { connect: { bag_name } } + } + }); + return { tiddler_id: rowDeleteMarker.tiddler_id }; + // // Delete the fields of this tiddler + // await this.engine.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: bag_name + // }); + // // Mark the tiddler itself as deleted + // const rowDeleteMarker = await this.engine.runStatement(` + // INSERT OR REPLACE INTO tiddlers (bag_id, title, is_deleted, attachment_blob) + // VALUES ( + // (SELECT bag_id FROM bags WHERE bag_name = $bag_name), + // $title, + // TRUE, + // NULL + // ) + // `, { + // $title: title, + // $bag_name: bag_name + // }); + // return { tiddler_id: rowDeleteMarker.lastInsertRowid }; + } + /* + returns {tiddler_id:,tiddler:,attachment_blob:} + */ + async getBagTiddler(title: string, bag_name: string) { + const { bag_id } = await this.engine.bags.findUniqueOrThrow({ + where: { bag_name }, + select: { bag_id: true } + }); + const tiddler = await this.engine.tiddlers.findUnique({ + where: { bag_id_title: { bag_id, title } }, + include: { fields: true } + }); + if (!tiddler) { + return null; + } + return { + tiddler_id: tiddler.tiddler_id, + attachment_blob: tiddler.attachment_blob, + tiddler: tiddler.fields.reduce((accumulator, value) => { + accumulator[value.field_name] = value.field_value; + return accumulator; + }, { title } as TiddlerFields) + }; + // const rowTiddler = await this.engine.runStatementGet(` + // SELECT t.tiddler_id, t.attachment_blob + // FROM bags AS b + // INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id + // WHERE t.title = $title AND b.bag_name = $bag_name AND t.is_deleted = FALSE + // `, { + // $title: title, + // $bag_name: bag_name + // }); + // if (!rowTiddler) { + // return null; + // } + // const rows = await this.engine.runStatementGetAll(` + // SELECT field_name, field_value, tiddler_id + // FROM fields + // WHERE tiddler_id = $tiddler_id + // `, { + // $tiddler_id: rowTiddler.tiddler_id + // }); + // if (rows.length === 0) { + // return null; + // } else { + // return { + // bag_name: bag_name, + // tiddler_id: rows[0].tiddler_id, + // attachment_blob: rowTiddler.attachment_blob, + // tiddler: rows.reduce((accumulator, value) => { + // accumulator[value["field_name"]] = value.field_value; + // return accumulator; + // }, { title: title }) + // }; + // } + } + /* + Returns {bag_name:, tiddler: {fields}, tiddler_id:, attachment_blob:} + */ + async getRecipeTiddler(title: string, recipe_name: string) { + const row = await this.engine.recipe_bags.findFirst({ + // where: the first recipe_bag containing this tiddler, in descending order + where: { recipe: { recipe_name }, bag: { tiddlers: { some: { title } } } }, + orderBy: { position: "desc" }, + // select: the tiddler and its fields + select: { + bag: { + select: { + bag_name: true, + tiddlers: { + where: { title }, + include: { fields: true } + } + } + }, + }, + }); + if (!row) { + return null; + } + const tiddler = row.bag.tiddlers[0]; + + return { + bag_name: row.bag.bag_name, + tiddler_id: tiddler.tiddler_id, + attachment_blob: tiddler.attachment_blob, + tiddler: tiddler.fields.reduce((accumulator, value) => { + accumulator[value.field_name] = value.field_value; + return accumulator; + }, { title } as TiddlerFields) + }; + // const rowTiddlerId = await this.engine.runStatementGet(` + // SELECT t.tiddler_id, t.attachment_blob, b.bag_name + // FROM bags AS b + // INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id + // 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 + // AND t.is_deleted = FALSE + // ORDER BY rb.position DESC + // LIMIT 1 + // `, { + // $title: title, + // $recipe_name: recipe_name + // }); + // if (!rowTiddlerId) { + // return null; + // } + // // Get the fields + // const rows = await this.engine.runStatementGetAll(` + // SELECT field_name, field_value + // FROM fields + // WHERE tiddler_id = $tiddler_id + // `, { + // $tiddler_id: rowTiddlerId.tiddler_id + // }); + // return { + // bag_name: rowTiddlerId.bag_name, + // tiddler_id: rowTiddlerId.tiddler_id, + // attachment_blob: rowTiddlerId.attachment_blob, + // tiddler: rows.reduce((accumulator, value) => { + // accumulator[value["field_name"]] = value.field_value; + // return accumulator; + // }, { title: title }) + // }; + } + /* + Checks if a user has permission to access a recipe + */ + async hasRecipePermission(user_id: number | undefined, recipe_name: string, permissionName: string) { + const recipe = await this.engine.recipes.findUnique({ + where: { recipe_name }, select: { owner_id: true } + }); + return await this.checkACLPermission( + user_id, "recipe", recipe_name, permissionName, recipe?.owner_id ?? undefined + ); + // try { + // // check if the user is the owner of the entity + // const recipe = await this.engine.runStatementGet(` + // SELECT owner_id + // FROM recipes + // WHERE recipe_name = $recipe_name + // `, { + // $recipe_name: recipeName + // }); + + // if (!!recipe?.owner_id && recipe?.owner_id === userId) { + // return true; + // } else { + // var permission = await this.checkACLPermission(userId, "recipe", recipeName, permissionName, recipe?.owner_id); + // return permission; + // } + + // } catch (error) { + // console.error(error); + // return false; + // } + } + /* + Checks if a user has permission to access a bag + */ + async hasBagPermission(userId: number | undefined, bagName: string, permissionName: string) { + return await this.checkACLPermission(userId, "bag", bagName, permissionName, undefined); + } + /** + * @overload + * @param {string} entityType + * @param {string} entityName + * @param {false} [fetchAll] + * @returns {Promise>} + * + * @overload + * @param {string} entityType + * @param {string} entityName + * @param {true} fetchAll + * @returns {Promise[]>} + */ + async getACLByName(entityType: EntityType, entityName: string, fetchAll?: boolean) { + okEntityType(entityType); + okTypeTruthy(entityName, "string", "No entityName provided"); + // okType(fetchAll, "boolean", "fetchAll must be a boolean"); + + return await this.engine.acl.findMany({ + where: { entity_type: entityType, entity_name: entityName }, + include: { permission: true }, + take: fetchAll ? undefined : 1, + }); + + // const entityInfo = this.entityTypeToTableMap[entityType]; + // if (!entityInfo) { + // throw new Error("Invalid entity type: " + entityType); + // } + + // // First, check if there's an ACL record for the entity and get the permission_id + // var checkACLExistsQuery = ` + // SELECT acl.*, permissions.permission_name + // FROM acl + // LEFT JOIN permissions ON acl.permission_id = permissions.permission_id + // WHERE acl.entity_type = $entity_type + // AND acl.entity_name = $entity_name + // `; + + // if (!fetchAll) { + // checkACLExistsQuery += ' LIMIT 1'; + // } + + // const aclRecord = await this.engine[fetchAll ? 'runStatementGetAll' : 'runStatementGet'](checkACLExistsQuery, { + // $entity_type: entityType, + // $entity_name: entityName + // }); + + // return aclRecord; + } + async checkACLPermission( + user_id: number | undefined, + entityType: EntityType, + entityName: string, + permissionName: string, + ownerId: number | undefined + ) { + if(user_id === undefined) return false; + + okType(user_id, "number", "No user_id provided"); + okEntityType(entityType); + okTypeTruthy(entityName, "string", "No entityName provided"); + okTypeTruthy(permissionName, "string", "No permissionName provided"); + ok(typeof ownerId === "number" || ownerId === undefined); + + // if the entityName starts with "$:/", we'll assume its a system bag/recipe, then grant the user permission + if (entityName.startsWith("$:/")) return true; + const aclRecords = await this.getACLByName(entityType, entityName, true); + const aclRecord = aclRecords.find(record => record.permission?.permission_name === permissionName); + + // If no ACL record exists, return true for hasPermission + if (!aclRecord && !ownerId && aclRecords.length === 0 + || !!aclRecord && !!ownerId && ownerId === user_id) { + return true; + } + + if (!aclRecord?.permission?.permission_id) return false; + + const result = await this.engine.users.findUnique({ + where: { + user_id, + user_roles: { + some: { + role: { + acls: { + some: { + entity_type: entityType, + entity_name: entityName, + permission_id: aclRecord.permission.permission_id + } + } + } + } + } + }, + select: { + user_id: true, + } + }); + return !!result; + + // // If ACL record exists, check for user permission using the retrieved permission_id + // const checkPermissionQuery = ` + // SELECT * + // FROM users u + // JOIN user_roles ur ON u.user_id = ur.user_id + // JOIN roles r ON ur.role_id = r.role_id + // JOIN acl a ON r.role_id = a.role_id + // WHERE u.user_id = $user_id + // AND a.entity_type = $entity_type + // AND a.entity_name = $entity_name + // AND a.permission_id = $permission_id + // LIMIT 1 + // `; + + // const result = await this.engine.runStatementGet(checkPermissionQuery, { + // $user_id: userId, + // $entity_type: entityType, + // $entity_name: entityName, + // $permission_id: aclRecord?.permission_id + // }); + } + /** + * Returns the ACL records for an entity (bag or recipe) + */ + async getEntityAclRecords(entity_name: string) { + return await this.engine.acl.findMany({ + where: { entity_name }, + }); + // const checkACLExistsQuery = ` + // SELECT * + // FROM acl + // WHERE entity_name = $entity_name + // `; + // const aclRecords = await this.engine.runStatementGetAll(checkACLExistsQuery, { + // $entity_name: entityName + // }); + // return aclRecords; + } + + /* + Get the entity by name + */ + async getEntityByName(entityType: EntityType, entityName: string) { + okEntityType(entityType); + okTypeTruthy(entityName, "string", "No entityName provided"); + // they have to be separated for typechecking, but the code is the same + switch (entityType) { + case "recipe": return await this.engine.recipes.findUnique({ + where: { recipe_name: entityName }, + }); + case "bag": return await this.engine.bags.findUnique({ + where: { bag_name: entityName }, + }); + } + // const entityTypeToTableMap = { + // bag: { + // table: "bags", + // column: "bag_name" + // }, + // recipe: { + // table: "recipes", + // column: "recipe_name" + // } + // } as const; + // const entityInfo = this.entityTypeToTableMap[entityType]; + // if (entityInfo) { + // return await this.engine.runStatementGet(`SELECT * FROM ${entityInfo.table} WHERE ${entityInfo.column} = $entity_name`, { + // $entity_name: entityName + // }); + // } + // return null; + } + /* + Get the titles of the tiddlers in a bag. Returns an empty array for bags that do not exist + */ + async getBagTiddlers(bag_name: string) { + const rows = await this.engine.tiddlers.findMany({ + where: { bag: { bag_name }, is_deleted: false }, + select: { title: true, tiddler_id: true }, + orderBy: { title: "asc" } + }); + return rows; + // const rows = await this.engine.runStatementGetAll(` + // SELECT DISTINCT title, tiddler_id + // FROM tiddlers + // WHERE bag_id IN ( + // SELECT bag_id + // FROM bags + // WHERE bag_name = $bag_name + // ) + // AND tiddlers.is_deleted = FALSE + // ORDER BY title ASC + // `, { + // $bag_name: bag_name + // }); + // return rows; + } + /* + Get the tiddler_id of the newest tiddler in a bag. Returns null for bags that do not exist + */ + async getBagLastTiddlerId(bag_name: string) { + const row = await this.engine.tiddlers.findFirst({ + where: { bag: { bag_name } }, + select: { tiddler_id: true }, + orderBy: { tiddler_id: "desc" } + }); + return row?.tiddler_id ?? null; + // const row = await this.engine.runStatementGet(` + // SELECT tiddler_id + // FROM tiddlers + // WHERE bag_id IN ( + // SELECT bag_id + // FROM bags + // WHERE bag_name = $bag_name + // ) + // ORDER BY tiddler_id DESC + // LIMIT 1 + // `, { + // $bag_name: bag_name + // }); + // if (row) { + // return row.tiddler_id; + // } else { + // return null; + // } + } + /** + Get the metadata of the tiddlers in a recipe as an array [{title:,tiddler_id:,bag_name:,is_deleted:}], + sorted in ascending order of tiddler_id. + + Options include: + + limit: optional maximum number of results to return + last_known_tiddler_id: tiddler_id of the last known update. Only returns tiddlers that have been created, modified or deleted since + include_deleted: boolean, defaults to false + + Returns null for recipes that do not exist + + @returns {} + */ + async getRecipeTiddlers(recipe_name: string, options: { + limit?: number, + last_known_tiddler_id?: number, + include_deleted?: boolean + } = {}): Promise<{ + bag_name: string + title: string + tiddler_id: number + is_deleted: boolean + }[] | null> { + + const tiddlers = await this.engine.tiddlers.findMany({ + // all tiddlers in a bag that are in the recipe + where: { + bag: { recipe_bags: { some: { recipe: { recipe_name } } } }, + tiddler_id: options.last_known_tiddler_id ? { + gt: options.last_known_tiddler_id + } : undefined, + is_deleted: options.include_deleted ? undefined : false + }, + select: { + title: true, + tiddler_id: true, + is_deleted: true, + bag: { + select: { + bag_name: true, + recipe_bags: { + select: { position: true, recipe_id: true }, + where: { recipe: { recipe_name } } + } + } + } + } + }); + + const tiddlerMap = new Map(); + for (const tiddler of tiddlers) { + if (!tiddler.bag.recipe_bags.length) + $tw.utils.warning(`Tiddler '${tiddler.title}' is not in the recipe '${recipe_name}'???`); + if (tiddler.bag.recipe_bags.length > 1) + $tw.utils.warning(`Tiddler bag '${tiddler.bag.bag_name}' is specified multiple times in the recipe '${recipe_name}'???`); + + const { position } = tiddler.bag.recipe_bags.sort((a, b) => b.position - a.position)[0]; + const current = tiddlerMap.get(tiddler.title); + if (current) if (current.position > position) continue; + tiddlerMap.set(tiddler.title, { position, tiddler }); + } + + return [...tiddlerMap.values()].map(({ tiddler }) => ({ + title: tiddler.title, + tiddler_id: tiddler.tiddler_id, + is_deleted: tiddler.is_deleted, + bag_name: tiddler.bag.bag_name, + })); + + + + // // Get the recipe ID + // const rowsCheckRecipe = await this.engine.runStatementGet(` + // SELECT recipe_id FROM recipes WHERE recipes.recipe_name = $recipe_name + // `, { + // $recipe_name: recipe_name + // }); + // if (!rowsCheckRecipe) { + // return null; + // } + // const recipe_id = rowsCheckRecipe.recipe_id; + // // Compose the query to get the tiddlers + // const params = { + // $recipe_id: recipe_id + // }; + // if (options.limit) { + // params.$limit = options.limit.toString(); + // } + // if (options.last_known_tiddler_id) { + // params.$last_known_tiddler_id = options.last_known_tiddler_id; + // } + // /** * @type {any} */ + // const rows = await this.engine.runStatementGetAll(` + // SELECT title, tiddler_id, is_deleted, bag_name + // FROM ( + // SELECT t.title, t.tiddler_id, t.is_deleted, b.bag_name, MAX(rb.position) AS position + // FROM bags AS b + // INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id + // INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id + // WHERE rb.recipe_id = $recipe_id + // ${options.include_deleted ? "" : "AND t.is_deleted = FALSE"} + // ${options.last_known_tiddler_id ? "AND tiddler_id > $last_known_tiddler_id" : ""} + // GROUP BY t.title + // ORDER BY t.title, tiddler_id DESC + // ${options.limit ? "LIMIT $limit" : ""} + // ) + // `, params); + + // return rows; + } + /* + Get the tiddler_id of the newest tiddler in a recipe. Returns null for recipes that do not exist + */ + async getRecipeLastTiddlerId(recipe_name: string) { + // the old code was returning the latest tiddler_id out of every bag in the recipe, + // not just the latest visible tiddler_id, so we'll do the same for now + const row = await this.engine.tiddlers.findFirst({ + where: { bag: { recipe_bags: { some: { recipe: { recipe_name } } } } }, + orderBy: { tiddler_id: "desc" }, + select: { tiddler_id: true } + }); + return row?.tiddler_id ?? null; + // const row = await this.engine.runStatementGet(` + // SELECT t.title, t.tiddler_id, b.bag_name, MAX(rb.position) AS position + // FROM bags AS b + // INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id + // 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.tiddler_id DESC + // LIMIT 1 + // `, { + // $recipe_name: recipe_name + // }); + // if (row) { + // return row.tiddler_id; + // } else { + // return null; + // } + } + async deleteAllTiddlersInBag(bag_name: string) { + const { bag_id } = await this.engine.bags.findUniqueOrThrow({ + where: { bag_name }, + select: { bag_id: true }, + }); + const tiddlers = await this.engine.tiddlers.findMany({ + where: { bag: { bag_name } }, + }) + await this.engine.tiddlers.deleteMany({ + where: { bag: { bag_name } }, + }) + // create the delete marker + await this.engine.tiddlers.createManyAndReturn({ + data: tiddlers.map(({ title }) => ({ + title, + bag_id, + is_deleted: true, + attachment_blob: null, + })), + select: { tiddler_id: true, title: true } + }); + + // await this.engine.tiddlers.deleteMany({ + // where: { bag: { bag_name } } + // }); + // await this.engine.tiddlers.createMany({ + // data: [{ title: "$:/status/IsReadOnly", is_deleted: false, bag: { connect: { bag_name } } }] + // }) + // // Delete the fields + // await this.engine.runStatement(` + // DELETE FROM fields + // WHERE tiddler_id IN ( + // SELECT tiddler_id + // FROM tiddlers + // WHERE bag_id = (SELECT bag_id FROM bags WHERE bag_name = $bag_name) + // AND is_deleted = FALSE + // ) + // `, { + // $bag_name: bag_name + // }); + // // Mark the tiddlers as deleted + // await this.engine.runStatement(` + // UPDATE tiddlers + // SET is_deleted = TRUE + // WHERE bag_id = (SELECT bag_id FROM bags WHERE bag_name = $bag_name) + // AND is_deleted = FALSE + // `, { + // $bag_name: bag_name + // }); + } + /* + Get the names of the bags in a recipe. Returns an empty array for recipes that do not exist + */ + async getRecipeBags(recipe_name: string) { + const rows = await this.engine.recipe_bags.findMany({ + where: { recipe: { recipe_name } }, + select: { bag: { select: { bag_name: true } } }, + orderBy: { position: "asc" } + }); + return rows.map(e => e.bag.bag_name); + // const rows = await this.engine.runStatementGetAll(` + // SELECT bags.bag_name + // FROM bags + // JOIN ( + // SELECT rb.bag_id, rb.position as position + // 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 + // ORDER BY position + // `, { + // $recipe_name: recipe_name + // }); + // return rows.map(value => value.bag_name); + + } + /* + Get the attachment value of a bag, if any exist + */ + async getBagTiddlerAttachmentBlob(title: string, bag_name: string) { + const row = await this.engine.tiddlers.findFirst({ + where: { bag: { bag_name }, title }, + select: { attachment_blob: true } + }); + return row?.attachment_blob ?? null; + // const row = await this.engine.runStatementGet(` + // SELECT t.attachment_blob + // FROM bags AS b + // INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id + // WHERE t.title = $title AND b.bag_name = $bag_name AND t.is_deleted = FALSE + // `, { + // $title: title, + // $bag_name: bag_name + // }); + // return row ? row.attachment_blob : null; + } + async getRecipeTiddlerBag({ title, recipe_name }: { title: string; recipe_name: string; }) { + const bag = await this.engine.recipe_bags.findFirst({ + // where: the first recipe_bag containing this tiddler, in descending order + where: { recipe: { recipe_name }, bag: { tiddlers: { some: { title } } } }, + orderBy: { position: "desc" }, + // select: the bag_id + select: { bag_id: true }, + }); + return bag; + } + /* + Get the attachment value of a recipe, if any exist + */ + async getRecipeTiddlerAttachmentBlob(title: string, recipe_name: string) { + const bag = await this.getRecipeTiddlerBag({ title, recipe_name }); + if (!bag) return null; + + const tiddler = await this.engine.tiddlers.findUnique({ + where: { bag_id_title: { bag_id: bag.bag_id, title } }, + select: { attachment_blob: true } + }); + + return tiddler?.attachment_blob ?? null; + // const row = await this.engine.runStatementGet(` + // SELECT t.attachment_blob + // FROM bags AS b + // INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id + // 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 AND t.is_deleted = FALSE + // ORDER BY rb.position DESC + // LIMIT 1 + // `, { + // $title: title, + // $recipe_name: recipe_name + // }); + // return row ? row.attachment_blob : null; + } + // User CRUD operations + async createUser(username: string, email: string, password: string) { + const result = await this.engine.users.create({ + data: { + username, + email, + password + }, + select: { + user_id: true + } + }); + return result.user_id; + // const result = await this.engine.runStatement(` + // INSERT INTO users (username, email, password) + // VALUES ($username, $email, $password) + // `, { + // $username: username, + // $email: email, + // $password: password + // }); + // return result.lastInsertRowid; + } + async getUser(user_id: number) { + okType(user_id, "number", "No userId provided"); + return await this.engine.users.findUnique({ + where: { user_id } + }); + // return await this.engine.runStatementGet(` + // SELECT * FROM users WHERE user_id = $userId + // `, { + // $userId: userId + // }); + } + async getUserByUsername(username: string) { + return await this.engine.users.findFirst({ + where: { username }, + }); + // return await this.engine.runStatementGet(` + // SELECT * FROM users WHERE username = $username + // `, { + // $username: username + // }); + } + async getUserByEmail(email: string) { + return await this.engine.users.findFirst({ + where: { email }, + }); + // return await this.engine.runStatementGet(` + // SELECT * FROM users WHERE email = $email + // `, { + // $email: email + // }); + } + async listUsersByRoleId(role_id: number) { + return await this.engine.users.findMany({ + where: { user_roles: { some: { role_id } } }, + orderBy: { username: "asc" } + }); + // return await this.engine.runStatementGetAll(` + // SELECT u.* + // FROM users u + // JOIN user_roles ur ON u.user_id = ur.user_id + // WHERE ur.role_id = $roleId + // ORDER BY u.username + // `, { + // $roleId: roleId + // }); + } + async updateUser(user_id: number, username: string, email: string, role_id?: number) { + const existingUser = await this.engine.users.findFirst({ + where: { user_id: { not: user_id }, email }, + }); + if (existingUser) { + return { + success: false, + message: "Email address already in use by another user." + }; + } + await this.engine.users.update({ + where: { user_id }, + data: { username, email } + }); + if (typeof role_id === "number") { + await this.engine.user_roles.deleteMany({ + where: { user_id } + }); + await this.engine.user_roles.create({ + data: { user_id, role_id } + }); + } + return { + success: true, + message: "User profile and role updated successfully." + }; + + // }); + // const existingUser = await this.engine.runStatementGet(` + // SELECT user_id FROM users + // WHERE email = $email AND user_id != $userId + // `, { + // $email: email, + // $userId: userId + // }); + + // if (existingUser) { + // return { + // success: false, + // message: "Email address already in use by another user." + // }; + // } + + // try { + // await this.engine.transaction(async () => { + // // Update user information + // await this.engine.runStatement(` + // UPDATE users + // SET username = $username, email = $email + // WHERE user_id = $userId + // `, { + // $userId: userId, + // $username: username, + // $email: email + // }); + + // if (roleId) { + // // Remove all existing roles for the user + // await this.engine.runStatement(` + // DELETE FROM user_roles + // WHERE user_id = $userId + // `, { + // $userId: userId + // }); + + // // Add the new role + // await this.engine.runStatement(` + // INSERT INTO user_roles (user_id, role_id) + // VALUES ($userId, $roleId) + // `, { + // $userId: userId, + // $roleId: roleId + // }); + // } + // }); + + // return { + // success: true, + // message: "User profile and role updated successfully." + // }; + // } catch (error) { + // return { + // success: false, + // message: "Failed to update user profile: " + error.message + // }; + // } + } + async updateUserPassword(user_id: number, password: string) { + await this.engine.users.update({ + where: { user_id }, + data: { password } + }); + // try { + // await this.engine.runStatement(` + // UPDATE users + // SET password = $newHash + // WHERE user_id = $userId + // `, { + // $userId: userId, + // $newHash: newHash, + // }); + + // return { + // success: true, + // message: "Password updated successfully." + // }; + // } catch (error) { + // return { + // success: false, + // message: "Failed to update password: " + error.message + // }; + // } + } + async deleteUser(user_id: number) { + await this.engine.users.delete({ + where: { user_id } + }); + // await this.engine.runStatement(` + // DELETE FROM users WHERE user_id = $userId + // `, { + // $userId: userId + // }); + } + async listUsers() { + return await this.engine.users.findMany({ + orderBy: { username: "asc" } + }); + // return await this.engine.runStatementGetAll(` + // SELECT * FROM users ORDER BY username + // `); + } + async createOrUpdateUserSession(user_id: number, session_id: string) { + const currentTimestamp = new Date().toISOString(); + // First, try to update an existing session + const updateResult = await this.engine.sessions.updateMany({ + where: { user_id }, + data: { + session_id, + last_accessed: currentTimestamp + }, + }); + // If no existing session was updated, create a new one + if (updateResult.count === 0) { + await this.engine.sessions.create({ + data: { + user_id, + session_id, + created_at: currentTimestamp, + last_accessed: currentTimestamp + } + }); + } + return session_id; + + // await this.engine.sessions.upsert({ + // create: { + // user_id, + // session_id, + // created_at: currentTimestamp, + // last_accessed: currentTimestamp + // }, + // update: { + // session_id, + // last_accessed: currentTimestamp + // }, + // where: { user_id }, + // }); + + // const currentTimestamp = new Date().toISOString(); + + // // First, try to update an existing session + // const updateResult = await this.engine.runStatement(` + // UPDATE sessions + // SET session_id = $sessionId, last_accessed = $timestamp + // WHERE user_id = $userId + // `, { + // $userId: userId, + // $sessionId: sessionId, + // $timestamp: currentTimestamp + // }); + + // // If no existing session was updated, create a new one + // if (updateResult.changes === 0) { + // await this.engine.runStatement(` + // INSERT INTO sessions (user_id, session_id, created_at, last_accessed) + // VALUES ($userId, $sessionId, $timestamp, $timestamp) + // `, { + // $userId: userId, + // $sessionId: sessionId, + // $timestamp: currentTimestamp + // }); + // } + + // return sessionId; + } + async createUserSession(user_id: number, session_id: string) { + const currentTimestamp = new Date().toISOString(); + // throw new Error("how are we even here?"); + await this.engine.sessions.create({ + data: { + user_id, + session_id, + created_at: currentTimestamp, + last_accessed: currentTimestamp + } + }); + // await this.engine.runStatement(` + // INSERT INTO sessions (user_id, session_id, created_at, last_accessed) + // VALUES ($userId, $sessionId, $timestamp, $timestamp) + // `, { + // $userId: userId, + // $sessionId: sessionId, + // $timestamp: currentTimestamp + // }); + return session_id; + } + /** + * @typedef {Object} User + * @property {number} user_id + * @property {string} username + * @property {string} email + * @property {string} [password] + * @property {string} created_at + * @property {string} last_login + */ + + async findUserBySessionId(session_id: string) { + const session = await this.engine.sessions.findUnique({ + where: { session_id }, + select: { last_accessed: true, user: true } + }); + if (!session) return null; + const lastAccessed = new Date(session.last_accessed); + const expirationTime = 24 * 60 * 60 * 1000; // 24 hours in milliseconds + if ((+new Date() - +lastAccessed) > expirationTime) { + await this.deleteSession(session_id); + return null; + } + // Update the last_accessed timestamp + const currentTimestamp = new Date().toISOString(); + await this.engine.sessions.update({ + where: { session_id }, + data: { last_accessed: currentTimestamp } + }); + return session.user; + + // // First, get the user_id from the sessions table + // const sessionResult = await this.engine.runStatementGet(` + // SELECT user_id, last_accessed + // FROM sessions + // WHERE session_id = $sessionId + // `, { + // $sessionId: sessionId + // }); + + // if (!sessionResult) { + // return null; // Session not found + // } + + // const lastAccessed = new Date(sessionResult.last_accessed); + // const expirationTime = 24 * 60 * 60 * 1000; // 24 hours in milliseconds + // if (new Date() - lastAccessed > expirationTime) { + // // Session has expired + // await this.deleteSession(sessionId); + // return null; + // } + + // // Update the last_accessed timestamp + // const currentTimestamp = new Date().toISOString(); + // await this.engine.runStatement(` + // UPDATE sessions + // SET last_accessed = $timestamp + // WHERE session_id = $sessionId + // `, { + // $sessionId: sessionId, + // $timestamp: currentTimestamp + // }); + // /** @type {any} */ + // const userResult = await this.engine.runStatementGet(` + // SELECT * + // FROM users + // WHERE user_id = $userId + // `, { + // $userId: sessionResult.user_id + // }); + + // if (!userResult) { + // return null; + // } + + // return userResult; + } + async deleteSession(session_id: string) { + await this.engine.sessions.delete({ + where: { session_id } + }); + // await this.engine.runStatement(` + // DELETE FROM sessions + // WHERE session_id = $sessionId + // `, { + // $sessionId: sessionId + // }); + } + async deleteUserSessions(user_id: number) { + await this.engine.sessions.deleteMany({ + where: { user_id } + }); + // await this.engine.runStatement(` + // DELETE FROM sessions + // WHERE user_id = $userId + // `, { + // $userId: userId + // }); + } + // Set the user as an admin + async setUserAdmin(user_id: number) { + const admin = await this.getRoleByName("ADMIN"); + if (admin) { + await this.addRoleToUser(user_id, admin.role_id); + } + // var admin = await this.getRoleByName("ADMIN"); + // if (admin) { + // await this.addRoleToUser(userId, admin.role_id); + // } + + } + // Group CRUD operations + async createGroup(group_name: string, description: string) { + const result = await this.engine.groups.create({ + data: { + group_name, + description + }, + select: { group_id: true } + }); + return result.group_id; + // const result = await this.engine.runStatement(` + // INSERT INTO groups (group_name, description) + // VALUES ($groupName, $description) + // `, { + // $groupName: groupName, + // $description: description + // }); + // return result.lastInsertRowid; + } + async getGroup(groupId: number) { + return await this.engine.groups.findUnique({ + where: { group_id: groupId } + }); + // return await this.engine.runStatementGet(` + // SELECT * FROM groups WHERE group_id = $groupId + // `, { + // $groupId: groupId + // }); + } + async updateGroup(groupId: number, groupName: string, description: string) { + await this.engine.groups.update({ + where: { group_id: groupId }, + data: { group_name: groupName, description } + }); + // await this.engine.runStatement(` + // UPDATE groups + // SET group_name = $groupName, description = $description + // WHERE group_id = $groupId + // `, { + // $groupId: groupId, + // $groupName: groupName, + // $description: description + // }); + } + async deleteGroup(groupId: number) { + return await this.engine.groups.delete({ + where: { group_id: groupId } + }); + // await this.engine.runStatement(` + // DELETE FROM groups WHERE group_id = $groupId + // `, { + // $groupId: groupId + // }); + } + async listGroups() { + return await this.engine.groups.findMany({ + orderBy: { group_name: "asc" } + }); + // return await this.engine.runStatementGetAll(` + // SELECT * FROM groups ORDER BY group_name + // `); + } + // Role CRUD operations + async createRole(roleName: string, description: string) { + const result = await this.engine.roles.create({ + data: { + role_name: roleName, + description + }, + select: { role_id: true } + }); + // const result = await this.engine.runStatement(` + // INSERT OR IGNORE INTO roles (role_name, description) + // VALUES ($roleName, $description) + // `, { + // $roleName: roleName, + // $description: description + // }); + // return result.lastInsertRowid; + } + async getRole(roleId: number) { + return await this.engine.roles.findUnique({ + where: { role_id: roleId } + }); + // return await this.engine.runStatementGet(` + // SELECT * FROM roles WHERE role_id = $roleId + // `, { + // $roleId: roleId + // }); + } + async getRoleByName(roleName: string) { + return await this.engine.roles.findFirst({ + where: { role_name: roleName } + }); + // return await this.engine.runStatementGet(` + // SELECT * FROM roles WHERE role_name = $roleName + // `, { + // $roleName: roleName + // }); + } + async updateRole(roleId: number, roleName: string, description: string) { + await this.engine.roles.update({ + where: { role_id: roleId }, + data: { role_name: roleName, description } + }); + // await this.engine.runStatement(` + // UPDATE roles + // SET role_name = $roleName, description = $description + // WHERE role_id = $roleId + // `, { + // $roleId: roleId, + // $roleName: roleName, + // $description: description + // }); + } + async deleteRole(roleId: number) { + await this.engine.roles.delete({ + where: { role_id: roleId } + }); + // await this.engine.runStatement(` + // DELETE FROM roles WHERE role_id = $roleId + // `, { + // $roleId: roleId + // }); + } + async listRoles() { + return await this.engine.roles.findMany({ + orderBy: { role_name: "asc" } + }); + // return await this.engine.runStatementGetAll(` + // SELECT * FROM roles ORDER BY role_name DESC + // `); + } + // Permission CRUD operations + async createPermission(permissionName: string, description: string) { + const result = await this.engine.permissions.create({ + data: { + permission_name: permissionName, + description + }, + select: { permission_id: true } + }); + return result.permission_id; + // const result = await this.engine.runStatement(` + // INSERT OR IGNORE INTO permissions (permission_name, description) + // VALUES ($permissionName, $description) + // `, { + // $permissionName: permissionName, + // $description: description + // }); + // return result.lastInsertRowid; + } + async getPermission(permissionId: number) { + return await this.engine.permissions.findUnique({ + where: { permission_id: permissionId } + }); + // return await this.engine.runStatementGet(` + // SELECT * FROM permissions WHERE permission_id = $permissionId + // `, { + // $permissionId: permissionId + // }); + } + async getPermissionByName(permissionName: string) { + return await this.engine.permissions.findFirst({ + where: { permission_name: permissionName } + }); + // return await this.engine.runStatementGet(` + // SELECT * FROM permissions WHERE permission_name = $permissionName + // `, { + // $permissionName: permissionName + // }); + } + async updatePermission(permissionId: number, permissionName: string, description: string) { + await this.engine.permissions.update({ + where: { permission_id: permissionId }, + data: { permission_name: permissionName, description } + }); + // await this.engine.runStatement(` + // UPDATE permissions + // SET permission_name = $permissionName, description = $description + // WHERE permission_id = $permissionId + // `, { + // $permissionId: permissionId, + // $permissionName: permissionName, + // $description: description + // }); + } + async deletePermission(permissionId: number) { + await this.engine.permissions.delete({ + where: { permission_id: permissionId } + }); + // await this.engine.runStatement(` + // DELETE FROM permissions WHERE permission_id = $permissionId + // `, { + // $permissionId: permissionId + // }); + } + async listPermissions() { + return await this.engine.permissions.findMany({ + orderBy: { permission_name: "asc" } + }); + // return await this.engine.runStatementGetAll(` + // SELECT * FROM permissions ORDER BY permission_name + // `); + } + // ACL CRUD operations + async createACL(entityName: string, entityType: EntityType, roleId: number, permissionId: number) { + if (entityName.startsWith("$:/")) return; + // No idea why this was insert or replace into because the only thing unique is the acl_id, + // which is not include in this call, so it would never replace. + await this.engine.acl.create({ + data: { + entity_name: entityName, + entity_type: entityType, + role_id: roleId, + permission_id: permissionId + } + }); + // if (!entityName.startsWith("$:/")) { + // const result = await this.engine.runStatement(` + // INSERT OR IGNORE INTO acl (entity_name, entity_type, role_id, permission_id) + // VALUES ($entityName, $entityType, $roleId, $permissionId) + // `, + // { + // $entityName: entityName, + // $entityType: entityType, + // $roleId: roleId, + // $permissionId: permissionId + // }); + // return result.lastInsertRowid; + // } + } + async getACL(aclId: number) { + return await this.engine.acl.findUnique({ + where: { acl_id: aclId } + }); + // return await this.engine.runStatementGet(` + // SELECT * FROM acl WHERE acl_id = $aclId + // `, { + // $aclId: aclId + // }); + } + async updateACL(aclId: number, entityId: string, entityType: EntityType, roleId: number, permissionId: number) { + await this.engine.acl.update({ + where: { acl_id: aclId }, + data: { + entity_name: entityId, + entity_type: entityType, + role_id: roleId, + permission_id: permissionId + } + }); + // await this.engine.runStatement(` + // UPDATE acl + // SET entity_name = $entityId, entity_type = $entityType, + // role_id = $roleId, permission_id = $permissionId + // WHERE acl_id = $aclId + // `, { + // $aclId: aclId, + // $entityId: entityId, + // $entityType: entityType, + // $roleId: roleId, + // $permissionId: permissionId + // }); + } + async deleteACL(aclId: number) { + await this.engine.acl.delete({ + where: { acl_id: aclId } + }); + // await this.engine.runStatement(` + // DELETE FROM acl WHERE acl_id = $aclId + // `, { + // $aclId: aclId + // }); + } + async listACLs() { + return await this.engine.acl.findMany({ + orderBy: [{ entity_type: "asc" }, { entity_name: "asc" }] + }); + // return await this.engine.runStatementGetAll(` + // SELECT * FROM acl ORDER BY entity_type, entity_name + // `); + } + // Association management functions + async addUserToGroup(userId: number, groupId: number) { + await this.engine.user_groups.create({ + data: { + user_id: userId, + group_id: groupId + }, + }); + // await this.engine.runStatement(` + // INSERT OR IGNORE INTO user_groups (user_id, group_id) + // VALUES ($userId, $groupId) + // `, { + // $userId: userId, + // $groupId: groupId + // }); + } + async isUserInGroup(userId: number, groupId: number) { + const result = await this.engine.user_groups.findFirst({ + where: { user_id: userId, group_id: groupId } + }); + return !!result; + // const result = await this.engine.runStatementGet(` + // SELECT 1 FROM user_groups + // WHERE user_id = $userId AND group_id = $groupId + // `, { + // $userId: userId, + // $groupId: groupId + // }); + // return result !== undefined; + } + async removeUserFromGroup(userId: number, groupId: number) { + await this.engine.user_groups.delete({ + where: { user_id_group_id: { user_id: userId, group_id: groupId } } + }); + + // await this.engine.runStatement(` + // DELETE FROM user_groups + // WHERE user_id = $userId AND group_id = $groupId + // `, { + // $userId: userId, + // $groupId: groupId + // }); + } + async addRoleToUser(userId: number, roleId: number) { + await this.engine.user_roles.create({ + data: { + user_id: userId, + role_id: roleId + } + }); + // await this.engine.runStatement(` + // INSERT OR IGNORE INTO user_roles (user_id, role_id) + // VALUES ($userId, $roleId) + // `, { + // $userId: userId, + // $roleId: roleId + // }); + } + async removeRoleFromUser(userId: number, roleId: number) { + await this.engine.user_roles.delete({ + where: { user_id_role_id: { user_id: userId, role_id: roleId } } + }); + // await this.engine.runStatement(` + // DELETE FROM user_roles + // WHERE user_id = $userId AND role_id = $roleId + // `, { + // $userId: userId, + // $roleId: roleId + // }); + } + async addRoleToGroup(groupId: number, roleId: number) { + await this.engine.group_roles.create({ + data: { + group_id: groupId, + role_id: roleId + } + }); + // await this.engine.runStatement(` + // INSERT OR IGNORE INTO group_roles (group_id, role_id) + // VALUES ($groupId, $roleId) + // `, { + // $groupId: groupId, + // $roleId: roleId + // }); + } + async removeRoleFromGroup(groupId: number, roleId: number) { + await this.engine.group_roles.delete({ + where: { group_id_role_id: { group_id: groupId, role_id: roleId } } + }); + // await this.engine.runStatement(` + // DELETE FROM group_roles + // WHERE group_id = $groupId AND role_id = $roleId + // `, { + // $groupId: groupId, + // $roleId: roleId + // }); + } + async addPermissionToRole(roleId: number, permissionId: number) { + await this.engine.role_permissions.create({ + data: { + role_id: roleId, + permission_id: permissionId + } + }); + // await this.engine.runStatement(` + // INSERT OR IGNORE INTO role_permissions (role_id, permission_id) + // VALUES ($roleId, $permissionId) + // `, { + // $roleId: roleId, + // $permissionId: permissionId + // }); + } + async removePermissionFromRole(roleId: number, permissionId: number) { + await this.engine.role_permissions.delete({ + where: { role_id_permission_id: { role_id: roleId, permission_id: permissionId } } + }); + // await this.engine.runStatement(` + // DELETE FROM role_permissions + // WHERE role_id = $roleId AND permission_id = $permissionId + // `, { + // $roleId: roleId, + // $permissionId: permissionId + // }); + } + async getUserRoles(userId: number) { + return await this.engine.user_roles.findFirst({ + where: { user_id: userId }, + select: { role: { select: { role_name: true } } } + }).then(e => e && ({ role_name: e.role.role_name })); + // const query = ` + // SELECT r.role_id, r.role_name + // FROM user_roles ur + // JOIN roles r ON ur.role_id = r.role_id + // WHERE ur.user_id = $userId + // LIMIT 1 + // `; + // return await this.engine.runStatementGet(query, { $userId: userId }); + } + async deleteUserRolesByRoleId(roleId: number) { + await this.engine.user_roles.deleteMany({ + where: { role_id: roleId } + }); + // await this.engine.runStatement(` + // DELETE FROM user_roles + // WHERE role_id = $roleId + // `, { + // $roleId: roleId + // }); + } + async deleteUserRolesByUserId(userId: number) { + await this.engine.user_roles.deleteMany({ + where: { user_id: userId } + }); + // await this.engine.runStatement(` + // DELETE FROM user_roles + // WHERE user_id = $userId + // `, { + // $userId: userId + // }); + } + async isRoleInUse(roleId: number) { + // Check if the role is assigned to any users + // const userRoleCheck = await this.engine.runStatementGet(` + // SELECT 1 + // FROM user_roles + // WHERE role_id = $roleId + // LIMIT 1 + // `, { + // $roleId: roleId + // }); + const userRoleCheck = await this.engine.user_roles.findFirst({ + where: { role_id: roleId } + }); + + if (userRoleCheck) { + return true; + } + + // Check if the role is used in any ACLs + // const aclRoleCheck = await this.engine.runStatementGet(` + // SELECT 1 + // FROM acl + // WHERE role_id = $roleId + // LIMIT 1 + // `, { + // $roleId: roleId + // }); + const aclRoleCheck = await this.engine.acl.findFirst({ + where: { role_id: roleId } + }); + + if (aclRoleCheck) { + return true; + } + + // If we've reached this point, the role is not in use + return false; + } + async getRoleById(roleId: number) { + return await this.engine.roles.findUnique({ + where: { role_id: roleId } + }); + // const role = await this.engine.runStatementGet(` + // SELECT role_id, role_name, description + // FROM roles + // WHERE role_id = $roleId + // `, { + // $roleId: roleId + // }); + + // return role; + } +} diff --git a/plugins/tiddlywiki/multiwikiserver/src/store/sql-tiddler-store.ts b/plugins/tiddlywiki/multiwikiserver/src/store/sql-tiddler-store.ts new file mode 100644 index 00000000000..5a52e0088c7 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/src/store/sql-tiddler-store.ts @@ -0,0 +1,470 @@ +/*\ +title: $:/plugins/tiddlywiki/multiwikiserver/store/sql-tiddler-store.js +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 + +\*/ +"use strict"; +import { PrismaClient } from "@prisma/client"; +import { AttachmentStore } from "./attachments"; +import { SqlTiddlerDatabase } from "./sql-tiddler-database"; +import * as path from "path"; + +type EventListener = (...args: T) => void; + +interface SqlTiddlerStoreEvents { + [x: string]: any[]; + change: []; +} + + +class Eventer> { + constructor(public skipDuplicate: boolean) { + + } + eventListeners: Record[]> = {} as any; + eventOutstanding: Record = {} as any; + addEventListener( + type: K, listener: EventListener + ): void { + this.eventListeners[type] = this.eventListeners[type] || []; + this.eventListeners[type].push(listener); + } + removeEventListener( + type: K, listener: EventListener + ): void { + const listeners = this.eventListeners[type]; + if (listeners) { + var p = listeners.indexOf(listener); + if (p !== -1) { + listeners.splice(p, 1); + } + } + } + dispatchEvent( + type: K, ...args: E[K] + ): void { + const self = this; + if (!this.eventOutstanding[type] || !this.skipDuplicate) { + $tw.utils.nextTick(function () { + self.eventOutstanding[type] = false; + const listeners = self.eventListeners[type]; + if (listeners) { + for (var p = 0; p < listeners.length; p++) { + var listener = listeners[p]; + listener.apply(listener, args); + } + } + }); + this.eventOutstanding[type] = true; + } + } +} + +interface EventSource> { + addEventListener(type: K, listener: EventListener): void; + removeEventListener(type: K, listener: EventListener): void; + dispatchEvent(type: K, ...args: E[K]): void; +} +type TxnType = "DEFERRED" | "IMMEDIATE" | "EXCLUSIVE" | unknown; +type DEFERRED = "DEFERRED" | "IMMEDIATE" | "EXCLUSIVE"; +type IMMEDIATE = "IMMEDIATE" | "EXCLUSIVE"; +type EXCLUSIVE = "EXCLUSIVE"; + +/* +Create a tiddler store. Options include: + +databasePath - path to the database file (can be ":memory:" to get a temporary database) +adminWiki - reference to $tw.Wiki object used for configuration +attachmentStore - reference to associated attachment store +engine - wasm | better +*/ +export class SqlTiddlerStore implements EventSource { + attachmentStore; + adminWiki; + sql: SqlTiddlerDatabase; + transactionType: TXN = "DEFERRED" as any; + + private eventer = new Eventer(true); + addEventListener = this.eventer.addEventListener.bind(this.eventer); + removeEventListener = this.eventer.removeEventListener.bind(this.eventer); + dispatchEvent = this.eventer.dispatchEvent.bind(this.eventer); + + constructor(options: { + // /** path to the database file (can be ":memory:" or missing to get a temporary database) */ + // databasePath?: string; + /** reference to $tw.Wiki object used for configuration */ + adminWiki?: Wiki; + /** reference to associated attachment store */ + attachmentStore: AttachmentStore; + // /** which engine to use, default is "node" */ + // engine?: "node" | "wasm" | "better"; + prisma: PrismaTxnClient; + } = {} as any) { + if (!options?.attachmentStore) { + throw new Error("SqlTiddlerStore requires an attachment store"); + } + this.attachmentStore = options.attachmentStore; + this.adminWiki = options.adminWiki || $tw.wiki; + this.sql = new SqlTiddlerDatabase(options.prisma); + + } + + + /* + Returns null if a bag/recipe name is valid, or a string error message if not + */ + validateItemName(name: string, allowPrivilegedCharacters: boolean = false) { + if (typeof name !== "string") { + return "Not a valid string"; + } + if (name.length > 256) { + return "Too long"; + } + // Removed ~ from this list temporarily + if (allowPrivilegedCharacters) { + if (!(/^[^\s\u00A0\x00-\x1F\x7F`!@#%^&*()+={}\[\];\'\"<>,\\\?]+$/g.test(name))) { + return "Invalid character(s)"; + } + } else { + if (!(/^[^\s\u00A0\x00-\x1F\x7F`!@#$%^&*()+={}\[\];:\'\"<>.,\/\\\?]+$/g.test(name))) { + return "Invalid character(s)"; + } + } + return null; + } + /* + Returns null if the argument is an array of valid bag/recipe names, or a string error message if not + */ + validateItemNames(names: string[], allowPrivilegedCharacters?: boolean) { + if (!$tw.utils.isArray(names)) { + return "Not a valid array"; + } + var errors = []; + for (const name of names) { + const result = this.validateItemName(name, allowPrivilegedCharacters); + if (result && errors.indexOf(result) === -1) { + errors.push(result); + } + } + if (errors.length === 0) { + return null; + } else { + return errors.join("\n"); + } + } + + /* + Given tiddler fields, tiddler_id and a bag_name, return the tiddler fields after the following process: + - Apply the tiddler_id as the revision field + - Apply the bag_name as the bag field + */ + processOutgoingTiddler(tiddlerFields: Record, tiddler_id: any, bag_name: any, attachment_blob: any) { + if (attachment_blob !== null) { + const bagStr = $tw.utils.encodeURIComponentExtended(bag_name); + const titleStr = $tw.utils.encodeURIComponentExtended(tiddlerFields.title); + return $tw.utils.extend( + {}, + tiddlerFields, + { + text: undefined, + _canonical_uri: `/bags/${bagStr}/tiddlers/${titleStr}/blob` + } + ); + } else { + return tiddlerFields; + } + } + /* + */ + processIncomingTiddler(tiddlerFields: Record, existing_attachment_blob: string | null, existing_canonical_uri: string | undefined) { + let attachmentSizeLimit = $tw.utils.parseNumber(this.adminWiki.getTiddlerText("$:/config/MultiWikiServer/AttachmentSizeLimit")); + if (attachmentSizeLimit < 100 * 1024) { + attachmentSizeLimit = 100 * 1024; + } + const attachmentsEnabled = this.adminWiki.getTiddlerText("$:/config/MultiWikiServer/EnableAttachments", "yes") === "yes"; + const contentTypeInfo = $tw.config.contentTypeInfo[tiddlerFields.type || "text/vnd.tiddlywiki"]; + const isBinary = !!contentTypeInfo && contentTypeInfo.encoding === "base64"; + + let shouldProcessAttachment = tiddlerFields.text && tiddlerFields.text.length > attachmentSizeLimit; + + if (existing_attachment_blob) { + const fileSize = this.attachmentStore.getAttachmentFileSize(existing_attachment_blob); + if (fileSize && (fileSize <= attachmentSizeLimit)) { + const existingAttachmentMeta = this.attachmentStore.getAttachmentMetadata(existing_attachment_blob); + const hasCanonicalField = !!tiddlerFields._canonical_uri; + const skipAttachment = hasCanonicalField && (tiddlerFields._canonical_uri === (existingAttachmentMeta ? existingAttachmentMeta._canonical_uri : existing_canonical_uri)); + shouldProcessAttachment = !skipAttachment; + } else { + shouldProcessAttachment = false; + } + } + + if (attachmentsEnabled && isBinary && shouldProcessAttachment) { + const attachment_blob = existing_attachment_blob || this.attachmentStore.saveAttachment({ + text: tiddlerFields.text, + type: tiddlerFields.type, + reference: tiddlerFields.title, + _canonical_uri: tiddlerFields._canonical_uri + }); + + if (tiddlerFields && tiddlerFields._canonical_uri) { + delete tiddlerFields._canonical_uri; + } + + return { + tiddlerFields: Object.assign({}, tiddlerFields, { text: undefined }), + attachment_blob: attachment_blob + }; + } else { + return { + tiddlerFields: tiddlerFields, + attachment_blob: existing_attachment_blob + }; + } + } + /** + * + * @param this Requires a transaction with at least IMMEDIATE isolation + * @param tiddler_files_path `resolve($tw.boot.corePath, $tw.config.editionsPath, tiddler_files_path)` + * @param bag_name + */ + async saveTiddlersFromPath(this: SqlTiddlerStore, tiddler_files_path: string, bag_name: any) { + // Clear out the bag + await this.deleteAllTiddlersInBag(bag_name); + // Get the tiddlers + var tiddlersFromPath = $tw.loadTiddlersFromPath(path.resolve($tw.boot.corePath, $tw.config.editionsPath, tiddler_files_path)); + // Save the tiddlers + for (const tiddlersFromFile of tiddlersFromPath) { + for (const tiddler of tiddlersFromFile.tiddlers) { + await this.saveBagTiddler(tiddler, bag_name); + } + } + this.dispatchEvent("change"); + } + async listBags() { + return await this.sql.listBags(); + } + /* + Options include: + + allowPrivilegedCharacters - allows "$", ":" and "/" to appear in recipe name + */ + async createBag(bag_name: string, description: string, options?: { allowPrivilegedCharacters?: boolean; }) { + options = options || {}; + var self = this; + const validationBagName = self.validateItemName(bag_name, options.allowPrivilegedCharacters); + if (validationBagName) { + return { message: validationBagName }; + } + await self.sql.createBag(bag_name, description); + self.dispatchEvent("change"); + return null; + } + async listRecipes() { + return await this.sql.listRecipes(); + } + /* + Returns null on success, or {message:} on error + + Options include: + + allowPrivilegedCharacters - allows "$", ":" and "/" to appear in recipe name + */ + async createRecipe(recipe_name: string, bag_names: string[], description: string, options?: { allowPrivilegedCharacters?: boolean; }) { + bag_names = bag_names || []; + description = description || ""; + options = options || {}; + const validationRecipeName = this.validateItemName(recipe_name, options.allowPrivilegedCharacters); + if (validationRecipeName) { + return { message: validationRecipeName }; + } + if (bag_names.length === 0) { + return { message: "Recipes must contain at least one bag" }; + } + + // return await this.sql.transaction(async function () { + await this.sql.createRecipe(recipe_name, bag_names, description); + this.dispatchEvent("change"); + return null; + // }); + } + /* + Returns {tiddler_id:} + */ + async saveBagTiddler(incomingTiddlerFields: Record, bag_name: string) { + let _canonical_uri; + const existing_attachment_blob = await this.sql.getBagTiddlerAttachmentBlob(incomingTiddlerFields.title, bag_name); + if (existing_attachment_blob) { + _canonical_uri = `/bags/${$tw.utils.encodeURIComponentExtended(bag_name)}/tiddlers/${$tw.utils.encodeURIComponentExtended(incomingTiddlerFields.title)}/blob`; + } + const { tiddlerFields, attachment_blob } = this.processIncomingTiddler(incomingTiddlerFields, existing_attachment_blob, _canonical_uri); + const result = await this.sql.saveBagTiddler(tiddlerFields, bag_name, attachment_blob); + this.dispatchEvent("change"); + return result; + } + /* + Create a tiddler in a bag adopting the specified file as the attachment. The attachment file must be on the same disk as the attachment store + Options include: + + filepath - filepath to the attachment file + hash - string hash of the attachment file + type - content type of file as uploaded + + Returns {tiddler_id:} + */ + async saveBagTiddlerWithAttachment( + this: SqlTiddlerStore, + incomingTiddlerFields: Record, + bag_name: string, + options: { + filepath: any; type: any; hash: any; _canonical_uri: any; + } + ) { + const attachment_blob = this.attachmentStore.adoptAttachment(options.filepath, options.type, options.hash, options._canonical_uri); + if (attachment_blob) { + const result = await this.sql.saveBagTiddler(incomingTiddlerFields, bag_name, attachment_blob); + this.dispatchEvent("change"); + return result; + } else { + return null; + } + } + /* + Returns {tiddler_id:,bag_name:} + */ + async saveRecipeTiddler(incomingTiddlerFields: Record, recipe_name: string) { + const existing_attachment_blob = await this.sql.getRecipeTiddlerAttachmentBlob( + incomingTiddlerFields.title, recipe_name); + + const { tiddlerFields, attachment_blob } = this.processIncomingTiddler( + incomingTiddlerFields, existing_attachment_blob, incomingTiddlerFields._canonical_uri); + + const result = await this.sql.saveRecipeTiddler(tiddlerFields, recipe_name, attachment_blob); + this.dispatchEvent("change"); + return result; + } + async deleteTiddler(title: string, bag_name: string) { + const result = await this.sql.deleteTiddler(title, bag_name); + this.dispatchEvent("change"); + return result; + } + /* + returns {tiddler_id:,tiddler:} + */ + async getBagTiddler(title: string, bag_name: string) { + var tiddlerInfo = await this.sql.getBagTiddler(title, bag_name); + if (tiddlerInfo) { + return await Object.assign( + {}, + tiddlerInfo, + { + tiddler: this.processOutgoingTiddler(tiddlerInfo.tiddler, tiddlerInfo.tiddler_id, bag_name, tiddlerInfo.attachment_blob) + }); + } else { + return null; + } + } + /* + Get an attachment ready to stream. Returns null if there is an error or: + tiddler_id: revision of tiddler + stream: stream of file + type: type of file + Returns {tiddler_id:,bag_name:} + */ + async getBagTiddlerStream(title: string, bag_name: string) { + const tiddlerInfo = await this.sql.getBagTiddler(title, bag_name); + if (tiddlerInfo) { + if (tiddlerInfo.attachment_blob) { + return $tw.utils.extend( + {}, + this.attachmentStore.getAttachmentStream(tiddlerInfo.attachment_blob), + { + tiddler_id: tiddlerInfo.tiddler_id, + bag_name: bag_name + } + ); + } else { + const { Readable } = require('stream'); + const stream = new Readable(); + stream._read = function () { + // Push data + const type = tiddlerInfo.tiddler.type || "text/plain"; + stream.push(tiddlerInfo.tiddler.text || "", ($tw.config.contentTypeInfo[type] || { encoding: "utf8" }).encoding); + // Push null to indicate the end of the stream + stream.push(null); + }; + return { + tiddler_id: tiddlerInfo.tiddler_id, + bag_name: bag_name, + stream: stream, + type: tiddlerInfo.tiddler.type || "text/plain" + }; + } + } else { + return null; + } + } + /* + Returns {bag_name:, tiddler: {fields}, tiddler_id:} + */ + async getRecipeTiddler(title: string, recipe_name: string) { + var tiddlerInfo = await this.sql.getRecipeTiddler(title, recipe_name); + if (tiddlerInfo) { + return Object.assign({}, tiddlerInfo, { + tiddler: this.processOutgoingTiddler(tiddlerInfo.tiddler, tiddlerInfo.tiddler_id, tiddlerInfo.bag_name, tiddlerInfo.attachment_blob) + }); + } else { + return null; + } + } + /* + Get the titles of the tiddlers in a bag. Returns an empty array for bags that do not exist + */ + async getBagTiddlers(bag_name: string) { + return await this.sql.getBagTiddlers(bag_name); + } + /* + Get the tiddler_id of the newest tiddler in a bag. Returns null for bags that do not exist + */ + async getBagLastTiddlerId(bag_name: string) { + return await this.sql.getBagLastTiddlerId(bag_name); + } + /* + Get the titles of the tiddlers in a recipe as {title:,bag_name:}. Returns null for recipes that do not exist + */ + async getRecipeTiddlers(recipe_name: string, options: { limit?: number; last_known_tiddler_id?: number; include_deleted?: boolean; } = {}) { + return await this.sql.getRecipeTiddlers(recipe_name, options); + } + /* + Get the tiddler_id of the newest tiddler in a recipe. Returns null for recipes that do not exist + */ + async getRecipeLastTiddlerId(recipe_name: string) { + return await this.sql.getRecipeLastTiddlerId(recipe_name); + } + async deleteAllTiddlersInBag(bag_name: string) { + var self = this; + // return await this.sql.transaction(async function () { + const result = await self.sql.deleteAllTiddlersInBag(bag_name); + self.dispatchEvent("change"); + return result; + // }); + } + /* + Get the names of the bags in a recipe. Returns an empty array for recipes that do not exist + */ + async getRecipeBags(recipe_name: any) { + return await this.sql.getRecipeBags(recipe_name); + } +} + + + diff --git a/plugins/tiddlywiki/multiwikiserver/test.ts b/plugins/tiddlywiki/multiwikiserver/test.ts new file mode 100644 index 00000000000..c47d41c22b5 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/test.ts @@ -0,0 +1,181 @@ +import prisma from "@prisma/client"; + + +// const utils = require("@prisma/driver-adapter-utils"); +import { Database } from "node-sqlite3-wasm"; +import { readFileSync } from "fs"; +const db = new Database(":memory:", {}); +console.log(db.exec(readFileSync("./schema.prisma.sql", "utf-8"))); +console.log(db.run(` + + -- CreateTable +CREATE TABLE "everything" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "String" TEXT NOT NULL, + "Boolean" BOOLEAN NOT NULL, + "Int" INTEGER NOT NULL, + "BigInt" BIGINT NOT NULL, + "Float" REAL NOT NULL, + "Decimal" DECIMAL NOT NULL, + "DateTime" DATETIME NOT NULL, + "Json" JSONB NOT NULL, + "Bytes" BLOB NOT NULL, + "Enum" TEXT NOT NULL +); + +`)) +console.log(db.all("select * from 'everything';")); +const query = { + sql: 'INSERT INTO `users` (`username`, `email`, `password`, `created_at`) VALUES (?,?,?,?) RETURNING `user_id` AS `user_id`, `username` AS `username`, `email` AS `email`, `password` AS `password`, `created_at` AS `created_at`, `last_login` AS `last_login`', + args: [Date.now().toString(), Date.now().toString(), Date.now().toString(), "datetime('now')"], + argTypes: ['Text', 'Text', 'Text', 'Text'] +}; +enum ColumnType { + INTEGER = 1, + FLOAT = 2, + TEXT = 3, + BLOB = 4, + NULL = 5, +} +// String TEXT +// Boolean BOOLEAN +// Int INTEGER +// BigInt INTEGER +// Float REAL +// Decimal DECIMAL +// DateTime NUMERIC +// Json JSONB +// Bytes BLOB +// Enum TEXT +const stmt = db.prepare(query.sql); +// console.log(stmt._getColumnNames()); +// console.log(stmt._getColumnTypes().map((type) => ColumnType[type])); + +console.log(ColumnType); +console.log(stmt.all(query.args)); + +class Result_4 { + ok: S; + value: T; + constructor(ok: S, value: T) { + this.ok = ok; + this.value = value; + } + map(fn: (value: T) => U): Result_4 { + return new Result_4(this.ok, fn(this.value)); + } + flatMap(fn: (value: T) => Result_4): Result_4 { + return fn(this.value); + } +} +const t = { + sql: 'INSERT INTO `main`.`everything` (`id`, `String`, `Boolean`, `Int`, `BigInt`, `Float`, `Decimal`, `DateTime`, `Json`, `Bytes`, `Enum`) VALUES (?,?,?,?,?,?,?,?,?,?,?) RETURNING `id` AS `id`, `String` AS `String`, `Boolean` AS `Boolean`, `Int` AS `Int`, `BigInt` AS `BigInt`, `Float` AS `Float`, `Decimal` AS `Decimal`, `DateTime` AS `DateTime`, `Json` AS `Json`, `Bytes` AS `Bytes`, `Enum` AS `Enum`', + args: [ + 1736630219114, + 'test', + true, + 1, + 1, + 1.1, + 1.1, + '2025-01-11T21:16:59.114+00:00', + '{"test":1}', + Uint8Array.from([ 1, 2, 3 ]) , + 'test' + ], + argTypes: [ + 'Int64', + 'Text', + 'Boolean', + 'Int64', + 'Int64', + 'Numeric', + 'Numeric', + 'DateTime', + 'Json', + 'Bytes', + 'Enum' + ] +}; + +const client = new prisma.PrismaClient({ + adapter: { + adapterName: "sqlite", + provider: "sqlite", + executeRaw: async (query) => { + + }, + queryRaw: async (query) => { + console.log(query); + const argTypeMap: Record = { + Array: ColumnType.BLOB, + Boolean: ColumnType.INTEGER, + Date: ColumnType.INTEGER, + Bytes: ColumnType.BLOB, + Char: ColumnType.TEXT, + DateTime: ColumnType.INTEGER, + Double: ColumnType.FLOAT, + Float: ColumnType.FLOAT, + Enum: ColumnType.TEXT, + EnumArray: ColumnType.BLOB, + Int32: ColumnType.INTEGER, + Int64: ColumnType.INTEGER, + Json: ColumnType.TEXT, + Numeric: ColumnType.FLOAT, + Text: ColumnType.TEXT, + Time: ColumnType.INTEGER, + Uuid: ColumnType.TEXT, + Xml: ColumnType.TEXT, + } + const rows = db.prepare(query.sql).all(query.args as any[]); + // console.log(stmt._getColumnNames()); + const columnNames = rows.length > 0 ? Object.keys(rows[0]) : []; + const columnTypes = rows.length > 0 ? Object.values(rows[0]).map((value) => { + if (typeof value === "bigint" || typeof value === "number" && Number.isInteger(value)) { + return ColumnType.INTEGER; + } else if (typeof value === "number") { + return ColumnType.FLOAT; + } else if (typeof value === "string") { + return ColumnType.TEXT; + } else if (value instanceof Uint8Array) { + return ColumnType.BLOB; + } else if(value == null) { + return ColumnType.NULL; + } else { + console.log(value, typeof value); + } + }) : []; + + const lastInsertId = db.prepare("SELECT last_insert_rowid() insert_id;").get()?.insert_id ?? 0; + return new Result_4(true, { columnNames, columnTypes, rows, lastInsertId }); + }, + transactionContext: async (callback) => { + throw ""; + return callback(); + } + } +}); + +Promise.resolve().then(async () => { + + await client.everything.create({ + data: { + BigInt: BigInt(1), + Boolean: true, + Bytes: new Uint8Array([1, 2, 3]), + DateTime: new Date(), + Decimal: 1.1, + Enum: "test", + Float: 1.1, + Int: 1, + Json: { test: 1 }, + String: "test", + id: Date.now() + } + }); +}); + +process.on("unhandledException", (err) => { + console.log(err); + process.exit(1); +}); \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/tsconfig.build.json b/plugins/tiddlywiki/multiwikiserver/tsconfig.build.json new file mode 100644 index 00000000000..29de0ac2806 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./modules", + "rootDir": "./src", + "allowJs": false, + "checkJs": false, + "noEmit": false, + "sourceMap": true, + }, + "exclude": ["*.js"] +} \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/tsconfig.json b/plugins/tiddlywiki/multiwikiserver/tsconfig.json new file mode 100644 index 00000000000..5cffd4b4150 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "noEmit": true, + "target": "ES2017", + "module": "commonjs", + "moduleResolution": "node", + "lib": ["ESNext"], + "paths": { + "$:/plugins/tiddlywiki/multiwikiserver/auth/*": [ + "./auth/*" + ], + "$:/plugins/tiddlywiki/multiwikiserver/routes/*": [ + "./modules/routes/*" + ], + "$:/plugins/tiddlywiki/multiwikiserver/*": [ + "./src/*" + ], + }, + "types":["node"], + "strict": true, + "checkJs": true, + "allowJs": true, + }, + "include": [ + "./src/globals.d.ts", + "./src/**/*.ts", + "./**/*.js", + ], + +} \ No newline at end of file