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..12512e6d3b3 100644 --- a/boot/boot.js +++ b/boot/boot.js @@ -928,7 +928,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 +2117,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 +105,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 +137,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 +188,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 +202,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 +214,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 +305,284 @@ "node": ">=18" } }, + "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==", + "dev": true, + "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 +704,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", @@ -402,6 +769,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 +809,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 +823,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 +875,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 +905,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 +927,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 +1059,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 +1121,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 +1147,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 +1242,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 +1347,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 +1517,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 +1693,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 +1775,32 @@ "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/pump": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", @@ -1194,6 +1819,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 +1861,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 +1889,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 +2016,36 @@ "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/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 +2110,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 +2157,47 @@ "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==", + "dev": true + }, "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..fb1f19fe76a 100644 --- a/package.json +++ b/package.json @@ -24,17 +24,20 @@ "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": { "node": ">=0.8.2" }, "scripts": { - "start": "node ./tiddlywiki.js ./editions/multiwikiserver --mws-load-plugin-bags --build load-mws-demo-data --mws-listen", + "start": "node ./tiddlywiki.js ./editions/multiwikiserver --mws-load-plugin-bags --build load-mws-demo-data --mws-listen host=0.0.0.0", "build:test-edition": "node ./tiddlywiki.js ./editions/test --verbose --version --build index", "test:multiwikiserver-edition": "node ./tiddlywiki.js ./editions/multiwikiserver/ --build load-mws-demo-data --mws-listen --mws-test-server http://127.0.0.1:8080/ --quit", "mws-add-user": "node ./tiddlywiki.js ./editions/multiwikiserver --build load-mws-demo-data --mws-listen --build mws-add-user --quit", diff --git a/plugins/tiddlywiki/multiwikiserver/eslint.config.js b/plugins/tiddlywiki/multiwikiserver/eslint.config.js new file mode 100644 index 00000000000..41490a64eeb --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/eslint.config.js @@ -0,0 +1,426 @@ +//@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"); +const AlwaysAwaitRule = { + 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; + } + }, +}; + +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/", + ] + }, + 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: "./jsconfig.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-await": "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", + }, + } +); diff --git a/plugins/tiddlywiki/multiwikiserver/globals.d.ts b/plugins/tiddlywiki/multiwikiserver/globals.d.ts new file mode 100644 index 00000000000..bc54df250c9 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/globals.d.ts @@ -0,0 +1,72 @@ +import { SqlTiddlerDatabase } from "./modules/store/sql-tiddler-database"; +import { IncomingMessage as HTTPIncomingMessage } from "http"; +declare global { + const $tw: { + loadTiddlersFromPath: any; + loadPluginFolder: any; + getLibraryItemSearchPaths: any; + wiki: $TW.Wiki; + utils: any; + boot: any; + config: any; + node: any; + modules: any; + hooks: any; + sjcl: any; + Wiki: { new(): $TW.Wiki }; + Tiddler: { new(fields: Record): $TW.Tiddler }; + mws: { + serverManager: ServerManager; + store: SqlTiddlerStore + } + } + + namespace $TW { + interface Wiki extends Record { + + } + interface Boot { + + } + interface Tiddler { + + } + } + type SqlTiddlerDatabase = import("./modules/store/sql-tiddler-database").SqlTiddlerDatabase; + type SqlTiddlerStore = import("./modules/store/sql-tiddler-store").SqlTiddlerStore; + type Server = import("./modules/mws-server.js").Server; + type ServerState = Awaited>; + interface IncomingMessage extends HTTPIncomingMessage { + url: string + }; + type ServerResponse = import("http").ServerResponse; + interface ServerRoute { + path: RegExp; + handler: ServerRouteHandler; + method?: string; + useACL?: boolean; + /** this is required if useACL is true */ + entityName?: string; + csrfDisable?: boolean; + bodyFormat?: string; + } + interface ServerOptions { + sqlTiddlerDatabase?: SqlTiddlerDatabase; + variables?: Server["defaultVariables"]; + authenticators?: unknown[]; + routes?: []; + wiki?: any; + boot?: any; + } + + interface ServerRouteHandler { + (this: ServerRoute, req: IncomingMessage, res: ServerResponse, state: ServerState): Promise; + } + +} +class ServerManager { + constructor(); + createServer(options: ServerOptions); +} + +export { }; \ 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..4508f6c82d8 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/jsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2015", + "module": "commonjs", + "moduleResolution": "node", + "paths": { + "$:/plugins/tiddlywiki/multiwikiserver/auth/*": [ + "./auth/*" + ], + "$:/plugins/tiddlywiki/multiwikiserver/*": [ + "./modules/*" + ], + }, + "checkJs": true, + "strictNullChecks": true, + + }, + "include": [ + "./**/*.js", + "./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..c8f200fb09b 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-permission.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-permission.js @@ -23,7 +23,7 @@ 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) { @@ -37,7 +37,7 @@ Command.prototype.execute = function() { var permission_name = this.params[0]; var description = this.params[1]; - $tw.mws.store.sqlTiddlerDatabase.createPermission(permission_name, description); + await $tw.mws.store.sqlTiddlerDatabase.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..07dfdf6163a 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-role.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-role.js @@ -23,7 +23,7 @@ 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) { @@ -37,7 +37,7 @@ Command.prototype.execute = function() { var role_name = this.params[0]; var description = this.params[1]; - $tw.mws.store.sqlTiddlerDatabase.createRole(role_name, description); + await $tw.mws.store.sqlTiddlerDatabase.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..e2f952aba5e 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-user.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-add-user.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; if(this.params.length < 2) { @@ -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.sqlTiddlerDatabase.getUserByUsername(username); if(!user) { - $tw.mws.store.sqlTiddlerDatabase.createUser(username, email, hashedPassword); + await $tw.mws.store.sqlTiddlerDatabase.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..b4a44a5c3bc 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-assign-role-permission.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-assign-role-permission.js @@ -23,7 +23,7 @@ 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) { @@ -36,8 +36,8 @@ Command.prototype.execute = function() { 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.sqlTiddlerDatabase.getRoleByName(role_name); + var permission = await $tw.mws.store.sqlTiddlerDatabase.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.sqlTiddlerDatabase.getPermissionByName(permission_name); - $tw.mws.store.sqlTiddlerDatabase.addPermissionToRole(role.role_id, permission.permission_id); + await $tw.mws.store.sqlTiddlerDatabase.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..76102d5d449 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-assign-user-role.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-assign-user-role.js @@ -23,7 +23,7 @@ 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) { @@ -36,8 +36,8 @@ Command.prototype.execute = function() { 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.sqlTiddlerDatabase.getRoleByName(role_name); + var user = await $tw.mws.store.sqlTiddlerDatabase.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.sqlTiddlerDatabase.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-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-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 void} options.cbPartStart + * @param {(chunk: Buffer) => void} options.cbPartChunk + * @param {() => void} options.cbPartEnd + * @param {(err: string | null) => void} options.cbFinished + */ 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")) { + if(!contentType?.startsWith("multipart/form-data")) { return options.cbFinished("Expected multipart/form-data content type"); } // Extract the boundary string from the Content-Type header @@ -215,6 +236,7 @@ function streamMultipartData(request,options) { } // 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(": "); @@ -222,7 +244,9 @@ function streamMultipartData(request,options) { }); // Parse the content disposition header const contentDisposition = { + /** @type {string | null} */ name: null, + /** @type {string | null} */ filename: null }; if(currentHeaders["content-disposition"]) { @@ -292,16 +316,54 @@ Server.prototype.defaultVariables = { "system-tiddler-render-type": "text/plain", "system-tiddler-render-template": "$:/core/templates/wikified-tiddler", "debug-level": "none", + /** @type {"yes" | "no"} */ "gzip": "no", - "use-browser-cache": "no" -}; + /** @type {"yes" | "no"} */ + "use-browser-cache": "no", + "path-prefix": "", + /** @type {"yes" | "no"} */ + "csrf-disable": "no", + /** @type {string | undefined} */ + username: undefined, + /** @type {string | undefined} */ + password: undefined, + /** @type {string | undefined} */ + credentials: undefined, + /** @type {string | undefined} */ + readers: undefined, + /** @type {string | undefined} */ + writers: undefined, + /** @type {string | undefined} */ + admin: undefined, + /** @type {string | undefined} TLS Private Key file path */ + "tls-key": undefined, + /** @type {string | undefined} TLS Public Cert file path */ + "tls-cert": undefined, + /** @type {string | undefined} TLS Private Key passphrase */ + "tls-passphrase": undefined, + /** @type {string | undefined} Server name, mostly for 403 errors */ + "server-name": undefined, + /** @type {string | undefined} the expected origin header */ + "origin": undefined, +}; +/** + * @template {keyof Server["defaultVariables"]} K + * @param {K} name + * @returns {Server["defaultVariables"][K]} + */ Server.prototype.get = function(name) { return this.variables[name]; }; - -Server.prototype.addRoute = function(route) { - this.routes.push(route); +/** + * + * @param {ServerRoute} route + * @param {string} title + */ +Server.prototype.addRoute = function(route, title) { + if(!route.path) $tw.utils.log("Warning: Route has no path: " + title); + else if(route.useACL && !route.entityName) $tw.utils.log("Warning: Route has no entityName: " + title); + else this.routes.push(route); }; Server.prototype.addAuthenticator = function(AuthenticatorClass) { @@ -315,9 +377,14 @@ Server.prototype.addAuthenticator = function(AuthenticatorClass) { this.authenticators.push(authenticator); } }; - -Server.prototype.findMatchingRoute = function(request,state) { - for(var t=0; t { - const parts = cookie.split('='); + const parts = cookie.split('='); if (parts.length >= 2) { - const key = parts[0].trim(); - const value = parts.slice(1).join('=').trim(); - cookies[key] = decodeURIComponent(value); - } + const key = parts[0].trim(); + const value = parts.slice(1).join('=').trim(); + cookies[key] = decodeURIComponent(value); + } }); return cookies; } -Server.prototype.authenticateUser = function(request, response) { +Server.prototype.authenticateUser = async function(request, response) { const {session: session_id} = this.parseCookieString(request.headers.cookie) if (!session_id) { - return false; + return null; } // get user info - const user = this.sqlTiddlerDatabase.findUserBySessionId(session_id); + const user = await this.sqlTiddlerDatabase.findUserBySessionId(session_id); if (!user) { - return false + return null; } delete user.password; - const userRole = this.sqlTiddlerDatabase.getUserRoles(user.user_id); - user['isAdmin'] = userRole?.role_name?.toLowerCase() === 'admin' - user['sessionId'] = session_id + const userRole = await this.sqlTiddlerDatabase.getUserRoles(user.user_id); - return user + return { + ...user, + isAdmin: userRole?.role_name?.toLowerCase() === 'admin', + sessionId: session_id, + password: undefined, // for typing + }; }; Server.prototype.requestAuthentication = function(response) { @@ -424,35 +494,36 @@ Server.prototype.getAnonymousAccessConfig = function() { showAnonConfig: showAnonymousAccessModal === "yes" }; } - - -Server.prototype.requestHandler = function(request,response,options) { - options = options || {}; - const queryString = require("querystring"); +/** + * + * @param {IncomingMessage} request + * @param {ServerResponse} response + * @param {*} options + */ +Server.prototype.makeRequestState = async function(request, response, options) { // Authenticate the user - const authenticatedUser = this.authenticateUser(request, response); + const authenticatedUser = await 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.wiki = options.wiki || this.wiki; + state.boot = options.boot || this.boot; + state.server = this; state.urlInfo = url.parse(request.url); - state.queryParameters = querystring.parse(state.urlInfo.query); + state.queryParameters = state.urlInfo.query ? 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.sendResponse = sendResponse.bind(this,request,response); + state.redirect = redirect.bind(this,request,response); + state.streamMultipartData = streamMultipartData.bind(this,request); + state.makeTiddlerEtag = makeTiddlerEtag.bind(this); 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(); @@ -461,7 +532,24 @@ Server.prototype.requestHandler = function(request,response,options) { state.allowAnonReads = allowReads; state.allowAnonWrites = allowWrites; state.showAnonConfig = !!state.authenticatedUser?.isAdmin && showAnonConfig; - state.firstGuestUser = this.sqlTiddlerDatabase.listUsers().length === 0 && !state.authenticatedUser; + state.firstGuestUser = (await this.sqlTiddlerDatabase.listUsers()).length === 0 && !state.authenticatedUser; + /** @type {any} */ + state.data = ""; + return state; +} +/** + * + * @param {IncomingMessage} request + * @param {ServerResponse} response + * @param {*} options + * @returns + */ +Server.prototype.requestHandler = async function(request,response,options) { + options = options || {}; + var self = this; + const queryString = require("querystring"); + // Compose the state object + const state = await this.makeRequestState(request, response, options); // Authorize with the authenticated username if(!this.isAuthorized(state.authorizationType,state.authenticatedUsername) && !response.headersSent) { @@ -471,28 +559,33 @@ Server.prototype.requestHandler = function(request,response,options) { } // Find the route that matches this path - var route = self.findMatchingRoute(request,state); + var route = this.findMatchingRoute(request,state); + + // Return a 404 if we didn't find a route + if(!route) { + response.writeHead(404); + response.end(); + return; + } // If the route is configured to use ACL middleware, check that the user has permission if(route?.useACL) { + if(!route.entityName) { + response.writeHead(500); + response.end(); + return; + } const permissionName = this.methodACLPermMappings[route.method]; - aclMiddleware(request,response,state,route.entityName,permissionName) + await aclMiddleware(request,response,state,route.entityName,permissionName) } - + // Optionally output debug info - if(self.get("debug-level") !== "none") { + 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); } - - // 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 + "'"); @@ -503,30 +596,39 @@ Server.prototype.requestHandler = function(request,response,options) { // 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); + await 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); - }) + await /** @type {Promise} */(new Promise((resolve) => { + /** @type {any} */ + var data = ""; + request.on("data", function (chunk) { + data += chunk.toString(); + }); + request.on("end", function () { + ok(route); + if (route.bodyFormat === "www-form-urlencoded") { + data = queryString.parse(data); + } + state.data = data; + resolve(); + }); + })); + await route.handler(request,response,state); + } else if (route.bodyFormat === "buffer") { + await /** @type {Promise} */(new Promise((resolve) => { + /** @type {any} */ + var data = []; + request.on("data", function (chunk) { + data.push(chunk); + }); + request.on("end", function () { + state.data = Buffer.concat(data); + resolve(); + }) + })); + await route.handler(request, response, state); } else { response.writeHead(400,"Invalid bodyFormat " + route.bodyFormat + " in route " + route.method + " " + route.path.source); response.end(); @@ -568,9 +670,9 @@ Server.prototype.listen = function(port,host,prefix,options) { 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); + void self.requestHandler(request,response,options).catch(console.error); }); // 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() { @@ -579,8 +681,9 @@ Server.prototype.listen = function(port,host,prefix,options) { server.close(); }); // Log listening details - var address = server.address(), - url = self.protocol + "://" + (address.family === "IPv6" ? "[" + address.address + "]" : address.address) + ":" + address.port + prefix; + var address = server.address(); + ok(typeof address === "object" && address, "Expected server.address() to return an object"); + var 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) { @@ -593,4 +696,22 @@ Server.prototype.listen = function(port,host,prefix,options) { exports.Server = Server; + +class ServerManager { + constructor() { + this.servers = []; + } + + /** + * @param {ServerOptions} options + */ + createServer(options) { + const server = new Server(options); + this.servers.push(server); + return server; + } +} + +exports.ServerManager = ServerManager; + })(); 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..8bf86acbaa4 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/change-user-password.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/change-user-password.js @@ -21,7 +21,8 @@ exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; -exports.handler = function (request, response, state) { +/** @type {ServerRouteHandler} */ +exports.handler = async 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"); @@ -65,7 +66,7 @@ exports.handler = function (request, response, state) { return; } - var userData = state.server.sqlTiddlerDatabase.getUser(userId); + var userData = await state.server.sqlTiddlerDatabase.getUser(userId); if(!userData) { $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ @@ -78,7 +79,7 @@ exports.handler = function (request, response, state) { } var newHash = auth.hashPassword(newPassword); - var result = state.server.sqlTiddlerDatabase.updateUserPassword(userId, newHash); + var result = await state.server.sqlTiddlerDatabase.updateUserPassword(userId, newHash); $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/change-password/" + userId + "/success", diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-acl.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-acl.js index a4c4768e4e5..7e170ea5488 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-acl.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-acl.js @@ -12,7 +12,8 @@ POST /admin/delete-acl /*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,16 +24,17 @@ POST /admin/delete-acl exports.csrfDisable = true; - exports.handler = function (request, response, state) { + /** @type {ServerRouteHandler} */ + exports.handler = async 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"); + await aclMiddleware(request, response, state, entity_type, "WRITE"); - sqlTiddlerDatabase.deleteACL(acl_id); + await sqlTiddlerDatabase.deleteACL(acl_id); 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..33bfdf8e009 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,20 @@ 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} */ +exports.handler = async function(request,response,state) { + await aclMiddleware(request, response, state, "bag", "WRITE"); // 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 $tw.mws.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..6e69ab67cfd 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-role.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-role.js @@ -19,8 +19,8 @@ POST /admin/delete-role exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; - - exports.handler = function (request, response, state) { + /** @type {ServerRouteHandler} */ + exports.handler = async function (request, response, state) { var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase; var role_id = state.data.role_id; @@ -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..7143e8fd050 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-user-account.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-user-account.js @@ -19,8 +19,8 @@ exports.path = /^\/delete-user-account\/?$/; exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; - -exports.handler = function (request, response, state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function (request, response, state) { var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase; var userId = state.data.userId; @@ -47,7 +47,7 @@ exports.handler = function (request, response, state) { } // Check if the user exists - var user = sqlTiddlerDatabase.getUser(userId); + var user = await sqlTiddlerDatabase.getUser(userId); if(!user) { $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/delete-user/error", @@ -59,7 +59,7 @@ exports.handler = function (request, response, state) { } // Check if this is the last admin account - var adminRole = sqlTiddlerDatabase.getRoleByName("ADMIN"); + var adminRole = await sqlTiddlerDatabase.getRoleByName("ADMIN"); if(!adminRole) { $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/delete-user/error", @@ -70,7 +70,7 @@ exports.handler = function (request, response, state) { return; } - var adminUsers = sqlTiddlerDatabase.listUsersByRoleId(adminRole.role_id); + var adminUsers = await sqlTiddlerDatabase.listUsersByRoleId(adminRole.role_id); if(adminUsers.length <= 1 && adminUsers.some(admin => admin.user_id === parseInt(userId))) { $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/delete-user/error", @@ -81,9 +81,9 @@ exports.handler = function (request, response, state) { return; } - sqlTiddlerDatabase.deleteUserRolesByUserId(userId); - sqlTiddlerDatabase.deleteUserSessions(userId); - sqlTiddlerDatabase.deleteUser(userId); + await sqlTiddlerDatabase.deleteUserRolesByUserId(userId); + await sqlTiddlerDatabase.deleteUserSessions(userId); + await sqlTiddlerDatabase.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..b441af2b51e 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) { +/** @type {ServerRouteHandler} */ +exports.handler = async function (request, response, state) { var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase; 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.server.sqlTiddlerDatabase.listRoles(); + var permissions = await state.server.sqlTiddlerDatabase.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,29 +58,33 @@ 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 }) }); 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..e3c7d459e2c 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,19 +12,19 @@ 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} */ +exports.handler = async function(request,response,state) { + await aclMiddleware(request, response, state, "bag", "READ"); // 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 $tw.mws.store.getBagTiddlerStream(title,bag_name); if(result && !response.headersSent) { response.writeHead(200, "OK",{ Etag: state.makeTiddlerEtag(result), 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..7a0f8fb3aa5 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,18 @@ 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} */ +exports.handler = async function(request,response,state) { + await aclMiddleware(request, response, state, "bag", "READ"); // 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 $tw.mws.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 +38,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 $tw.mws.store.getBagTiddlerStream(title,bag_name); if(result) { if(!response.headersSent){ response.writeHead(200, "OK",{ diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag.js index 7d262b83fcf..60285d393e6 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag.js @@ -20,8 +20,8 @@ exports.path = /^\/bags\/([^\/]+)(\/?)$/; exports.useACL = true; exports.entityName = "bag" - -exports.handler = function (request, response, state) { +/** @type {ServerRouteHandler} */ +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 + "/"); @@ -29,7 +29,7 @@ exports.handler = function (request, response, state) { } // 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 $tw.mws.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) { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-index.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-index.js index 1b5dc96fe6f..d06efd017fd 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,59 @@ GET /?show_system=true exports.method = "GET"; exports.path = /^\/$/; - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function(request,response,state) { // Get the bag and recipe information - var bagList = $tw.mws.store.listBags(), - recipeList = $tw.mws.store.listRecipes(), + var bagList = await $tw.mws.store.listBags(), + recipeList = await $tw.mws.store.listRecipes(), sqlTiddlerDatabase = state.server.sqlTiddlerDatabase; // 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 sqlTiddlerDatabase.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 sqlTiddlerDatabase.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 sqlTiddlerDatabase.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",{ variables: { "show-system": state.queryParameters.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 +81,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..8b524067395 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-login.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-login.js @@ -15,10 +15,10 @@ GET /login exports.method = "GET"; exports.path = /^\/login$/; - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function(request,response,state) { // Check if the user already has a valid session - var authenticatedUser = state.server.authenticateUser(request, response); + var authenticatedUser = await state.server.authenticateUser(request, response); if(authenticatedUser) { // User is already logged in, redirect to home page response.writeHead(302, { "Location": "/" }); 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..b69d3f13a6c 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-events.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-events.js @@ -21,8 +21,8 @@ const SSE_HEARTBEAT_INTERVAL_MS = 10 * 1000; exports.method = "GET"; exports.path = /^\/recipes\/([^\/]+)\/events$/; - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler} */ +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; @@ -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 $tw.mws.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 $tw.mws.store.getRecipeTiddler(tiddlerInfo.title,recipe_name); if(tiddler) { data = $tw.utils.extend({},data,{tiddler: tiddler.tiddler}) } @@ -71,7 +71,7 @@ exports.handler = function(request,response,state) { } } // Send current and future changes - sendUpdates(); + await sendUpdates(); $tw.mws.store.addEventListener("change",sendUpdates); // Clean up when the connection closes response.on("close",function () { 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..8453146e743 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} */ +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 $tw.mws.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 $tw.mws.store.getBagTiddlerStream(title,tiddlerInfo.bag_name); if(result) { if(!response.headersSent){ response.writeHead(200, "OK",{ 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..27f9e22cc8d 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,14 +15,14 @@ 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} */ +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,{ + var recipeTiddlers = await $tw.mws.store.getRecipeTiddlers(recipe_name,{ include_deleted: state.queryParameters.include_deleted === "true", last_known_tiddler_id: state.queryParameters.last_known_tiddler_id }); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-system.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-system.js index a5346c1a658..17969074819 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-system.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-system.js @@ -23,8 +23,9 @@ exports.method = "GET"; exports.path = /^\/\.system\/(.+)$/; const SYSTEM_FILE_TITLE_PREFIX = "$:/plugins/tiddlywiki/multiwikiserver/system-files/"; - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler} */ +// eslint-disable-next-line require-await +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..2e0f7d59dfa 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-users.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-users.js @@ -15,10 +15,10 @@ 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("*")) { +/** @type {ServerRouteHandler} */ +exports.handler = async function(request,response,state) { + var userList = await 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"); } @@ -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; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-wiki.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-wiki.js index 1765f5e208f..86fdf9eed24 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-wiki.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-wiki.js @@ -19,11 +19,11 @@ exports.path = /^\/wiki\/([^\/]+)$/; exports.useACL = true; exports.entityName = "recipe" - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler} */ +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 $tw.mws.store.getRecipeTiddlers(recipe_name); // Check request is valid if(recipe_name && recipeTiddlers) { // Start the response @@ -58,15 +58,21 @@ exports.handler = function(request,response,state) { } response.write(template.substring(0,markerPos + marker.length)); const bagInfo = {}, - revisionInfo = {}; - $tw.utils.each(recipeTiddlers,function(recipeTiddlerInfo) { - var result = $tw.mws.store.getRecipeTiddler(recipeTiddlerInfo.title,recipe_name); + revisionInfo = {}, + recipeTiddlerInfos = []; + + $tw.utils.each(recipeTiddlers, function(recipeTiddlerInfo) { + recipeTiddlerInfos.push(recipeTiddlerInfo); + }); + for(const recipeTiddlerInfo of recipeTiddlerInfos){ + var result = await $tw.mws.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), @@ -83,7 +89,7 @@ exports.handler = function(request,response,state) { }); writeTiddler({ title: "$:/state/multiwikiclient/recipe/last_tiddler_id", - text: ($tw.mws.store.getRecipeLastTiddlerId(recipe_name) || 0).toString() + text: (await $tw.mws.store.getRecipeLastTiddlerId(recipe_name) || 0).toString() }); response.write(template.substring(markerPos + marker.length)) // Finish response diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-roles.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-roles.js index e6400dbb92c..45ca5cb7b5f 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("*")) { +/** @type {ServerRouteHandler} */ +exports.handler = async 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"); } - var roles = state.server.sqlTiddlerDatabase.listRoles(); + var roles = await state.server.sqlTiddlerDatabase.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; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-user.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-user.js index cada04b832f..17e9b512ee5 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-user.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-user.js @@ -15,10 +15,10 @@ GET /admin/users/:user_id exports.method = "GET"; exports.path = /^\/admin\/users\/([^\/]+)\/?$/; - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler} */ +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.server.sqlTiddlerDatabase.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"); @@ -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,8 +63,8 @@ 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.server.sqlTiddlerDatabase.getUserRoles(user_id); + var allRoles = await state.server.sqlTiddlerDatabase.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) }); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-acl.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-acl.js index 63a9f414f1b..254e7ff0e13 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-acl.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-acl.js @@ -19,8 +19,8 @@ exports.path = /^\/admin\/post-acl\/?$/; exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; - -exports.handler = function (request, response, state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function (request, response, state) { var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase; var entity_type = state.data.entity_type; var recipe_name = state.data.recipe_name; @@ -30,7 +30,7 @@ exports.handler = function (request, response, state) { var isRecipe = entity_type === "recipe" try { - var entityAclRecords = sqlTiddlerDatabase.getACLByName(entity_type, isRecipe ? recipe_name : bag_name, true); + var entityAclRecords = await sqlTiddlerDatabase.getACLByName(entity_type, isRecipe ? recipe_name : bag_name, true); var aclExists = entityAclRecords.some((record) => ( record.role_id == role_id && record.permission_id == permission_id @@ -50,7 +50,7 @@ exports.handler = function (request, response, state) { return } - sqlTiddlerDatabase.createACL( + await sqlTiddlerDatabase.createACL( isRecipe ? recipe_name : bag_name, entity_type, role_id, 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..918bd320814 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,9 @@ exports.path = /^\/admin\/post-anon-config\/?$/; exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; - -exports.handler = function(request, response, state) { +/** @type {ServerRouteHandler} */ +// eslint-disable-next-line require-await +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" }); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-anon.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-anon.js index 911b6ef971c..003db6851c1 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-anon.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-anon.js @@ -19,8 +19,9 @@ exports.path = /^\/admin\/anon\/?$/; exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; - -exports.handler = function(request, response, state) { +/** @type {ServerRouteHandler} */ +// eslint-disable-next-line require-await +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 +31,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..e10065a6315 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,9 @@ exports.csrfDisable = true; exports.useACL = true; exports.entityName = "bag" - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler} */ +// eslint-disable-next-line require-await +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; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag.js index bd59b06427f..aacd9a835e1 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) { +/** @type {ServerRouteHandler} */ +exports.handler = async function(request,response,state) { if(state.data.bag_name) { - const result = $tw.mws.store.createBag(state.data.bag_name,state.data.description); + const result = await $tw.mws.store.createBag(state.data.bag_name,state.data.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..00789fab1b5 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,12 +25,12 @@ exports.path = /^\/login$/; exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; - -exports.handler = function(request,response,state) { +/** @type {ServerRouteHandler} */ +exports.handler = async 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); + var user = await state.server.sqlTiddlerDatabase.getUserByUsername(username); var isPasswordValid = auth.verifyPassword(password, user ? user.password : null) if(user && isPasswordValid) { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-logout.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-logout.js index 36d901b4467..2372f898154 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} */ +exports.handler = async function(request,response,state) { + if(state.authenticatedUser) { + await state.server.sqlTiddlerDatabase.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..a19ec007d23 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} */ +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); + const result = await $tw.mws.store.createRecipe(state.data.recipe_name,$tw.utils.parseStringArray(state.data.bag_names),state.data.description); if(!result) { if(state.authenticatedUser) { - sqlTiddlerDatabase.assignRecipeToUser(state.data.recipe_name,state.authenticatedUser.user_id); + await sqlTiddlerDatabase.assignRecipeToUser(state.data.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..f65ffe0dfdf 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-role.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-role.js @@ -19,8 +19,8 @@ exports.path = /^\/admin\/post-role\/?$/; exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; - -exports.handler = function (request, response, state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function (request, response, state) { var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase; var role_name = state.data.role_name; var role_description = state.data.role_description; @@ -47,7 +47,7 @@ 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({ title: "$:/temp/mws/post-role/error", @@ -58,7 +58,7 @@ 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({ title: "$:/temp/mws/post-role/success", diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-user.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-user.js index ff3acbfc916..c6902d4c37b 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-user.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-user.js @@ -29,9 +29,9 @@ function deleteTempTiddlers() { $tw.mws.store.adminWiki.deleteTiddler("$:/temp/mws/post-user/success"); }, 1000); } +/** @type {ServerRouteHandler} */ +exports.handler = async function(request, response, state) { -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; @@ -84,8 +84,8 @@ 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({ @@ -109,16 +109,16 @@ 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; @@ -160,12 +160,12 @@ exports.handler = function(request, response, state) { 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(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-bag.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-bag.js index d174ee8cea2..ed97705346b 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} */ +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 $tw.mws.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..4eab83e308c 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} */ +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 $tw.mws.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..86fd28a9627 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} */ +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 $tw.mws.store.createRecipe(recipe_name, data.bag_names, data.description); if(!result) { state.sendResponse(204, { "Content-Type": "text/plain" diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-role.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-role.js index 081ba9b7374..401211051f0 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) { +/** @type {ServerRouteHandler} */ +exports.handler = async function(request, response, state) { var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase; var role_id = state.params[0]; var role_name = state.data.role_name; var role_description = state.data.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..9049dc517f1 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-user-profile.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-user-profile.js @@ -19,8 +19,8 @@ exports.path = /^\/update-user-profile\/?$/; exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; - -exports.handler = function (request,response,state) { +/** @type {ServerRouteHandler} */ +exports.handler = async function (request,response,state) { if(!state.authenticatedUser) { $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/login/error", @@ -50,11 +50,11 @@ exports.handler = function (request,response,state) { } if(!state.authenticatedUser.isAdmin) { - var userRole = state.server.sqlTiddlerDatabase.getUserRoles(userId); + var userRole = await state.server.sqlTiddlerDatabase.getUserRoles(userId); roleId = userRole.role_id; } - var result = state.server.sqlTiddlerDatabase.updateUser(userId, username, email, roleId); + var result = await state.server.sqlTiddlerDatabase.updateUser(userId, username, email, roleId); if(result.success) { $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js index 7210ed82c68..e52ed5b4ca2 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,8 +34,16 @@ 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} entityType + * @param {string} permissionName + * @returns + */ +exports.middleware = async function (request, response, state, entityType, permissionName) { var extensionRegex = /\.[A-Za-z0-9]{1,4}$/; var server = state.server, @@ -46,11 +54,11 @@ exports.middleware = function (request, response, state, entityType, permissionN 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..46b535928bb 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 = { @@ -85,13 +94,17 @@ exports.processIncomingStream = function(options) { tiddlerFields[part.name.slice(tiddlerFieldPrefix.length)] = part.value.trim(); } } + // eslint-disable-next-line custom-rules/always-await options.store.saveBagTiddlerWithAttachment(tiddlerFields,options.bag_name,{ 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/startup.js b/plugins/tiddlywiki/multiwikiserver/modules/startup.js index 42686e889be..39220b268c6 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/startup.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/startup.js @@ -16,43 +16,29 @@ 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; -} - -function ServerManager(store) { - this.servers = []; -} - -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 { AttachmentStore } = require("$:/plugins/tiddlywiki/multiwikiserver/store/attachments.js") + const attachmentStore = new AttachmentStore({ + storePath: path.resolve($tw.boot.wikiPath, "store/") + }); + + const { SqlTiddlerStore } = require("$:/plugins/tiddlywiki/multiwikiserver/store/sql-tiddler-store.js"); + const store = new SqlTiddlerStore({ + databasePath: path.resolve($tw.boot.wikiPath, "store/database.sqlite"), + engine: $tw.wiki.getTiddlerText("$:/config/MultiWikiServer/Engine", "better"), // better || wasm + attachmentStore: attachmentStore + }); + await store.init(); + + const { ServerManager } = require("$:/plugins/tiddlywiki/multiwikiserver/mws-server.js"); + const serverManager = new ServerManager(); + + $tw.mws = { store, serverManager }; + } })(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/attachments.js b/plugins/tiddlywiki/multiwikiserver/modules/store/attachments.js index 352f96a8386..ddd13b98416 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/attachments.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/attachments.js @@ -34,7 +34,8 @@ Class to handle an attachment store. Options include: storePath - path to the store */ -function AttachmentStore(options) { +class AttachmentStore { +constructor(options) { options = options || {}; this.storePath = options.storePath; } @@ -42,10 +43,10 @@ function AttachmentStore(options) { /* Check if an attachment name is valid */ -AttachmentStore.prototype.isValidAttachmentName = function(attachment_name) { - const re = new RegExp('^[a-f0-9]{64}$'); +isValidAttachmentName(attachment_name) { + const re = new RegExp("^[a-f0-9]{64}$"); return re.test(attachment_name); -}; +} /* Saves an attachment to a file. Options include: @@ -55,9 +56,8 @@ 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"); +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 @@ -78,14 +78,13 @@ AttachmentStore.prototype.saveAttachment = function(options) { 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"); +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 @@ -105,16 +104,15 @@ AttachmentStore.prototype.adoptAttachment = function(incomingFilepath,type,hash, 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"); +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 @@ -138,15 +136,14 @@ AttachmentStore.prototype.getAttachmentStream = function(attachment_name) { } // 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"); +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 @@ -163,11 +160,10 @@ AttachmentStore.prototype.getAttachmentFileSize = function(contentHash) { } // 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"); +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)) { @@ -175,7 +171,9 @@ AttachmentStore.prototype.getAttachmentMetadata = function(attachmentBlob) { return metadata; } return null; -}; +} +} + exports.AttachmentStore = AttachmentStore; })(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-engine-worker.js b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-engine-worker.js new file mode 100644 index 00000000000..8ae206b6633 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-engine-worker.js @@ -0,0 +1,99 @@ +/*\ +title: $:/plugins/tiddlywiki/multiwikiserver/store/sql-engine-worker.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. + +\*/ + +const {Worker, isMainThread, parentPort, workerData} = require('worker_threads'); + + +function SqlEngineWorker(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 + 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; + default: + throw new Error("Unknown database engine " + this.engine); + } + this.db = new Database(databasePath, {}); + parentPort?.on('message', (message) => { + if(!parentPort) return; //really? + const {verb, sql, params, messageId} = message; + switch(verb) { + case "init": { + parentPort.postMessage({messageId}); + return; + } + case "close": { + this.db.close(); + parentPort.postMessage({messageId}); + parentPort.removeAllListeners(); + return; + } + case "pragma": { + const result = this.engine === "node" + ? this.db.exec("PRAGMA " + sql) + : this.db.pragma(sql); + parentPort.postMessage({messageId, result}); + return; + } + case "finalize": { + if(this.statements[sql].finalize) + this.statements[sql].finalize(); + delete this.statements[sql]; + parentPort.postMessage({messageId}); + return; + } + } + + try { + this.statements[sql] = this.statements[sql] || this.db.prepare(sql); + switch(verb) { + case "prepare": { + parentPort.postMessage({messageId}); + break; + } + case "run": { + const result = this.statements[sql].run(params); + parentPort.postMessage({messageId, result}); + break; + } + case "get": { + const result = this.statements[sql].get(params); + parentPort.postMessage({messageId, result}); + break; + } + case "all": { + const result = this.statements[sql].all(params); + parentPort.postMessage({messageId, result}); + break; + } + } + } catch(error) { + parentPort.postMessage({messageId, error}); + } + }); + +} +const worker = new SqlEngineWorker(workerData); \ 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 index 00d15edf370..a8a14812baa 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-engine.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-engine.js @@ -10,61 +10,144 @@ This class is intended to encapsulate all engine-specific logic. \*/ (function() { +const {Worker} = require('worker_threads'); +class SqlWorkerProxy { + /** + * Create a database engine. + * + * @param {Object} options + * @param {string} [options.databasePath] path to the database file (can be ":memory:" or missing to get a temporary database) + * @param {"node" | "wasm" | "better"} [options.engine] which engine to use, default is "node" + */ + + constructor(options) { + options = options || {}; + const workerTitle = "$:/plugins/tiddlywiki/multiwikiserver/store/sql-engine-worker.js"; + this.worker = new Worker($tw.modules.titles[workerTitle].definition, { + eval: true, + workerData: { + databasePath: options.databasePath, + engine: options.engine + } + }); + this.worker.on('message', (message) => { + const {messageId, result, error} = message; + const req = this.messageMap.get(messageId); + if(!req) { + return $tw.utils.warning(`No request found for message ${messageId}`); + } + const {resolve, reject} = req; + this.messageMap.delete(messageId); + if(error) { + reject(error); + } else { + resolve(result); + } + }); -/* -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 -*/ + messageId = 0; + + /** @type {Map void, reject: (err) => void}>} */ + messageMap = new Map(); + + /** + * + * @param {"run" | "get" | "all" | "prepare" | "pragma" | "init" | "close" | "finalize"} verb + * @param {string} sql + * @param {any} [params] + * @returns + */ + async sendMessage(verb, sql, params) { + const messageId = this.messageId++; + return await new Promise((resolve, reject) => { + this.messageMap.set(messageId, {resolve, reject}); + this.worker.postMessage({sql, params, verb, messageId}); + }); + } + + async init() { + return await this.sendMessage("init", ""); + } + + async close() { + return await this.sendMessage("close", ""); + } + + async pragma(pragma) { + return await this.sendMessage("pragma", pragma); + } + + async prepare(sql) { + await this.sendMessage("prepare", sql); + return { + run: async (params) => await this.sendMessage("run", sql, params), + get: async (params) => await this.sendMessage("get", sql, params), + all: async (params) => await this.sendMessage("all", sql, params), + finalize: async () => await this.sendMessage("finalize", sql) + }; + } + +} + +/** + * Create a database engine. + * + * @param {Object} options + * @param {string} [options.databasePath] path to the database file (can be ":memory:" or missing to get a temporary database) + * @param {"node" | "wasm" | "better"} [options.engine] which engine to use, default is "node" + */ 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 + /** @type {"node" | "wasm" | "better"} Choose engine */ + this.engine = options.engine || "node"; 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; + // Create the database file directories if needed + if(databasePath !== ":memory:") { + $tw.utils.createFileDirectories(databasePath); } - this.db = new Database(databasePath,{ - verbose: undefined && console.log + this.db = new SqlWorkerProxy({ + databasePath, + engine: this.engine + }); + const _syncError = new Error("init was not immediately called on SqlEngine") + /** @type {any} */ + this._syncCheck = setTimeout(() => { + $tw.utils.warning(_syncError); }); + + this.transactionQueue = []; + +} + +SqlEngine.prototype.init = async function() { + clearTimeout(this._syncCheck); + this._syncCheck = undefined; // 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"); + await this.db.pragma("journal_mode = WAL"); } } -SqlEngine.prototype.close = function() { +SqlEngine.prototype.close = async function() { for(const sql in this.statements) { if(this.statements[sql].finalize) { - this.statements[sql].finalize(); + await this.statements[sql].finalize(); } } this.statements = Object.create(null); - this.db.close(); + await this.db.close(); this.db = undefined; }; -SqlEngine.prototype.normaliseParams = function(params) { +// eslint-disable-next-line require-await -- we need to return a promise in case a replacement adapter needs it +SqlEngine.prototype.normaliseParams = async function(params) { params = params || {}; const result = Object.create(null); for(const paramName in params) { @@ -77,57 +160,69 @@ SqlEngine.prototype.normaliseParams = function(params) { return result; }; -SqlEngine.prototype.prepareStatement = function(sql) { +/** + * + * @param {string} sql + */ +SqlEngine.prototype.prepareStatement = async function(sql) { if(!(sql in this.statements)) { - this.statements[sql] = this.db.prepare(sql); + this.statements[sql] = await this.db.prepare(sql); } - return this.statements[sql]; + return /** @type {ReturnType} */(this.statements[sql]); }; -SqlEngine.prototype.runStatement = function(sql,params) { - params = this.normaliseParams(params); - const statement = this.prepareStatement(sql); - return statement.run(params); +SqlEngine.prototype.runStatement = async function(sql,params) { + params = await this.normaliseParams(params); + const statement = await this.prepareStatement(sql); + return await statement.run(params); }; -SqlEngine.prototype.runStatementGet = function(sql,params) { - params = this.normaliseParams(params); - const statement = this.prepareStatement(sql); - return statement.get(params); +SqlEngine.prototype.runStatementGet = async function(sql,params) { + params = await this.normaliseParams(params); + const statement = await this.prepareStatement(sql); + return /** @type {Record} */(await statement.get(params)); }; -SqlEngine.prototype.runStatementGetAll = function(sql,params) { - params = this.normaliseParams(params); - const statement = this.prepareStatement(sql); - return statement.all(params); +SqlEngine.prototype.runStatementGetAll = async function(sql,params) { + params = await this.normaliseParams(params); + const statement = await this.prepareStatement(sql); + return /** @type {Record[]} */(await statement.all(params)); }; -SqlEngine.prototype.runStatements = function(sqlArray) { - for(const sql of sqlArray) { - this.runStatement(sql); +SqlEngine.prototype.runStatements = async function(sqlArray) { + /** @type {Awaited>[]} */ + const results = new Array(sqlArray.length); + for(let t = 0; t < sqlArray.length; t++) { + results[t] = await this.runStatement(sqlArray[t]); } + return results; }; -/* +/** 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 + +@template T +@param {() => Promise} fn - function to execute in the transaction +@returns {Promise} - the result + */ -SqlEngine.prototype.transaction = function(fn) { +SqlEngine.prototype.transaction = async function(fn) { const alreadyInTransaction = this.transactionDepth > 0; this.transactionDepth++; - try { + try { if(alreadyInTransaction) { - return fn(); + return await fn(); } else { - this.runStatement(`BEGIN TRANSACTION`); + await this.runStatement(`BEGIN TRANSACTION`); try { - var result = fn(); - this.runStatement(`COMMIT TRANSACTION`); + var result = await fn(); + await this.runStatement(`COMMIT TRANSACTION`); } catch(e) { - this.runStatement(`ROLLBACK TRANSACTION`); + await this.runStatement(`ROLLBACK TRANSACTION`); throw(e); } return result; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js index 14f8641f4ce..88693329db7 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js @@ -21,6 +21,7 @@ engine - wasm | better function SqlTiddlerDatabase(options) { options = options || {}; const SqlEngine = require("$:/plugins/tiddlywiki/multiwikiserver/store/sql-engine.js").SqlEngine; + /** @type {SqlEngine} */ this.engine = new SqlEngine({ databasePath: options.databasePath, engine: options.engine @@ -37,17 +38,21 @@ function SqlTiddlerDatabase(options) { }; } -SqlTiddlerDatabase.prototype.close = function() { - this.engine.close(); +SqlTiddlerDatabase.prototype.init = async function() { + await this.engine.init(); +}; + +SqlTiddlerDatabase.prototype.close = async function() { + await this.engine.close(); }; -SqlTiddlerDatabase.prototype.transaction = function(fn) { - return this.engine.transaction(fn); +SqlTiddlerDatabase.prototype.transaction = async function(fn) { + return await this.engine.transaction(fn); }; -SqlTiddlerDatabase.prototype.createTables = function() { - this.engine.runStatements([` +SqlTiddlerDatabase.prototype.createTables = async function() { + await this.engine.runStatements([` -- Users table CREATE TABLE IF NOT EXISTS users ( user_id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -194,8 +199,8 @@ SqlTiddlerDatabase.prototype.createTables = function() { `]); }; -SqlTiddlerDatabase.prototype.listBags = function() { - const rows = this.engine.runStatementGetAll(` +SqlTiddlerDatabase.prototype.listBags = async function() { + const rows = await this.engine.runStatementGetAll(` SELECT bag_name, bag_id, accesscontrol, description FROM bags ORDER BY bag_name @@ -207,16 +212,16 @@ SqlTiddlerDatabase.prototype.listBags = function() { Create or update a bag Returns the bag_id of the bag */ -SqlTiddlerDatabase.prototype.createBag = function(bag_name,description,accesscontrol) { +SqlTiddlerDatabase.prototype.createBag = async function(bag_name,description,accesscontrol) { accesscontrol = accesscontrol || ""; // Run the queries - var bag = this.engine.runStatement(` + var bag = await this.engine.runStatement(` INSERT OR IGNORE INTO bags (bag_name, accesscontrol, description) VALUES ($bag_name, '', '') `,{ $bag_name: bag_name }); - const updateBags = this.engine.runStatement(` + const updateBags = await this.engine.runStatement(` UPDATE bags SET accesscontrol = $accesscontrol, description = $description @@ -232,8 +237,8 @@ SqlTiddlerDatabase.prototype.createBag = function(bag_name,description,accesscon /* Returns array of {recipe_name:,recipe_id:,description:,bag_names: []} */ -SqlTiddlerDatabase.prototype.listRecipes = function() { - const rows = this.engine.runStatementGetAll(` +SqlTiddlerDatabase.prototype.listRecipes = async function() { + 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 @@ -251,6 +256,7 @@ SqlTiddlerDatabase.prototype.listRecipes = function() { recipe_id: row.recipe_id, description: row.description, owner_id: row.owner_id, + /** @type {string[]} */ bag_names: [] }); } @@ -263,15 +269,15 @@ SqlTiddlerDatabase.prototype.listRecipes = function() { Create or update a recipe Returns the recipe_id of the recipe */ -SqlTiddlerDatabase.prototype.createRecipe = function(recipe_name,bag_names,description) { +SqlTiddlerDatabase.prototype.createRecipe = async function(recipe_name,bag_names,description) { // Run the queries - this.engine.runStatement(` + 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 = this.engine.runStatement(` + 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) @@ -279,7 +285,7 @@ SqlTiddlerDatabase.prototype.createRecipe = function(recipe_name,bag_names,descr $recipe_name: recipe_name, $description: description }); - this.engine.runStatement(` + 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 @@ -297,8 +303,8 @@ SqlTiddlerDatabase.prototype.createRecipe = function(recipe_name,bag_names,descr /* Assign a recipe to a user */ -SqlTiddlerDatabase.prototype.assignRecipeToUser = function(recipe_name,user_id) { - this.engine.runStatement(` +SqlTiddlerDatabase.prototype.assignRecipeToUser = async function(recipe_name,user_id) { + await this.engine.runStatement(` UPDATE recipes SET owner_id = $user_id WHERE recipe_name = $recipe_name `,{ $recipe_name: recipe_name, @@ -309,10 +315,10 @@ SqlTiddlerDatabase.prototype.assignRecipeToUser = function(recipe_name,user_id) /* Returns {tiddler_id:} */ -SqlTiddlerDatabase.prototype.saveBagTiddler = function(tiddlerFields,bag_name,attachment_blob) { +SqlTiddlerDatabase.prototype.saveBagTiddler = async function(tiddlerFields,bag_name,attachment_blob) { attachment_blob = attachment_blob || null; // Update the tiddlers table - var info = this.engine.runStatement(` + 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), @@ -326,7 +332,7 @@ SqlTiddlerDatabase.prototype.saveBagTiddler = function(tiddlerFields,bag_name,at $bag_name: bag_name }); // Update the fields table - this.engine.runStatement(` + await this.engine.runStatement(` INSERT OR REPLACE INTO fields (tiddler_id, field_name, field_value) SELECT t.tiddler_id, @@ -355,9 +361,9 @@ SqlTiddlerDatabase.prototype.saveBagTiddler = function(tiddlerFields,bag_name,at /* Returns {tiddler_id:,bag_name:} or null if the recipe is empty */ -SqlTiddlerDatabase.prototype.saveRecipeTiddler = function(tiddlerFields,recipe_name,attachment_blob) { +SqlTiddlerDatabase.prototype.saveRecipeTiddler = async function(tiddlerFields,recipe_name,attachment_blob) { // Find the topmost bag in the recipe - var row = this.engine.runStatementGet(` + var row = await this.engine.runStatementGet(` SELECT b.bag_name FROM bags AS b JOIN ( @@ -379,7 +385,7 @@ SqlTiddlerDatabase.prototype.saveRecipeTiddler = function(tiddlerFields,recipe_n return null; } // Save the tiddler to the topmost bag - var info = this.saveBagTiddler(tiddlerFields,row.bag_name,attachment_blob); + var info = await this.saveBagTiddler(tiddlerFields,row.bag_name,attachment_blob); return { tiddler_id: info.tiddler_id, bag_name: row.bag_name @@ -389,9 +395,9 @@ SqlTiddlerDatabase.prototype.saveRecipeTiddler = function(tiddlerFields,recipe_n /* Returns {tiddler_id:} of the delete marker */ -SqlTiddlerDatabase.prototype.deleteTiddler = function(title,bag_name) { +SqlTiddlerDatabase.prototype.deleteTiddler = async function(title,bag_name) { // Delete the fields of this tiddler - this.engine.runStatement(` + await this.engine.runStatement(` DELETE FROM fields WHERE tiddler_id IN ( SELECT t.tiddler_id @@ -404,7 +410,7 @@ SqlTiddlerDatabase.prototype.deleteTiddler = function(title,bag_name) { $bag_name: bag_name }); // Mark the tiddler itself as deleted - const rowDeleteMarker = this.engine.runStatement(` + 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), @@ -422,8 +428,8 @@ SqlTiddlerDatabase.prototype.deleteTiddler = function(title,bag_name) { /* returns {tiddler_id:,tiddler:,attachment_blob:} */ -SqlTiddlerDatabase.prototype.getBagTiddler = function(title,bag_name) { - const rowTiddler = this.engine.runStatementGet(` +SqlTiddlerDatabase.prototype.getBagTiddler = async function(title,bag_name) { + 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 @@ -435,7 +441,7 @@ SqlTiddlerDatabase.prototype.getBagTiddler = function(title,bag_name) { if(!rowTiddler) { return null; } - const rows = this.engine.runStatementGetAll(` + const rows = await this.engine.runStatementGetAll(` SELECT field_name, field_value, tiddler_id FROM fields WHERE tiddler_id = $tiddler_id @@ -459,8 +465,8 @@ SqlTiddlerDatabase.prototype.getBagTiddler = function(title,bag_name) { /* Returns {bag_name:, tiddler: {fields}, tiddler_id:, attachment_blob:} */ -SqlTiddlerDatabase.prototype.getRecipeTiddler = function(title,recipe_name) { - const rowTiddlerId = this.engine.runStatementGet(` +SqlTiddlerDatabase.prototype.getRecipeTiddler = async function(title,recipe_name) { + 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 @@ -479,7 +485,7 @@ SqlTiddlerDatabase.prototype.getRecipeTiddler = function(title,recipe_name) { return null; } // Get the fields - const rows = this.engine.runStatementGetAll(` + const rows = await this.engine.runStatementGetAll(` SELECT field_name, field_value FROM fields WHERE tiddler_id = $tiddler_id @@ -500,10 +506,10 @@ SqlTiddlerDatabase.prototype.getRecipeTiddler = function(title,recipe_name) { /* Checks if a user has permission to access a recipe */ -SqlTiddlerDatabase.prototype.hasRecipePermission = function(userId, recipeName, permissionName) { +SqlTiddlerDatabase.prototype.hasRecipePermission = async function(userId, recipeName, permissionName) { try { // check if the user is the owner of the entity - const recipe = this.engine.runStatementGet(` + const recipe = await this.engine.runStatementGet(` SELECT owner_id FROM recipes WHERE recipe_name = $recipe_name @@ -514,7 +520,7 @@ SqlTiddlerDatabase.prototype.hasRecipePermission = function(userId, recipeName, if(!!recipe?.owner_id && recipe?.owner_id === userId) { return true; } else { - var permission = this.checkACLPermission(userId, "recipe", recipeName, permissionName, recipe?.owner_id) + var permission = await this.checkACLPermission(userId, "recipe", recipeName, permissionName, recipe?.owner_id) return permission; } @@ -527,11 +533,23 @@ SqlTiddlerDatabase.prototype.hasRecipePermission = function(userId, recipeName, /* 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.hasBagPermission = async function(userId, bagName, permissionName) { + return await this.checkACLPermission(userId, "bag", bagName, permissionName) }; - -SqlTiddlerDatabase.prototype.getACLByName = function(entityType, entityName, fetchAll) { +/** + * @overload + * @param {string} entityType + * @param {string} entityName + * @param {false} [fetchAll] + * @returns {Promise>} + * + * @overload + * @param {string} entityType + * @param {string} entityName + * @param {true} fetchAll + * @returns {Promise[]>} + */ +SqlTiddlerDatabase.prototype.getACLByName = async function(entityType, entityName, fetchAll) { const entityInfo = this.entityTypeToTableMap[entityType]; if (!entityInfo) { throw new Error("Invalid entity type: " + entityType); @@ -550,7 +568,7 @@ SqlTiddlerDatabase.prototype.getACLByName = function(entityType, entityName, fet checkACLExistsQuery += ' LIMIT 1' } - const aclRecord = this.engine[fetchAll ? 'runStatementGetAll' : 'runStatementGet'](checkACLExistsQuery, { + const aclRecord = await this.engine[fetchAll ? 'runStatementGetAll' : 'runStatementGet'](checkACLExistsQuery, { $entity_type: entityType, $entity_name: entityName }); @@ -558,14 +576,14 @@ SqlTiddlerDatabase.prototype.getACLByName = function(entityType, entityName, fet return aclRecord; } -SqlTiddlerDatabase.prototype.checkACLPermission = function(userId, entityType, entityName, permissionName, ownerId) { +SqlTiddlerDatabase.prototype.checkACLPermission = async 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 aclRecords = await this.getACLByName(entityType, entityName, true); const aclRecord = aclRecords.find(record => record.permission_name === permissionName); // If no ACL record exists, return true for hasPermission @@ -587,7 +605,7 @@ SqlTiddlerDatabase.prototype.checkACLPermission = function(userId, entityType, e LIMIT 1 `; - const result = this.engine.runStatementGet(checkPermissionQuery, { + const result = await this.engine.runStatementGet(checkPermissionQuery, { $user_id: userId, $entity_type: entityType, $entity_name: entityName, @@ -607,14 +625,14 @@ SqlTiddlerDatabase.prototype.checkACLPermission = function(userId, entityType, e /** * Returns the ACL records for an entity (bag or recipe) */ -SqlTiddlerDatabase.prototype.getEntityAclRecords = function(entityName) { +SqlTiddlerDatabase.prototype.getEntityAclRecords = async function(entityName) { const checkACLExistsQuery = ` SELECT * FROM acl WHERE entity_name = $entity_name `; - const aclRecords = this.engine.runStatementGetAll(checkACLExistsQuery, { + const aclRecords = await this.engine.runStatementGetAll(checkACLExistsQuery, { $entity_name: entityName }); @@ -624,10 +642,10 @@ SqlTiddlerDatabase.prototype.getEntityAclRecords = function(entityName) { /* Get the entity by name */ -SqlTiddlerDatabase.prototype.getEntityByName = function(entityType, entityName) { +SqlTiddlerDatabase.prototype.getEntityByName = async function(entityType, entityName) { const entityInfo = this.entityTypeToTableMap[entityType]; if (entityInfo) { - return this.engine.runStatementGet(`SELECT * FROM ${entityInfo.table} WHERE ${entityInfo.column} = $entity_name`, { + return await this.engine.runStatementGet(`SELECT * FROM ${entityInfo.table} WHERE ${entityInfo.column} = $entity_name`, { $entity_name: entityName }); } @@ -637,8 +655,8 @@ SqlTiddlerDatabase.prototype.getEntityByName = function(entityType, entityName) /* 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(` +SqlTiddlerDatabase.prototype.getBagTiddlers = async function(bag_name) { + const rows = await this.engine.runStatementGetAll(` SELECT DISTINCT title, tiddler_id FROM tiddlers WHERE bag_id IN ( @@ -657,8 +675,8 @@ SqlTiddlerDatabase.prototype.getBagTiddlers = function(bag_name) { /* 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(` +SqlTiddlerDatabase.prototype.getBagLastTiddlerId = async function(bag_name) { + const row = await this.engine.runStatementGet(` SELECT tiddler_id FROM tiddlers WHERE bag_id IN ( @@ -690,10 +708,10 @@ include_deleted: boolean, defaults to false Returns null for recipes that do not exist */ -SqlTiddlerDatabase.prototype.getRecipeTiddlers = function(recipe_name,options) { +SqlTiddlerDatabase.prototype.getRecipeTiddlers = async function(recipe_name,options) { options = options || {}; // Get the recipe ID - const rowsCheckRecipe = this.engine.runStatementGet(` + const rowsCheckRecipe = await this.engine.runStatementGet(` SELECT recipe_id FROM recipes WHERE recipes.recipe_name = $recipe_name `,{ $recipe_name: recipe_name @@ -712,7 +730,7 @@ SqlTiddlerDatabase.prototype.getRecipeTiddlers = function(recipe_name,options) { if(options.last_known_tiddler_id) { params.$last_known_tiddler_id = options.last_known_tiddler_id; } - const rows = this.engine.runStatementGetAll(` + 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 @@ -733,8 +751,8 @@ SqlTiddlerDatabase.prototype.getRecipeTiddlers = function(recipe_name,options) { /* 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(` +SqlTiddlerDatabase.prototype.getRecipeLastTiddlerId = async function(recipe_name) { + 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 @@ -754,9 +772,9 @@ SqlTiddlerDatabase.prototype.getRecipeLastTiddlerId = function(recipe_name) { } }; -SqlTiddlerDatabase.prototype.deleteAllTiddlersInBag = function(bag_name) { +SqlTiddlerDatabase.prototype.deleteAllTiddlersInBag = async function(bag_name) { // Delete the fields - this.engine.runStatement(` + await this.engine.runStatement(` DELETE FROM fields WHERE tiddler_id IN ( SELECT tiddler_id @@ -768,7 +786,7 @@ SqlTiddlerDatabase.prototype.deleteAllTiddlersInBag = function(bag_name) { $bag_name: bag_name }); // Mark the tiddlers as deleted - this.engine.runStatement(` + await this.engine.runStatement(` UPDATE tiddlers SET is_deleted = TRUE WHERE bag_id = (SELECT bag_id FROM bags WHERE bag_name = $bag_name) @@ -781,8 +799,8 @@ SqlTiddlerDatabase.prototype.deleteAllTiddlersInBag = function(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(` +SqlTiddlerDatabase.prototype.getRecipeBags = async function(recipe_name) { + const rows = await this.engine.runStatementGetAll(` SELECT bags.bag_name FROM bags JOIN ( @@ -802,8 +820,8 @@ SqlTiddlerDatabase.prototype.getRecipeBags = function(recipe_name) { /* Get the attachment value of a bag, if any exist */ -SqlTiddlerDatabase.prototype.getBagTiddlerAttachmentBlob = function(title,bag_name) { - const row = this.engine.runStatementGet(` +SqlTiddlerDatabase.prototype.getBagTiddlerAttachmentBlob = async function(title,bag_name) { + 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 @@ -818,8 +836,8 @@ SqlTiddlerDatabase.prototype.getBagTiddlerAttachmentBlob = function(title,bag_na /* Get the attachment value of a recipe, if any exist */ -SqlTiddlerDatabase.prototype.getRecipeTiddlerAttachmentBlob = function(title,recipe_name) { - const row = this.engine.runStatementGet(` +SqlTiddlerDatabase.prototype.getRecipeTiddlerAttachmentBlob = async function(title,recipe_name) { + 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 @@ -836,8 +854,8 @@ SqlTiddlerDatabase.prototype.getRecipeTiddlerAttachmentBlob = function(title,rec }; // User CRUD operations -SqlTiddlerDatabase.prototype.createUser = function(username, email, password) { - const result = this.engine.runStatement(` +SqlTiddlerDatabase.prototype.createUser = async function(username, email, password) { + const result = await this.engine.runStatement(` INSERT INTO users (username, email, password) VALUES ($username, $email, $password) `, { @@ -848,32 +866,32 @@ SqlTiddlerDatabase.prototype.createUser = function(username, email, password) { return result.lastInsertRowid; }; -SqlTiddlerDatabase.prototype.getUser = function(userId) { - return this.engine.runStatementGet(` +SqlTiddlerDatabase.prototype.getUser = async function(userId) { + return await this.engine.runStatementGet(` SELECT * FROM users WHERE user_id = $userId `, { $userId: userId }); }; -SqlTiddlerDatabase.prototype.getUserByUsername = function(username) { - return this.engine.runStatementGet(` +SqlTiddlerDatabase.prototype.getUserByUsername = async function(username) { + return await this.engine.runStatementGet(` SELECT * FROM users WHERE username = $username `, { $username: username }); }; -SqlTiddlerDatabase.prototype.getUserByEmail = function(email) { - return this.engine.runStatementGet(` +SqlTiddlerDatabase.prototype.getUserByEmail = async function(email) { + return await this.engine.runStatementGet(` SELECT * FROM users WHERE email = $email `, { $email: email }); }; -SqlTiddlerDatabase.prototype.listUsersByRoleId = function(roleId) { - return this.engine.runStatementGetAll(` +SqlTiddlerDatabase.prototype.listUsersByRoleId = async function(roleId) { + return await this.engine.runStatementGetAll(` SELECT u.* FROM users u JOIN user_roles ur ON u.user_id = ur.user_id @@ -884,8 +902,8 @@ SqlTiddlerDatabase.prototype.listUsersByRoleId = function(roleId) { }); }; -SqlTiddlerDatabase.prototype.updateUser = function (userId, username, email, roleId) { - const existingUser = this.engine.runStatement(` +SqlTiddlerDatabase.prototype.updateUser = async function (userId, username, email, roleId) { + const existingUser = await this.engine.runStatementGet(` SELECT user_id FROM users WHERE email = $email AND user_id != $userId `, { @@ -893,7 +911,7 @@ SqlTiddlerDatabase.prototype.updateUser = function (userId, username, email, rol $userId: userId }); - if (existingUser.length > 0) { + if (existingUser) { return { success: false, message: "Email address already in use by another user." @@ -901,9 +919,9 @@ SqlTiddlerDatabase.prototype.updateUser = function (userId, username, email, rol } try { - this.engine.transaction(() => { + await this.engine.transaction(async () => { // Update user information - this.engine.runStatement(` + await this.engine.runStatement(` UPDATE users SET username = $username, email = $email WHERE user_id = $userId @@ -915,7 +933,7 @@ SqlTiddlerDatabase.prototype.updateUser = function (userId, username, email, rol if (roleId) { // Remove all existing roles for the user - this.engine.runStatement(` + await this.engine.runStatement(` DELETE FROM user_roles WHERE user_id = $userId `, { @@ -923,7 +941,7 @@ SqlTiddlerDatabase.prototype.updateUser = function (userId, username, email, rol }); // Add the new role - this.engine.runStatement(` + await this.engine.runStatement(` INSERT INTO user_roles (user_id, role_id) VALUES ($userId, $roleId) `, { @@ -945,9 +963,9 @@ SqlTiddlerDatabase.prototype.updateUser = function (userId, username, email, rol } }; -SqlTiddlerDatabase.prototype.updateUserPassword = function (userId, newHash) { +SqlTiddlerDatabase.prototype.updateUserPassword = async function (userId, newHash) { try { - this.engine.runStatement(` + await this.engine.runStatement(` UPDATE users SET password = $newHash WHERE user_id = $userId @@ -968,25 +986,25 @@ SqlTiddlerDatabase.prototype.updateUserPassword = function (userId, newHash) { } }; -SqlTiddlerDatabase.prototype.deleteUser = function(userId) { - this.engine.runStatement(` +SqlTiddlerDatabase.prototype.deleteUser = async function(userId) { + await this.engine.runStatement(` DELETE FROM users WHERE user_id = $userId `, { $userId: userId }); }; -SqlTiddlerDatabase.prototype.listUsers = function() { - return this.engine.runStatementGetAll(` +SqlTiddlerDatabase.prototype.listUsers = async function() { + return await this.engine.runStatementGetAll(` SELECT * FROM users ORDER BY username `); }; -SqlTiddlerDatabase.prototype.createOrUpdateUserSession = function(userId, sessionId) { +SqlTiddlerDatabase.prototype.createOrUpdateUserSession = async function(userId, sessionId) { const currentTimestamp = new Date().toISOString(); // First, try to update an existing session - const updateResult = this.engine.runStatement(` + const updateResult = await this.engine.runStatement(` UPDATE sessions SET session_id = $sessionId, last_accessed = $timestamp WHERE user_id = $userId @@ -998,7 +1016,7 @@ SqlTiddlerDatabase.prototype.createOrUpdateUserSession = function(userId, sessio // If no existing session was updated, create a new one if (updateResult.changes === 0) { - this.engine.runStatement(` + await this.engine.runStatement(` INSERT INTO sessions (user_id, session_id, created_at, last_accessed) VALUES ($userId, $sessionId, $timestamp, $timestamp) `, { @@ -1011,9 +1029,9 @@ SqlTiddlerDatabase.prototype.createOrUpdateUserSession = function(userId, sessio return sessionId; }; -SqlTiddlerDatabase.prototype.createUserSession = function(userId, sessionId) { +SqlTiddlerDatabase.prototype.createUserSession = async function(userId, sessionId) { const currentTimestamp = new Date().toISOString(); - this.engine.runStatement(` + await this.engine.runStatement(` INSERT INTO sessions (user_id, session_id, created_at, last_accessed) VALUES ($userId, $sessionId, $timestamp, $timestamp) `, { @@ -1025,9 +1043,24 @@ SqlTiddlerDatabase.prototype.createUserSession = function(userId, sessionId) { return sessionId; }; -SqlTiddlerDatabase.prototype.findUserBySessionId = function(sessionId) { + +/** + * @typedef {Object} User + * @property {number} user_id + * @property {string} username + * @property {string} email + * @property {string} [password] + * @property {string} created_at + * @property {string} last_login +*/ +/** + * + * @param {any} sessionId + * @returns {Promise} + */ +SqlTiddlerDatabase.prototype.findUserBySessionId = async function(sessionId) { // First, get the user_id from the sessions table - const sessionResult = this.engine.runStatementGet(` + const sessionResult = await this.engine.runStatementGet(` SELECT user_id, last_accessed FROM sessions WHERE session_id = $sessionId @@ -1043,13 +1076,13 @@ SqlTiddlerDatabase.prototype.findUserBySessionId = function(sessionId) { const expirationTime = 24 * 60 * 60 * 1000; // 24 hours in milliseconds if (new Date() - lastAccessed > expirationTime) { // Session has expired - this.deleteSession(sessionId); + await this.deleteSession(sessionId); return null; } // Update the last_accessed timestamp const currentTimestamp = new Date().toISOString(); - this.engine.runStatement(` + await this.engine.runStatement(` UPDATE sessions SET last_accessed = $timestamp WHERE session_id = $sessionId @@ -1057,8 +1090,8 @@ SqlTiddlerDatabase.prototype.findUserBySessionId = function(sessionId) { $sessionId: sessionId, $timestamp: currentTimestamp }); - - const userResult = this.engine.runStatementGet(` + /** @type {any} */ + const userResult = await this.engine.runStatementGet(` SELECT * FROM users WHERE user_id = $userId @@ -1073,8 +1106,8 @@ SqlTiddlerDatabase.prototype.findUserBySessionId = function(sessionId) { return userResult; }; -SqlTiddlerDatabase.prototype.deleteSession = function(sessionId) { - this.engine.runStatement(` +SqlTiddlerDatabase.prototype.deleteSession = async function(sessionId) { + await this.engine.runStatement(` DELETE FROM sessions WHERE session_id = $sessionId `, { @@ -1082,8 +1115,8 @@ SqlTiddlerDatabase.prototype.deleteSession = function(sessionId) { }); }; -SqlTiddlerDatabase.prototype.deleteUserSessions = function(userId) { - this.engine.runStatement(` +SqlTiddlerDatabase.prototype.deleteUserSessions = async function(userId) { + await this.engine.runStatement(` DELETE FROM sessions WHERE user_id = $userId `, { @@ -1092,16 +1125,16 @@ SqlTiddlerDatabase.prototype.deleteUserSessions = function(userId) { }; // Set the user as an admin -SqlTiddlerDatabase.prototype.setUserAdmin = function(userId) { - var admin = this.getRoleByName("ADMIN"); +SqlTiddlerDatabase.prototype.setUserAdmin = async function(userId) { + var admin = await this.getRoleByName("ADMIN"); if(admin) { - this.addRoleToUser(userId, admin.role_id); + await this.addRoleToUser(userId, admin.role_id); } }; // Group CRUD operations -SqlTiddlerDatabase.prototype.createGroup = function(groupName, description) { - const result = this.engine.runStatement(` +SqlTiddlerDatabase.prototype.createGroup = async function(groupName, description) { + const result = await this.engine.runStatement(` INSERT INTO groups (group_name, description) VALUES ($groupName, $description) `, { @@ -1111,16 +1144,16 @@ SqlTiddlerDatabase.prototype.createGroup = function(groupName, description) { return result.lastInsertRowid; }; -SqlTiddlerDatabase.prototype.getGroup = function(groupId) { - return this.engine.runStatementGet(` +SqlTiddlerDatabase.prototype.getGroup = async function(groupId) { + return await this.engine.runStatementGet(` SELECT * FROM groups WHERE group_id = $groupId `, { $groupId: groupId }); }; -SqlTiddlerDatabase.prototype.updateGroup = function(groupId, groupName, description) { - this.engine.runStatement(` +SqlTiddlerDatabase.prototype.updateGroup = async function(groupId, groupName, description) { + await this.engine.runStatement(` UPDATE groups SET group_name = $groupName, description = $description WHERE group_id = $groupId @@ -1131,23 +1164,23 @@ SqlTiddlerDatabase.prototype.updateGroup = function(groupId, groupName, descript }); }; -SqlTiddlerDatabase.prototype.deleteGroup = function(groupId) { - this.engine.runStatement(` +SqlTiddlerDatabase.prototype.deleteGroup = async function(groupId) { + await this.engine.runStatement(` DELETE FROM groups WHERE group_id = $groupId `, { $groupId: groupId }); }; -SqlTiddlerDatabase.prototype.listGroups = function() { - return this.engine.runStatementGetAll(` +SqlTiddlerDatabase.prototype.listGroups = async function() { + return await this.engine.runStatementGetAll(` SELECT * FROM groups ORDER BY group_name `); }; // Role CRUD operations -SqlTiddlerDatabase.prototype.createRole = function(roleName, description) { - const result = this.engine.runStatement(` +SqlTiddlerDatabase.prototype.createRole = async function(roleName, description) { + const result = await this.engine.runStatement(` INSERT OR IGNORE INTO roles (role_name, description) VALUES ($roleName, $description) `, { @@ -1157,24 +1190,24 @@ SqlTiddlerDatabase.prototype.createRole = function(roleName, description) { return result.lastInsertRowid; }; -SqlTiddlerDatabase.prototype.getRole = function(roleId) { - return this.engine.runStatementGet(` +SqlTiddlerDatabase.prototype.getRole = async function(roleId) { + return await this.engine.runStatementGet(` SELECT * FROM roles WHERE role_id = $roleId `, { $roleId: roleId }); }; -SqlTiddlerDatabase.prototype.getRoleByName = function(roleName) { - return this.engine.runStatementGet(` +SqlTiddlerDatabase.prototype.getRoleByName = async function(roleName) { + return await this.engine.runStatementGet(` SELECT * FROM roles WHERE role_name = $roleName `, { $roleName: roleName }); } -SqlTiddlerDatabase.prototype.updateRole = function(roleId, roleName, description) { - this.engine.runStatement(` +SqlTiddlerDatabase.prototype.updateRole = async function(roleId, roleName, description) { + await this.engine.runStatement(` UPDATE roles SET role_name = $roleName, description = $description WHERE role_id = $roleId @@ -1185,23 +1218,23 @@ SqlTiddlerDatabase.prototype.updateRole = function(roleId, roleName, description }); }; -SqlTiddlerDatabase.prototype.deleteRole = function(roleId) { - this.engine.runStatement(` +SqlTiddlerDatabase.prototype.deleteRole = async function(roleId) { + await this.engine.runStatement(` DELETE FROM roles WHERE role_id = $roleId `, { $roleId: roleId }); }; -SqlTiddlerDatabase.prototype.listRoles = function() { - return this.engine.runStatementGetAll(` +SqlTiddlerDatabase.prototype.listRoles = async function() { + return await 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(` +SqlTiddlerDatabase.prototype.createPermission = async function(permissionName, description) { + const result = await this.engine.runStatement(` INSERT OR IGNORE INTO permissions (permission_name, description) VALUES ($permissionName, $description) `, { @@ -1211,24 +1244,24 @@ SqlTiddlerDatabase.prototype.createPermission = function(permissionName, descrip return result.lastInsertRowid; }; -SqlTiddlerDatabase.prototype.getPermission = function(permissionId) { - return this.engine.runStatementGet(` +SqlTiddlerDatabase.prototype.getPermission = async function(permissionId) { + return await this.engine.runStatementGet(` SELECT * FROM permissions WHERE permission_id = $permissionId `, { $permissionId: permissionId }); }; -SqlTiddlerDatabase.prototype.getPermissionByName = function(permissionName) { - return this.engine.runStatementGet(` +SqlTiddlerDatabase.prototype.getPermissionByName = async function(permissionName) { + return await this.engine.runStatementGet(` SELECT * FROM permissions WHERE permission_name = $permissionName `, { $permissionName: permissionName }); }; -SqlTiddlerDatabase.prototype.updatePermission = function(permissionId, permissionName, description) { - this.engine.runStatement(` +SqlTiddlerDatabase.prototype.updatePermission = async function(permissionId, permissionName, description) { + await this.engine.runStatement(` UPDATE permissions SET permission_name = $permissionName, description = $description WHERE permission_id = $permissionId @@ -1239,24 +1272,24 @@ SqlTiddlerDatabase.prototype.updatePermission = function(permissionId, permissio }); }; -SqlTiddlerDatabase.prototype.deletePermission = function(permissionId) { - this.engine.runStatement(` +SqlTiddlerDatabase.prototype.deletePermission = async function(permissionId) { + await this.engine.runStatement(` DELETE FROM permissions WHERE permission_id = $permissionId `, { $permissionId: permissionId }); }; -SqlTiddlerDatabase.prototype.listPermissions = function() { - return this.engine.runStatementGetAll(` +SqlTiddlerDatabase.prototype.listPermissions = async function() { + return await this.engine.runStatementGetAll(` SELECT * FROM permissions ORDER BY permission_name `); }; // ACL CRUD operations -SqlTiddlerDatabase.prototype.createACL = function(entityName, entityType, roleId, permissionId) { +SqlTiddlerDatabase.prototype.createACL = async function(entityName, entityType, roleId, permissionId) { if(!entityName.startsWith("$:/")) { - const result = this.engine.runStatement(` + const result = await this.engine.runStatement(` INSERT OR IGNORE INTO acl (entity_name, entity_type, role_id, permission_id) VALUES ($entityName, $entityType, $roleId, $permissionId) `, @@ -1270,16 +1303,16 @@ SqlTiddlerDatabase.prototype.createACL = function(entityName, entityType, roleId } }; -SqlTiddlerDatabase.prototype.getACL = function(aclId) { - return this.engine.runStatementGet(` +SqlTiddlerDatabase.prototype.getACL = async function(aclId) { + return await this.engine.runStatementGet(` SELECT * FROM acl WHERE acl_id = $aclId `, { $aclId: aclId }); }; -SqlTiddlerDatabase.prototype.updateACL = function(aclId, entityId, entityType, roleId, permissionId) { - this.engine.runStatement(` +SqlTiddlerDatabase.prototype.updateACL = async function(aclId, entityId, entityType, roleId, permissionId) { + await this.engine.runStatement(` UPDATE acl SET entity_name = $entityId, entity_type = $entityType, role_id = $roleId, permission_id = $permissionId @@ -1293,23 +1326,23 @@ SqlTiddlerDatabase.prototype.updateACL = function(aclId, entityId, entityType, r }); }; -SqlTiddlerDatabase.prototype.deleteACL = function(aclId) { - this.engine.runStatement(` +SqlTiddlerDatabase.prototype.deleteACL = async function(aclId) { + await this.engine.runStatement(` DELETE FROM acl WHERE acl_id = $aclId `, { $aclId: aclId }); }; -SqlTiddlerDatabase.prototype.listACLs = function() { - return this.engine.runStatementGetAll(` +SqlTiddlerDatabase.prototype.listACLs = async function() { + return await this.engine.runStatementGetAll(` SELECT * FROM acl ORDER BY entity_type, entity_name `); }; // Association management functions -SqlTiddlerDatabase.prototype.addUserToGroup = function(userId, groupId) { - this.engine.runStatement(` +SqlTiddlerDatabase.prototype.addUserToGroup = async function(userId, groupId) { + await this.engine.runStatement(` INSERT OR IGNORE INTO user_groups (user_id, group_id) VALUES ($userId, $groupId) `, { @@ -1318,8 +1351,8 @@ SqlTiddlerDatabase.prototype.addUserToGroup = function(userId, groupId) { }); }; -SqlTiddlerDatabase.prototype.isUserInGroup = function(userId, groupId) { - const result = this.engine.runStatementGet(` +SqlTiddlerDatabase.prototype.isUserInGroup = async function(userId, groupId) { + const result = await this.engine.runStatementGet(` SELECT 1 FROM user_groups WHERE user_id = $userId AND group_id = $groupId `, { @@ -1329,8 +1362,8 @@ SqlTiddlerDatabase.prototype.isUserInGroup = function(userId, groupId) { return result !== undefined; }; -SqlTiddlerDatabase.prototype.removeUserFromGroup = function(userId, groupId) { - this.engine.runStatement(` +SqlTiddlerDatabase.prototype.removeUserFromGroup = async function(userId, groupId) { + await this.engine.runStatement(` DELETE FROM user_groups WHERE user_id = $userId AND group_id = $groupId `, { @@ -1339,8 +1372,8 @@ SqlTiddlerDatabase.prototype.removeUserFromGroup = function(userId, groupId) { }); }; -SqlTiddlerDatabase.prototype.addRoleToUser = function(userId, roleId) { - this.engine.runStatement(` +SqlTiddlerDatabase.prototype.addRoleToUser = async function(userId, roleId) { + await this.engine.runStatement(` INSERT OR IGNORE INTO user_roles (user_id, role_id) VALUES ($userId, $roleId) `, { @@ -1349,8 +1382,8 @@ SqlTiddlerDatabase.prototype.addRoleToUser = function(userId, roleId) { }); }; -SqlTiddlerDatabase.prototype.removeRoleFromUser = function(userId, roleId) { - this.engine.runStatement(` +SqlTiddlerDatabase.prototype.removeRoleFromUser = async function(userId, roleId) { + await this.engine.runStatement(` DELETE FROM user_roles WHERE user_id = $userId AND role_id = $roleId `, { @@ -1359,8 +1392,8 @@ SqlTiddlerDatabase.prototype.removeRoleFromUser = function(userId, roleId) { }); }; -SqlTiddlerDatabase.prototype.addRoleToGroup = function(groupId, roleId) { - this.engine.runStatement(` +SqlTiddlerDatabase.prototype.addRoleToGroup = async function(groupId, roleId) { + await this.engine.runStatement(` INSERT OR IGNORE INTO group_roles (group_id, role_id) VALUES ($groupId, $roleId) `, { @@ -1369,8 +1402,8 @@ SqlTiddlerDatabase.prototype.addRoleToGroup = function(groupId, roleId) { }); }; -SqlTiddlerDatabase.prototype.removeRoleFromGroup = function(groupId, roleId) { - this.engine.runStatement(` +SqlTiddlerDatabase.prototype.removeRoleFromGroup = async function(groupId, roleId) { + await this.engine.runStatement(` DELETE FROM group_roles WHERE group_id = $groupId AND role_id = $roleId `, { @@ -1379,8 +1412,8 @@ SqlTiddlerDatabase.prototype.removeRoleFromGroup = function(groupId, roleId) { }); }; -SqlTiddlerDatabase.prototype.addPermissionToRole = function(roleId, permissionId) { - this.engine.runStatement(` +SqlTiddlerDatabase.prototype.addPermissionToRole = async function(roleId, permissionId) { + await this.engine.runStatement(` INSERT OR IGNORE INTO role_permissions (role_id, permission_id) VALUES ($roleId, $permissionId) `, { @@ -1389,8 +1422,8 @@ SqlTiddlerDatabase.prototype.addPermissionToRole = function(roleId, permissionId }); }; -SqlTiddlerDatabase.prototype.removePermissionFromRole = function(roleId, permissionId) { - this.engine.runStatement(` +SqlTiddlerDatabase.prototype.removePermissionFromRole = async function(roleId, permissionId) { + await this.engine.runStatement(` DELETE FROM role_permissions WHERE role_id = $roleId AND permission_id = $permissionId `, { @@ -1399,7 +1432,7 @@ SqlTiddlerDatabase.prototype.removePermissionFromRole = function(roleId, permiss }); }; -SqlTiddlerDatabase.prototype.getUserRoles = function(userId) { +SqlTiddlerDatabase.prototype.getUserRoles = async function(userId) { const query = ` SELECT r.role_id, r.role_name FROM user_roles ur @@ -1408,11 +1441,11 @@ SqlTiddlerDatabase.prototype.getUserRoles = function(userId) { LIMIT 1 `; - return this.engine.runStatementGet(query, { $userId: userId }); + return await this.engine.runStatementGet(query, { $userId: userId }); }; -SqlTiddlerDatabase.prototype.deleteUserRolesByRoleId = function(roleId) { - this.engine.runStatement(` +SqlTiddlerDatabase.prototype.deleteUserRolesByRoleId = async function(roleId) { + await this.engine.runStatement(` DELETE FROM user_roles WHERE role_id = $roleId `, { @@ -1420,8 +1453,8 @@ SqlTiddlerDatabase.prototype.deleteUserRolesByRoleId = function(roleId) { }); }; -SqlTiddlerDatabase.prototype.deleteUserRolesByUserId = function(userId) { - this.engine.runStatement(` +SqlTiddlerDatabase.prototype.deleteUserRolesByUserId = async function(userId) { + await this.engine.runStatement(` DELETE FROM user_roles WHERE user_id = $userId `, { @@ -1429,9 +1462,9 @@ SqlTiddlerDatabase.prototype.deleteUserRolesByUserId = function(userId) { }); }; -SqlTiddlerDatabase.prototype.isRoleInUse = function(roleId) { +SqlTiddlerDatabase.prototype.isRoleInUse = async function(roleId) { // Check if the role is assigned to any users - const userRoleCheck = this.engine.runStatementGet(` + const userRoleCheck = await this.engine.runStatementGet(` SELECT 1 FROM user_roles WHERE role_id = $roleId @@ -1445,7 +1478,7 @@ SqlTiddlerDatabase.prototype.isRoleInUse = function(roleId) { } // Check if the role is used in any ACLs - const aclRoleCheck = this.engine.runStatementGet(` + const aclRoleCheck = await this.engine.runStatementGet(` SELECT 1 FROM acl WHERE role_id = $roleId @@ -1462,8 +1495,8 @@ SqlTiddlerDatabase.prototype.isRoleInUse = function(roleId) { return false; }; -SqlTiddlerDatabase.prototype.getRoleById = function(roleId) { - const role = this.engine.runStatementGet(` +SqlTiddlerDatabase.prototype.getRoleById = async function(roleId) { + const role = await this.engine.runStatementGet(` SELECT role_id, role_name, description FROM roles WHERE role_id = $roleId diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js index bb32eba18d3..078da381ce2 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js @@ -23,8 +23,18 @@ adminWiki - reference to $tw.Wiki object used for configuration attachmentStore - reference to associated attachment store engine - wasm | better */ +/** + * + * @param {Object} options + * @param {string} [options.databasePath] path to the database file (can be ":memory:" or missing to get a temporary database) + * @param {$TW.Wiki} [options.adminWiki] reference to $tw.Wiki object used for configuration + * @param {import("./attachments.js").AttachmentStore} options.attachmentStore reference to associated attachment store + * @param {"node" | "wasm" | "better"} [options.engine] which engine to use, default is "node" + */ function SqlTiddlerStore(options) { - options = options || {}; + if(!options?.attachmentStore) { + throw new Error("SqlTiddlerStore requires an attachment store"); + } this.attachmentStore = options.attachmentStore; this.adminWiki = options.adminWiki || $tw.wiki; this.eventListeners = {}; // Hashmap by type of array of event listener functions @@ -36,9 +46,14 @@ function SqlTiddlerStore(options) { databasePath: this.databasePath, engine: options.engine }); - this.sqlTiddlerDatabase.createTables(); + } +SqlTiddlerStore.prototype.init = async function() { + await this.sqlTiddlerDatabase.init(); + await this.sqlTiddlerDatabase.createTables(); +}; + SqlTiddlerStore.prototype.addEventListener = function(type,listener) { this.eventListeners[type] = this.eventListeners[type] || []; this.eventListeners[type].push(listener); @@ -116,8 +131,8 @@ SqlTiddlerStore.prototype.validateItemNames = function(names,allowPrivilegedChar } }; -SqlTiddlerStore.prototype.close = function() { - this.sqlTiddlerDatabase.close(); +SqlTiddlerStore.prototype.close = async function() { + await this.sqlTiddlerDatabase.close(); this.sqlTiddlerDatabase = undefined; }; @@ -156,7 +171,7 @@ SqlTiddlerStore.prototype.processIncomingTiddler = function(tiddlerFields, exist if(existing_attachment_blob) { const fileSize = this.attachmentStore.getAttachmentFileSize(existing_attachment_blob); - if(fileSize <= attachmentSizeLimit) { + 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)); @@ -190,26 +205,26 @@ SqlTiddlerStore.prototype.processIncomingTiddler = function(tiddlerFields, exist } }; -SqlTiddlerStore.prototype.saveTiddlersFromPath = function(tiddler_files_path,bag_name) { +SqlTiddlerStore.prototype.saveTiddlersFromPath = async function(tiddler_files_path,bag_name) { var self = this; - this.sqlTiddlerDatabase.transaction(function() { + await this.sqlTiddlerDatabase.transaction(async function() { // Clear out the bag - self.deleteAllTiddlersInBag(bag_name); + await 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); + await self.saveBagTiddler(tiddler,bag_name); } } }); self.dispatchEvent("change"); }; -SqlTiddlerStore.prototype.listBags = function() { - return this.sqlTiddlerDatabase.listBags(); +SqlTiddlerStore.prototype.listBags = async function() { + return await this.sqlTiddlerDatabase.listBags(); }; /* @@ -217,22 +232,22 @@ Options include: allowPrivilegedCharacters - allows "$", ":" and "/" to appear in recipe name */ -SqlTiddlerStore.prototype.createBag = function(bag_name,description,options) { +SqlTiddlerStore.prototype.createBag = async function(bag_name,description,options) { options = options || {}; var self = this; - return this.sqlTiddlerDatabase.transaction(function() { + return await this.sqlTiddlerDatabase.transaction(async function() { const validationBagName = self.validateItemName(bag_name,options.allowPrivilegedCharacters); if(validationBagName) { return {message: validationBagName}; } - self.sqlTiddlerDatabase.createBag(bag_name,description); + await self.sqlTiddlerDatabase.createBag(bag_name,description); self.dispatchEvent("change"); return null; }); }; -SqlTiddlerStore.prototype.listRecipes = function() { - return this.sqlTiddlerDatabase.listRecipes(); +SqlTiddlerStore.prototype.listRecipes = async function() { + return await this.sqlTiddlerDatabase.listRecipes(); }; /* @@ -242,7 +257,7 @@ Options include: allowPrivilegedCharacters - allows "$", ":" and "/" to appear in recipe name */ -SqlTiddlerStore.prototype.createRecipe = function(recipe_name,bag_names,description,options) { +SqlTiddlerStore.prototype.createRecipe = async function(recipe_name,bag_names,description,options) { bag_names = bag_names || []; description = description || ""; options = options || {}; @@ -254,8 +269,8 @@ SqlTiddlerStore.prototype.createRecipe = function(recipe_name,bag_names,descript 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); + return await this.sqlTiddlerDatabase.transaction(async function() { + await self.sqlTiddlerDatabase.createRecipe(recipe_name,bag_names,description); self.dispatchEvent("change"); return null; }); @@ -264,14 +279,14 @@ SqlTiddlerStore.prototype.createRecipe = function(recipe_name,bag_names,descript /* Returns {tiddler_id:} */ -SqlTiddlerStore.prototype.saveBagTiddler = function(incomingTiddlerFields,bag_name) { +SqlTiddlerStore.prototype.saveBagTiddler = async function(incomingTiddlerFields,bag_name) { let _canonical_uri; - const existing_attachment_blob = this.sqlTiddlerDatabase.getBagTiddlerAttachmentBlob(incomingTiddlerFields.title,bag_name) + const existing_attachment_blob = await 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); + const result = await this.sqlTiddlerDatabase.saveBagTiddler(tiddlerFields,bag_name,attachment_blob); this.dispatchEvent("change"); return result; }; @@ -286,10 +301,10 @@ type - content type of file as uploaded Returns {tiddler_id:} */ -SqlTiddlerStore.prototype.saveBagTiddlerWithAttachment = function(incomingTiddlerFields,bag_name,options) { +SqlTiddlerStore.prototype.saveBagTiddlerWithAttachment = async 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); + const result = await this.sqlTiddlerDatabase.saveBagTiddler(incomingTiddlerFields,bag_name,attachment_blob); this.dispatchEvent("change"); return result; } else { @@ -300,16 +315,16 @@ SqlTiddlerStore.prototype.saveBagTiddlerWithAttachment = function(incomingTiddle /* Returns {tiddler_id:,bag_name:} */ -SqlTiddlerStore.prototype.saveRecipeTiddler = function(incomingTiddlerFields,recipe_name) { - const existing_attachment_blob = this.sqlTiddlerDatabase.getRecipeTiddlerAttachmentBlob(incomingTiddlerFields.title,recipe_name) +SqlTiddlerStore.prototype.saveRecipeTiddler = async function(incomingTiddlerFields,recipe_name) { + const existing_attachment_blob = await 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); + const result = await 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); +SqlTiddlerStore.prototype.deleteTiddler = async function(title,bag_name) { + const result = await this.sqlTiddlerDatabase.deleteTiddler(title,bag_name); this.dispatchEvent("change"); return result; }; @@ -317,10 +332,10 @@ SqlTiddlerStore.prototype.deleteTiddler = function(title,bag_name) { /* returns {tiddler_id:,tiddler:} */ -SqlTiddlerStore.prototype.getBagTiddler = function(title,bag_name) { - var tiddlerInfo = this.sqlTiddlerDatabase.getBagTiddler(title,bag_name); +SqlTiddlerStore.prototype.getBagTiddler = async function(title,bag_name) { + var tiddlerInfo = await this.sqlTiddlerDatabase.getBagTiddler(title,bag_name); if(tiddlerInfo) { - return Object.assign( + return await Object.assign( {}, tiddlerInfo, { @@ -338,8 +353,8 @@ 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); +SqlTiddlerStore.prototype.getBagTiddlerStream = async function(title,bag_name) { + const tiddlerInfo = await this.sqlTiddlerDatabase.getBagTiddler(title,bag_name); if(tiddlerInfo) { if(tiddlerInfo.attachment_blob) { return $tw.utils.extend( @@ -375,8 +390,8 @@ SqlTiddlerStore.prototype.getBagTiddlerStream = function(title,bag_name) { /* Returns {bag_name:, tiddler: {fields}, tiddler_id:} */ -SqlTiddlerStore.prototype.getRecipeTiddler = function(title,recipe_name) { - var tiddlerInfo = this.sqlTiddlerDatabase.getRecipeTiddler(title,recipe_name); +SqlTiddlerStore.prototype.getRecipeTiddler = async function(title,recipe_name) { + var tiddlerInfo = await this.sqlTiddlerDatabase.getRecipeTiddler(title,recipe_name); if(tiddlerInfo) { return Object.assign( {}, @@ -392,35 +407,35 @@ SqlTiddlerStore.prototype.getRecipeTiddler = function(title,recipe_name) { /* 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); +SqlTiddlerStore.prototype.getBagTiddlers = async function(bag_name) { + return await 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); +SqlTiddlerStore.prototype.getBagLastTiddlerId = async function(bag_name) { + return await 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); +SqlTiddlerStore.prototype.getRecipeTiddlers = async function(recipe_name,options) { + return await 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.getRecipeLastTiddlerId = async function(recipe_name) { + return await this.sqlTiddlerDatabase.getRecipeLastTiddlerId(recipe_name); }; -SqlTiddlerStore.prototype.deleteAllTiddlersInBag = function(bag_name) { +SqlTiddlerStore.prototype.deleteAllTiddlersInBag = async function(bag_name) { var self = this; - return this.sqlTiddlerDatabase.transaction(function() { - const result = self.sqlTiddlerDatabase.deleteAllTiddlersInBag(bag_name); + return await this.sqlTiddlerDatabase.transaction(async function() { + const result = await self.sqlTiddlerDatabase.deleteAllTiddlersInBag(bag_name); self.dispatchEvent("change"); return result; }); @@ -429,8 +444,8 @@ SqlTiddlerStore.prototype.deleteAllTiddlersInBag = function(bag_name) { /* 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); +SqlTiddlerStore.prototype.getRecipeBags = async function(recipe_name) { + return await this.sqlTiddlerDatabase.getRecipeBags(recipe_name); }; exports.SqlTiddlerStore = SqlTiddlerStore; 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..f18f13e3655 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,97 @@ function runSqlDatabaseTests(engine) { const sqlTiddlerDatabase = new SqlTiddlerDatabase({ engine: engine }); - sqlTiddlerDatabase.createTables(); + // eslint-disable-next-line custom-rules/always-await + const beforeStart = sqlTiddlerDatabase.init(); + beforeAll(async () => { + await beforeStart; + 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 +131,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"); } }); });