From 0cc52a382033f75e81782b7f612ff57f602cefa5 Mon Sep 17 00:00:00 2001 From: arlen22 Date: Thu, 9 Jan 2025 04:38:46 -0500 Subject: [PATCH 01/14] setup typing and linting --- package-lock.json | 878 +++++++++++++++++- package.json | 9 +- .../multiwikiserver/eslint.config.js | 365 ++++++++ .../tiddlywiki/multiwikiserver/globals.d.ts | 71 ++ .../tiddlywiki/multiwikiserver/jsconfig.json | 22 + 5 files changed, 1297 insertions(+), 48 deletions(-) create mode 100644 plugins/tiddlywiki/multiwikiserver/eslint.config.js create mode 100644 plugins/tiddlywiki/multiwikiserver/globals.d.ts create mode 100644 plugins/tiddlywiki/multiwikiserver/jsconfig.json diff --git a/package-lock.json b/package-lock.json index a6a2620dc08..a4518f68bab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,15 +16,41 @@ "tiddlywiki": "tiddlywiki.js" }, "devDependencies": { - "@eslint/js": "^9.12.0", + "@eslint/js": "^9.17.0", "@playwright/test": "^1.47.2", - "eslint": "^9.12.0", - "playwright": "^1.47.2" + "@types/jest": "^29.5.14", + "eslint": "^9.17.0", + "playwright": "^1.47.2", + "typescript": "^5.7.2", + "typescript-eslint": "^8.19.1" }, "engines": { "node": ">=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", @@ -65,12 +91,12 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", - "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", + "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", "dev": true, "dependencies": { - "@eslint/object-schema": "^2.1.4", + "@eslint/object-schema": "^2.1.5", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -79,18 +105,21 @@ } }, "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.9.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz", + "integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==", "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, "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 +140,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.17.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz", + "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==", "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" @@ -201,6 +230,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 +321,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 +720,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 +785,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 +825,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 +839,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 +891,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,26 +921,26 @@ } }, "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.17.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz", + "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.18.0", - "@eslint/core": "^0.7.0", - "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.14.0", - "@eslint/plugin-kit": "^0.2.0", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.9.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.17.0", + "@eslint/plugin-kit": "^0.2.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.0", + "@humanwhocodes/retry": "^0.4.1", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", @@ -552,8 +959,7 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" @@ -668,12 +1074,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 +1136,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 +1162,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 +1257,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 +1362,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 +1532,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 +1708,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 +1790,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 +1834,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 +1876,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 +1904,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 +2031,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", @@ -1390,11 +2119,29 @@ "node": ">=6" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "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", @@ -1419,6 +2166,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..d686d1ec27a 100644 --- a/package.json +++ b/package.json @@ -24,10 +24,13 @@ "wiki" ], "devDependencies": { + "@eslint/js": "^9.17.0", "@playwright/test": "^1.47.2", - "eslint": "^9.12.0", - "@eslint/js": "^9.12.0", - "playwright": "^1.47.2" + "@types/jest": "^29.5.14", + "eslint": "^9.17.0", + "playwright": "^1.47.2", + "typescript": "^5.7.2", + "typescript-eslint": "^8.19.1" }, "license": "BSD", "engines": { diff --git a/plugins/tiddlywiki/multiwikiserver/eslint.config.js b/plugins/tiddlywiki/multiwikiserver/eslint.config.js new file mode 100644 index 00000000000..2ee6d86cf3f --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/eslint.config.js @@ -0,0 +1,365 @@ +//@ts-check +const globals = require("globals"); +const js = require("@eslint/js"); +const ts = require("typescript-eslint"); +// const { +// FlatCompat, +// } = require("@eslint/eslintrc"); + +// const compat = new FlatCompat({ +// baseDirectory: __dirname, +// recommendedConfig: ts.config( +// js.configs.recommended, +// ts.configs.recommended, +// ), +// allConfig: ts.config( +// js.configs.all, +// ts.configs.all, +// ), +// }); + +// import eslint from '@eslint/js'; +// import tseslint from 'typescript-eslint'; + +module.exports = ts.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/", + ] + }, + js.configs.recommended, + ts.configs.base, + { + 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: "off", + "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/promise-function-async": ["error", { + allowAny: true, + allowedPromiseNames: [], + checkArrowFunctions: true, + checkFunctionDeclarations: true, + checkFunctionExpressions: true, + checkMethodDeclarations: true, + }], + "@typescript-eslint/no-misused-promises": "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..21f106f0a2c --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/globals.d.ts @@ -0,0 +1,71 @@ +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; + 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 From 48e56fa7ba2b0e5f96c2176d91381b8ab64398ea Mon Sep 17 00:00:00 2001 From: arlen22 Date: Thu, 9 Jan 2025 04:39:10 -0500 Subject: [PATCH 02/14] handle promise based paths --- boot/boot.js | 31 ++++++++++++++++++++++--------- core/modules/commander.js | 24 +++++++++++++++--------- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/boot/boot.js b/boot/boot.js index ab403aa5ae8..f74b62d81fd 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 Date: Thu, 9 Jan 2025 12:23:42 -0500 Subject: [PATCH 03/14] simple async changes --- .../modules/commands/mws-add-permission.js | 4 +- .../modules/commands/mws-add-role.js | 4 +- .../modules/commands/mws-add-user.js | 6 +- .../commands/mws-assign-role-permission.js | 10 +-- .../modules/commands/mws-assign-user-role.js | 8 +- .../modules/commands/mws-create-bag.js | 4 +- .../modules/commands/mws-create-recipe.js | 6 +- .../modules/commands/mws-load-archive.js | 15 ++-- .../modules/commands/mws-load-plugin-bags.js | 83 +++++++++++-------- .../modules/commands/mws-load-tiddlers.js | 4 +- .../modules/commands/mws-load-wiki-folder.js | 12 +-- .../modules/commands/mws-save-archive.js | 20 +++-- .../modules/commands/mws-save-tiddler-text.js | 4 +- .../routes/handlers/change-user-password.js | 7 +- .../modules/routes/handlers/delete-acl.js | 8 +- .../routes/handlers/delete-bag-tiddler.js | 8 +- .../modules/routes/handlers/delete-role.js | 12 +-- .../routes/handlers/delete-user-account.js | 16 ++-- .../modules/routes/handlers/get-acl.js | 18 ++-- .../routes/handlers/get-bag-tiddler-blob.js | 8 +- .../routes/handlers/get-bag-tiddler.js | 10 +-- .../modules/routes/handlers/get-bag.js | 6 +- .../modules/routes/handlers/get-index.js | 10 +-- .../modules/routes/handlers/get-login.js | 6 +- .../routes/handlers/get-recipe-events.js | 12 +-- .../routes/handlers/get-recipe-tiddler.js | 8 +- .../handlers/get-recipe-tiddlers-json.js | 6 +- .../modules/routes/handlers/get-system.js | 5 +- .../modules/routes/handlers/get-users.js | 8 +- .../modules/routes/handlers/get-wiki.js | 22 +++-- .../modules/routes/handlers/manage-roles.js | 8 +- .../modules/routes/handlers/manage-user.js | 10 +-- .../modules/routes/handlers/post-acl.js | 8 +- .../routes/handlers/post-anon-config.js | 5 +- .../modules/routes/handlers/post-anon.js | 8 +- .../routes/handlers/post-bag-tiddlers.js | 5 +- .../modules/routes/handlers/post-bag.js | 6 +- .../modules/routes/handlers/post-login.js | 8 +- .../modules/routes/handlers/post-logout.js | 10 +-- .../modules/routes/handlers/post-recipe.js | 8 +- .../modules/routes/handlers/post-role.js | 8 +- .../modules/routes/handlers/post-user.js | 16 ++-- .../modules/routes/handlers/put-bag.js | 6 +- .../routes/handlers/put-recipe-tiddler.js | 6 +- .../modules/routes/handlers/put-recipe.js | 6 +- .../modules/routes/handlers/update-role.js | 8 +- .../routes/handlers/update-user-profile.js | 8 +- .../modules/routes/helpers/acl-middleware.js | 2 +- .../modules/routes/helpers/multipart-forms.js | 18 +++- .../multiwikiserver/modules/startup.js | 54 +++++------- .../modules/store/attachments.js | 46 +++++----- 51 files changed, 312 insertions(+), 292 deletions(-) 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 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..4cddbe6f39a 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,15 +33,15 @@ 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'))) + (!state.authenticatedUser || (recipeAclRecords.length > 0 && !await sqlTiddlerDatabase.hasRecipePermission(state.authenticatedUser.user_id, recipeName, "WRITE"))) ){ response.writeHead(403, "Forbidden"); response.end(); 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..c3753a4c43e 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) { +/** @type {ServerRouteHandler} */ +exports.handler = async function(request,response,state) { 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..657ed72b9d0 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) { +/** @type {ServerRouteHandler} */ +exports.handler = async function(request,response,state) { 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..9984fbeece0 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-index.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-index.js @@ -15,16 +15,16 @@ 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",{ 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..6beeda2fc02 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"); } 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..8ec85aef97b 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"); @@ -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..d0a32c647f6 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,7 +109,7 @@ 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 @@ -118,7 +118,7 @@ exports.handler = function(request, response, state) { 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..bd6bdb594cf 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-role.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-role.js @@ -19,8 +19,8 @@ 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; @@ -33,7 +33,7 @@ exports.handler = function(request, response, state) { } // 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..8c38d8301cf 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 diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/multipart-forms.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/multipart-forms.js index 6a46699fabb..7b066c3e1e8 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/multipart-forms.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/multipart-forms.js @@ -19,6 +19,15 @@ response - provided by server.js bag_name - name of bag to write to callback - invoked as callback(err,results). Results is an array of titles of imported tiddlers */ +/** + * + * @param {Object} options + * @param {SqlTiddlerStore} options.store + * @param {ServerState} options.state + * @param {ServerResponse} options.response + * @param {string} options.bag_name + * @param {function} options.callback + */ exports.processIncomingStream = function(options) { const self = this; const path = require("path"), @@ -72,7 +81,7 @@ exports.processIncomingStream = function(options) { } else { const partFile = parts.find(part => part.name === "file-to-upload" && !!part.filename); if(!partFile) { - return state.sendResponse(400, {"Content-Type": "text/plain"},"Missing file to upload"); + return options.state.sendResponse(400, {"Content-Type": "text/plain"},"Missing file to upload"); } const type = partFile.headers["content-type"]; const tiddlerFields = { @@ -89,9 +98,12 @@ exports.processIncomingStream = function(options) { filepath: partFile.inboxFilename, type: type, hash: partFile.hash + }).then(() => { + $tw.utils.deleteDirectory(inboxPath); + options.callback(null,[tiddlerFields.title]); + }, err => { + options.callback(err); }); - $tw.utils.deleteDirectory(inboxPath); - options.callback(null,[tiddlerFields.title]); } } }); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/startup.js b/plugins/tiddlywiki/multiwikiserver/modules/startup.js index 42686e889be..01dfef6242e 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.initCheck(); + + 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; })(); From 26d73c04c3166ae4d10915e2e6983f48744b0c7c Mon Sep 17 00:00:00 2001 From: arlen22 Date: Thu, 9 Jan 2025 12:27:02 -0500 Subject: [PATCH 04/14] store changes, first pass (ignore whitespace) --- .../multiwikiserver/modules/mws-server.js | 239 ++- .../modules/store/sql-engine.js | 99 +- .../modules/store/sql-tiddler-database.js | 1770 +++++++++-------- .../modules/store/sql-tiddler-store.js | 338 ++-- 4 files changed, 1311 insertions(+), 1135 deletions(-) diff --git a/plugins/tiddlywiki/multiwikiserver/modules/mws-server.js b/plugins/tiddlywiki/multiwikiserver/modules/mws-server.js index 132159a8fe9..a95507c79a4 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/mws-server.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/mws-server.js @@ -20,7 +20,8 @@ if($tw.node) { querystring = require("querystring"), crypto = require("crypto"), zlib = require("zlib"), - aclMiddleware = require('$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js').middleware; + aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/routes/helpers/acl-middleware.js").middleware; + } /* @@ -29,14 +30,21 @@ options: variables - optional hashmap of variables to set (a misnomer - they are routes - optional array of routes to use wiki - reference to wiki object */ +/** + * + * @param {ServerOptions} options + */ function Server(options) { var self = this; + /** @type {ServerRoute[]} */ this.routes = options.routes || []; this.authenticators = options.authenticators || []; this.wiki = options.wiki; this.boot = options.boot || $tw.boot; + /** @type {SqlTiddlerDatabase} */ this.sqlTiddlerDatabase = options.sqlTiddlerDatabase || $tw.mws.store.sqlTiddlerDatabase; // Initialise the variables + /** @type {ServerOptions["variables"]} */ this.variables = $tw.utils.extend({},this.defaultVariables); if(options.variables) { for(var variable in options.variables) { @@ -76,10 +84,12 @@ function Server(options) { }); // Load route handlers $tw.modules.forEachModuleOfType("mws-route", function(title,routeDefinition) { - self.addRoute(routeDefinition); + self.addRoute(routeDefinition, title); }); // Initialise the http vs https + /** @type {import("https").ServerOptions} */ this.listenOptions = null; + /** @type {"http" | "https"} */ this.protocol = "http"; var tlsKeyFilepath = this.get("tls-key"), tlsCertFilepath = this.get("tls-cert"), @@ -92,6 +102,7 @@ function Server(options) { }; this.protocol = "https"; } + /** @type {import("http")} */ this.transport = require(this.protocol); // Name the server and init the boot state this.servername = $tw.utils.transliterateToSafeASCII(this.get("server-name") || this.wiki.getTiddlerText("$:/SiteTitle") || "TiddlyWiki5"); @@ -132,7 +143,7 @@ function sendResponse(request,response,statusCode,headers,data,encoding) { // We do not implement "*" as it makes no sense here. var ifNoneMatch = request.headers["if-none-match"]; if(ifNoneMatch) { - var matchParts = ifNoneMatch.split(",").map(function(etag) { + var matchParts = ifNoneMatch.split(",").map(function(/** @type {string} */ etag) { return etag.replace(/^[ "]+|[ "]+$/g, ""); }); if(matchParts.indexOf(contentDigest) != -1) { @@ -178,6 +189,15 @@ cbPartChunk(chunk) - invoked when a chunk of a file is received cbPartEnd() - invoked when a file finishes being received cbFinished(err) - invoked when the all the form data has been processed */ +/** + * + * @param {import("http").IncomingMessage} request + * @param {Object} options + * @param {(headers: Object, name: string, filename: string) => void} options.cbPartStart + * @param {(chunk: Buffer) => void} options.cbPartChunk + * @param {() => void} options.cbPartEnd + * @param {(err: string) => void} options.cbFinished + */ function streamMultipartData(request,options) { // Check that the Content-Type is multipart/form-data const contentType = request.headers['content-type']; @@ -215,6 +235,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(": "); @@ -292,16 +313,49 @@ 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); +Server.prototype.addRoute = function(route, title) { + if(!route.path) $tw.utils.log("Warning: Route has no path: " + title); + else this.routes.push(route); }; Server.prototype.addAuthenticator = function(AuthenticatorClass) { @@ -315,9 +369,15 @@ Server.prototype.addAuthenticator = function(AuthenticatorClass) { this.authenticators.push(authenticator); } }; - -Server.prototype.findMatchingRoute = function(request,state) { - for(var t=0; t} */(new Promise((resolve) => { + /** @type {any} */ + 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; + 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(); @@ -541,6 +630,8 @@ prefix: optional prefix (falls back to value of "path-prefix" variable) callback: optional callback(err) to be invoked when the listener is up and running */ Server.prototype.listen = function(port,host,prefix,options) { + const { ok } = require("assert"); + var self = this; // Handle defaults for port and host port = port || this.get("port"); @@ -563,6 +654,7 @@ Server.prototype.listen = function(port,host,prefix,options) { $tw.utils.warning(error); } // Create the server + require("https").createServer var server = this.transport.createServer(this.listenOptions || {},function(request,response,options) { if(self.get("debug-level") !== "none") { var start = $tw.utils.timer(); @@ -570,7 +662,7 @@ Server.prototype.listen = function(port,host,prefix,options) { console.log("Response time:",request.method,request.url,$tw.utils.timer() - start); }); } - self.requestHandler(request,response,options); + void self.requestHandler(request,response,options); }); // Display the port number after we've started listening (the port number might have been specified as zero, in which case we will get an assigned port) server.on("listening",function() { @@ -579,8 +671,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", "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 +686,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/store/sql-engine.js b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-engine.js index 00d15edf370..9421caa428c 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-engine.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-engine.js @@ -9,7 +9,8 @@ This class is intended to encapsulate all engine-specific logic. \*/ -(function() { +(function () { + /* Create a database engine. Options include: @@ -17,14 +18,17 @@ Create a database engine. Options include: databasePath - path to the database file (can be ":memory:" or missing to get a temporary database) engine - wasm | better */ -function SqlEngine(options) { +class SqlEngine { +constructor(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); @@ -43,7 +47,7 @@ function SqlEngine(options) { Database = require("better-sqlite3"); break; } - this.db = new Database(databasePath,{ + this.db = new Database(databasePath, { verbose: undefined && console.log }); // Turn on WAL mode for better-sqlite3 @@ -53,18 +57,18 @@ function SqlEngine(options) { } } -SqlEngine.prototype.close = function() { +async close() { 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(); this.db = undefined; -}; +} -SqlEngine.prototype.normaliseParams = function(params) { +normaliseParams(params) { params = params || {}; const result = Object.create(null); for(const paramName in params) { @@ -75,67 +79,88 @@ SqlEngine.prototype.normaliseParams = function(params) { } } return result; -}; +} -SqlEngine.prototype.prepareStatement = function(sql) { +async prepareStatement(sql) { if(!(sql in this.statements)) { - this.statements[sql] = this.db.prepare(sql); + // node:sqlite supports bigint, causing an error here + this.statements[sql] = await this.db.prepare(sql); } return this.statements[sql]; -}; +} -SqlEngine.prototype.runStatement = function(sql,params) { +/** + * @returns {Promise} + */ +async runStatement(sql, params) { params = this.normaliseParams(params); - const statement = this.prepareStatement(sql); - return statement.run(params); -}; + const statement = await this.prepareStatement(sql); + return await statement.run(params); +} -SqlEngine.prototype.runStatementGet = function(sql,params) { +/** + * @param {string} sql + * @returns {Promise} + */ +async runStatementGet(sql, params) { params = this.normaliseParams(params); - const statement = this.prepareStatement(sql); - return statement.get(params); -}; + const statement = await this.prepareStatement(sql); + return await statement.get(params); +} -SqlEngine.prototype.runStatementGetAll = function(sql,params) { +/** + * @returns {Promise} + */ +async runStatementGetAll(sql, params) { params = this.normaliseParams(params); - const statement = this.prepareStatement(sql); - return statement.all(params); -}; + const statement = await this.prepareStatement(sql); + return await statement.all(params); +} -SqlEngine.prototype.runStatements = function(sqlArray) { +/** + * @returns {Promise} + */ +async runStatements(sqlArray) { + const res = []; for(const sql of sqlArray) { - this.runStatement(sql); + res.push(await this.runStatement(sql)); } -}; + return res; +} -/* +/** 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 + +@param {() => Promise} fn - function to execute in the transaction +@returns {Promise} - the result +@template T */ -SqlEngine.prototype.transaction = function(fn) { +async transaction(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`); - throw(e); + await this.runStatement("ROLLBACK TRANSACTION"); + throw (e); } return result; } - } finally { + } finally{ this.transactionDepth--; } -}; +} +} exports.SqlEngine = SqlEngine; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js index 14f8641f4ce..ecd5c1f2a5b 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js @@ -10,44 +10,45 @@ Validation is for the most part left to the caller \*/ -(function() { - -/* -Create a tiddler store. Options include: - -databasePath - path to the database file (can be ":memory:" to get a temporary database) -engine - wasm | better -*/ -function SqlTiddlerDatabase(options) { - options = options || {}; - const SqlEngine = require("$:/plugins/tiddlywiki/multiwikiserver/store/sql-engine.js").SqlEngine; - this.engine = new SqlEngine({ - databasePath: options.databasePath, - engine: options.engine - }); - this.entityTypeToTableMap = { - bag: { - table: "bags", - column: "bag_name" - }, - recipe: { - table: "recipes", - column: "recipe_name" - } - }; -} - -SqlTiddlerDatabase.prototype.close = function() { - this.engine.close(); -}; - - -SqlTiddlerDatabase.prototype.transaction = function(fn) { - return this.engine.transaction(fn); -}; - -SqlTiddlerDatabase.prototype.createTables = function() { - this.engine.runStatements([` +(function () { + + /* + Create a tiddler store. Options include: + + databasePath - path to the database file (can be ":memory:" to get a temporary database) + engine - wasm | better + */ + class SqlTiddlerDatabase { + constructor(options) { + options = options || {}; + /** @type {typeof import("./sql-engine").SqlEngine} */ + const SqlEngine = require("$:/plugins/tiddlywiki/multiwikiserver/store/sql-engine.js").SqlEngine; + this.engine = new SqlEngine({ + databasePath: options.databasePath, + engine: options.engine + }); + this.entityTypeToTableMap = { + bag: { + table: "bags", + column: "bag_name" + }, + recipe: { + table: "recipes", + column: "recipe_name" + } + }; + } + + async close() { + await this.engine.close(); + } + + async transaction(fn) { + return await this.engine.transaction(fn); + } + + async createTables() { + await this.engine.runStatements([` -- Users table CREATE TABLE IF NOT EXISTS users ( user_id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -57,7 +58,7 @@ SqlTiddlerDatabase.prototype.createTables = function() { created_at TEXT DEFAULT (datetime('now')), last_login TEXT ) - `,` + `, ` -- User Session table CREATE TABLE IF NOT EXISTS sessions ( user_id INTEGER NOT NULL, @@ -67,28 +68,28 @@ SqlTiddlerDatabase.prototype.createTables = function() { PRIMARY KEY (session_id), FOREIGN KEY (user_id) REFERENCES users(user_id) ) - `,` + `, ` -- Groups table CREATE TABLE IF NOT EXISTS groups ( group_id INTEGER PRIMARY KEY AUTOINCREMENT, group_name TEXT UNIQUE NOT NULL, description TEXT ) - `,` + `, ` -- Roles table CREATE TABLE IF NOT EXISTS roles ( role_id INTEGER PRIMARY KEY AUTOINCREMENT, role_name TEXT UNIQUE NOT NULL, description TEXT ) - `,` + `, ` -- Permissions table CREATE TABLE IF NOT EXISTS permissions ( permission_id INTEGER PRIMARY KEY AUTOINCREMENT, permission_name TEXT UNIQUE NOT NULL, description TEXT ) - `,` + `, ` -- User-Group association table CREATE TABLE IF NOT EXISTS user_groups ( user_id INTEGER, @@ -97,7 +98,7 @@ SqlTiddlerDatabase.prototype.createTables = function() { FOREIGN KEY (user_id) REFERENCES users(user_id), FOREIGN KEY (group_id) REFERENCES groups(group_id) ) - `,` + `, ` -- User-Role association table CREATE TABLE IF NOT EXISTS user_roles ( user_id INTEGER, @@ -106,7 +107,7 @@ SqlTiddlerDatabase.prototype.createTables = function() { FOREIGN KEY (user_id) REFERENCES users(user_id), FOREIGN KEY (role_id) REFERENCES roles(role_id) ) - `,` + `, ` -- Group-Role association table CREATE TABLE IF NOT EXISTS group_roles ( group_id INTEGER, @@ -115,7 +116,7 @@ SqlTiddlerDatabase.prototype.createTables = function() { FOREIGN KEY (group_id) REFERENCES groups(group_id), FOREIGN KEY (role_id) REFERENCES roles(role_id) ) - `,` + `, ` -- Role-Permission association table CREATE TABLE IF NOT EXISTS role_permissions ( role_id INTEGER, @@ -124,7 +125,7 @@ SqlTiddlerDatabase.prototype.createTables = function() { FOREIGN KEY (role_id) REFERENCES roles(role_id), FOREIGN KEY (permission_id) REFERENCES permissions(permission_id) ) - `,` + `, ` -- Bags have names and access control settings CREATE TABLE IF NOT EXISTS bags ( bag_id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -132,7 +133,7 @@ SqlTiddlerDatabase.prototype.createTables = function() { accesscontrol TEXT NOT NULL, description TEXT NOT NULL ) - `,` + `, ` -- Recipes have names... CREATE TABLE IF NOT EXISTS recipes ( recipe_id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -141,7 +142,7 @@ SqlTiddlerDatabase.prototype.createTables = function() { owner_id INTEGER, FOREIGN KEY (owner_id) REFERENCES users(user_id) ) - `,` + `, ` -- ...and recipes also have an ordered list of bags CREATE TABLE IF NOT EXISTS recipe_bags ( recipe_id INTEGER NOT NULL, @@ -151,7 +152,7 @@ SqlTiddlerDatabase.prototype.createTables = function() { FOREIGN KEY (bag_id) REFERENCES bags(bag_id) ON UPDATE CASCADE ON DELETE CASCADE, UNIQUE (recipe_id, bag_id) ) - `,` + `, ` -- Tiddlers are contained in bags and have titles CREATE TABLE IF NOT EXISTS tiddlers ( tiddler_id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -162,7 +163,7 @@ SqlTiddlerDatabase.prototype.createTables = function() { FOREIGN KEY (bag_id) REFERENCES bags(bag_id) ON UPDATE CASCADE ON DELETE CASCADE, UNIQUE (bag_id, title) ) - `,` + `, ` -- Tiddlers also have unordered lists of fields, each of which has a name and associated value CREATE TABLE IF NOT EXISTS fields ( tiddler_id INTEGER, @@ -171,7 +172,7 @@ SqlTiddlerDatabase.prototype.createTables = function() { FOREIGN KEY (tiddler_id) REFERENCES tiddlers(tiddler_id) ON UPDATE CASCADE ON DELETE CASCADE, UNIQUE (tiddler_id, field_name) ) - `,` + `, ` -- ACL table (using bag/recipe ids directly) CREATE TABLE IF NOT EXISTS acl ( acl_id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -182,137 +183,137 @@ SqlTiddlerDatabase.prototype.createTables = function() { FOREIGN KEY (role_id) REFERENCES roles(role_id), FOREIGN KEY (permission_id) REFERENCES permissions(permission_id) ) - `,` + `, ` -- Indexes for performance (we can add more as needed based on query patterns) CREATE INDEX IF NOT EXISTS idx_tiddlers_bag_id ON tiddlers(bag_id) - `,` + `, ` CREATE INDEX IF NOT EXISTS idx_fields_tiddler_id ON fields(tiddler_id) - `,` + `, ` CREATE INDEX IF NOT EXISTS idx_recipe_bags_recipe_id ON recipe_bags(recipe_id) - `,` + `, ` CREATE INDEX IF NOT EXISTS idx_acl_entity_id ON acl(entity_name) `]); -}; + } -SqlTiddlerDatabase.prototype.listBags = function() { - const rows = this.engine.runStatementGetAll(` + async listBags() { + const rows = await this.engine.runStatementGetAll(` SELECT bag_name, bag_id, accesscontrol, description FROM bags ORDER BY bag_name `); - return rows; -}; - -/* -Create or update a bag -Returns the bag_id of the bag -*/ -SqlTiddlerDatabase.prototype.createBag = function(bag_name,description,accesscontrol) { - accesscontrol = accesscontrol || ""; - // Run the queries - var bag = this.engine.runStatement(` + return rows; + } + + /* + Create or update a bag + Returns the bag_id of the bag + */ + async createBag(bag_name, description, accesscontrol) { + accesscontrol = accesscontrol || ""; + // Run the queries + 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(` + `, { + $bag_name: bag_name + }); + const updateBags = await this.engine.runStatement(` UPDATE bags SET accesscontrol = $accesscontrol, description = $description WHERE bag_name = $bag_name - `,{ - $bag_name: bag_name, - $accesscontrol: accesscontrol, - $description: description - }); - return updateBags.lastInsertRowid; -}; - -/* -Returns array of {recipe_name:,recipe_id:,description:,bag_names: []} -*/ -SqlTiddlerDatabase.prototype.listRecipes = function() { - const rows = this.engine.runStatementGetAll(` + `, { + $bag_name: bag_name, + $accesscontrol: accesscontrol, + $description: description + }); + return updateBags.lastInsertRowid; + } + + /* + Returns array of {recipe_name:,recipe_id:,description:,bag_names: []} + */ + async listRecipes() { + const rows = await this.engine.runStatementGetAll(` SELECT r.recipe_name, r.recipe_id, r.description, r.owner_id, b.bag_name, rb.position FROM recipes AS r JOIN recipe_bags AS rb ON rb.recipe_id = r.recipe_id JOIN bags AS b ON rb.bag_id = b.bag_id ORDER BY r.recipe_name, rb.position `); - const results = []; - let currentRecipeName = null, currentRecipeIndex = -1; - for(const row of rows) { - if(row.recipe_name !== currentRecipeName) { - currentRecipeName = row.recipe_name; - currentRecipeIndex += 1; - results.push({ - recipe_name: row.recipe_name, - recipe_id: row.recipe_id, - description: row.description, - owner_id: row.owner_id, - bag_names: [] - }); - } - results[currentRecipeIndex].bag_names.push(row.bag_name); - } - return results; -}; - -/* -Create or update a recipe -Returns the recipe_id of the recipe -*/ -SqlTiddlerDatabase.prototype.createRecipe = function(recipe_name,bag_names,description) { - // Run the queries - this.engine.runStatement(` + const results = []; + let currentRecipeName = null, currentRecipeIndex = -1; + for (const row of rows) { + if (row.recipe_name !== currentRecipeName) { + currentRecipeName = row.recipe_name; + currentRecipeIndex += 1; + results.push({ + recipe_name: row.recipe_name, + recipe_id: row.recipe_id, + description: row.description, + owner_id: row.owner_id, + bag_names: [] + }); + } + results[currentRecipeIndex].bag_names.push(row.bag_name); + } + return results; + } + + /* + Create or update a recipe + Returns the recipe_id of the recipe + */ + async createRecipe(recipe_name, bag_names, description) { + // Run the queries + await this.engine.runStatement(` -- Delete existing recipe_bags entries for this recipe DELETE FROM recipe_bags WHERE recipe_id = (SELECT recipe_id FROM recipes WHERE recipe_name = $recipe_name) - `,{ - $recipe_name: recipe_name - }); - const updateRecipes = this.engine.runStatement(` + `, { + $recipe_name: recipe_name + }); + const updateRecipes = await this.engine.runStatement(` -- Create the entry in the recipes table if required INSERT OR REPLACE INTO recipes (recipe_name, description) VALUES ($recipe_name, $description) - `,{ - $recipe_name: recipe_name, - $description: description - }); - this.engine.runStatement(` + `, { + $recipe_name: recipe_name, + $description: description + }); + await this.engine.runStatement(` INSERT INTO recipe_bags (recipe_id, bag_id, position) SELECT r.recipe_id, b.bag_id, j.key as position FROM recipes r JOIN bags b INNER JOIN json_each($bag_names) AS j ON j.value = b.bag_name WHERE r.recipe_name = $recipe_name - `,{ - $recipe_name: recipe_name, - $bag_names: JSON.stringify(bag_names) - }); - - return updateRecipes.lastInsertRowid; -}; - -/* -Assign a recipe to a user -*/ -SqlTiddlerDatabase.prototype.assignRecipeToUser = function(recipe_name,user_id) { - this.engine.runStatement(` + `, { + $recipe_name: recipe_name, + $bag_names: JSON.stringify(bag_names) + }); + + return updateRecipes.lastInsertRowid; + } + + /* + Assign a recipe to a user + */ + async assignRecipeToUser(recipe_name, user_id) { + await this.engine.runStatement(` UPDATE recipes SET owner_id = $user_id WHERE recipe_name = $recipe_name - `,{ - $recipe_name: recipe_name, - $user_id: user_id - }); -}; - -/* -Returns {tiddler_id:} -*/ -SqlTiddlerDatabase.prototype.saveBagTiddler = function(tiddlerFields,bag_name,attachment_blob) { - attachment_blob = attachment_blob || null; - // Update the tiddlers table - var info = this.engine.runStatement(` + `, { + $recipe_name: recipe_name, + $user_id: user_id + }); + } + + /* + Returns {tiddler_id:} + */ + async saveBagTiddler(tiddlerFields, bag_name, attachment_blob) { + attachment_blob = attachment_blob || null; + // Update the tiddlers table + var info = await this.engine.runStatement(` INSERT OR REPLACE INTO tiddlers (bag_id, title, is_deleted, attachment_blob) VALUES ( (SELECT bag_id FROM bags WHERE bag_name = $bag_name), @@ -320,13 +321,13 @@ SqlTiddlerDatabase.prototype.saveBagTiddler = function(tiddlerFields,bag_name,at FALSE, $attachment_blob ) - `,{ - $title: tiddlerFields.title, - $attachment_blob: attachment_blob, - $bag_name: bag_name - }); - // Update the fields table - this.engine.runStatement(` + `, { + $title: tiddlerFields.title, + $attachment_blob: attachment_blob, + $bag_name: bag_name + }); + // Update the fields table + await this.engine.runStatement(` INSERT OR REPLACE INTO fields (tiddler_id, field_name, field_value) SELECT t.tiddler_id, @@ -342,22 +343,22 @@ SqlTiddlerDatabase.prototype.saveBagTiddler = function(tiddlerFields,bag_name,at ) AND title = $title ) AS t JOIN json_each($field_values) AS json_each - `,{ - $title: tiddlerFields.title, - $bag_name: bag_name, - $field_values: JSON.stringify(Object.assign({},tiddlerFields,{title: undefined})) - }); - return { - tiddler_id: info.lastInsertRowid - } -}; - -/* -Returns {tiddler_id:,bag_name:} or null if the recipe is empty -*/ -SqlTiddlerDatabase.prototype.saveRecipeTiddler = function(tiddlerFields,recipe_name,attachment_blob) { - // Find the topmost bag in the recipe - var row = this.engine.runStatementGet(` + `, { + $title: tiddlerFields.title, + $bag_name: bag_name, + $field_values: JSON.stringify(Object.assign({}, tiddlerFields, { title: undefined })) + }); + return { + tiddler_id: info.lastInsertRowid + }; + } + + /* + Returns {tiddler_id:,bag_name:} or null if the recipe is empty + */ + async saveRecipeTiddler(tiddlerFields, recipe_name, attachment_blob) { + // Find the topmost bag in the recipe + var row = await this.engine.runStatementGet(` SELECT b.bag_name FROM bags AS b JOIN ( @@ -372,26 +373,26 @@ SqlTiddlerDatabase.prototype.saveRecipeTiddler = function(tiddlerFields,recipe_n LIMIT 1 ) AS selected_bag ON b.bag_id = selected_bag.bag_id - `,{ - $recipe_name: recipe_name - }); - if(!row) { - return null; - } - // Save the tiddler to the topmost bag - var info = this.saveBagTiddler(tiddlerFields,row.bag_name,attachment_blob); - return { - tiddler_id: info.tiddler_id, - bag_name: row.bag_name - }; -}; - -/* -Returns {tiddler_id:} of the delete marker -*/ -SqlTiddlerDatabase.prototype.deleteTiddler = function(title,bag_name) { - // Delete the fields of this tiddler - this.engine.runStatement(` + `, { + $recipe_name: recipe_name + }); + if (!row) { + return null; + } + // Save the tiddler to the topmost bag + var info = await this.saveBagTiddler(tiddlerFields, row.bag_name, attachment_blob); + return { + tiddler_id: info.tiddler_id, + bag_name: row.bag_name + }; + } + + /* + Returns {tiddler_id:} of the delete marker + */ + async deleteTiddler(title, bag_name) { + // Delete the fields of this tiddler + await this.engine.runStatement(` DELETE FROM fields WHERE tiddler_id IN ( SELECT t.tiddler_id @@ -399,12 +400,12 @@ SqlTiddlerDatabase.prototype.deleteTiddler = function(title,bag_name) { INNER JOIN bags AS b ON t.bag_id = b.bag_id WHERE b.bag_name = $bag_name AND t.title = $title ) - `,{ - $title: title, - $bag_name: bag_name - }); - // Mark the tiddler itself as deleted - const rowDeleteMarker = this.engine.runStatement(` + `, { + $title: title, + $bag_name: bag_name + }); + // Mark the tiddler itself as deleted + const rowDeleteMarker = await this.engine.runStatement(` INSERT OR REPLACE INTO tiddlers (bag_id, title, is_deleted, attachment_blob) VALUES ( (SELECT bag_id FROM bags WHERE bag_name = $bag_name), @@ -412,55 +413,55 @@ SqlTiddlerDatabase.prototype.deleteTiddler = function(title,bag_name) { TRUE, NULL ) - `,{ - $title: title, - $bag_name: bag_name - }); - return {tiddler_id: rowDeleteMarker.lastInsertRowid}; -}; - -/* -returns {tiddler_id:,tiddler:,attachment_blob:} -*/ -SqlTiddlerDatabase.prototype.getBagTiddler = function(title,bag_name) { - const rowTiddler = this.engine.runStatementGet(` + `, { + $title: title, + $bag_name: bag_name + }); + return { tiddler_id: rowDeleteMarker.lastInsertRowid }; + } + + /* + returns {tiddler_id:,tiddler:,attachment_blob:} + */ + async getBagTiddler(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 WHERE t.title = $title AND b.bag_name = $bag_name AND t.is_deleted = FALSE - `,{ - $title: title, - $bag_name: bag_name - }); - if(!rowTiddler) { - return null; - } - const rows = this.engine.runStatementGetAll(` + `, { + $title: title, + $bag_name: bag_name + }); + if (!rowTiddler) { + return null; + } + const rows = await this.engine.runStatementGetAll(` SELECT field_name, field_value, tiddler_id FROM fields WHERE tiddler_id = $tiddler_id - `,{ - $tiddler_id: rowTiddler.tiddler_id - }); - if(rows.length === 0) { - return null; - } else { - return { - tiddler_id: rows[0].tiddler_id, - attachment_blob: rowTiddler.attachment_blob, - tiddler: rows.reduce((accumulator,value) => { - accumulator[value["field_name"]] = value.field_value; - return accumulator; - },{title: title}) - }; - } -}; + `, { + $tiddler_id: rowTiddler.tiddler_id + }); + if (rows.length === 0) { + return null; + } else { + return { + tiddler_id: rows[0].tiddler_id, + attachment_blob: rowTiddler.attachment_blob, + tiddler: rows.reduce((accumulator, value) => { + accumulator[value["field_name"]] = value.field_value; + return accumulator; + }, { title: title }) + }; + } + } -/* -Returns {bag_name:, tiddler: {fields}, tiddler_id:, attachment_blob:} -*/ -SqlTiddlerDatabase.prototype.getRecipeTiddler = function(title,recipe_name) { - const rowTiddlerId = this.engine.runStatementGet(` + /* + Returns {bag_name:, tiddler: {fields}, tiddler_id:, attachment_blob:} + */ + async getRecipeTiddler(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 @@ -471,74 +472,74 @@ SqlTiddlerDatabase.prototype.getRecipeTiddler = function(title,recipe_name) { AND t.is_deleted = FALSE ORDER BY rb.position DESC LIMIT 1 - `,{ - $title: title, - $recipe_name: recipe_name - }); - if(!rowTiddlerId) { - return null; - } - // Get the fields - const rows = this.engine.runStatementGetAll(` + `, { + $title: title, + $recipe_name: recipe_name + }); + if (!rowTiddlerId) { + return null; + } + // Get the fields + const rows = await this.engine.runStatementGetAll(` SELECT field_name, field_value FROM fields WHERE tiddler_id = $tiddler_id - `,{ - $tiddler_id: rowTiddlerId.tiddler_id - }); - return { - bag_name: rowTiddlerId.bag_name, - tiddler_id: rowTiddlerId.tiddler_id, - attachment_blob: rowTiddlerId.attachment_blob, - tiddler: rows.reduce((accumulator,value) => { - accumulator[value["field_name"]] = value.field_value; - return accumulator; - },{title: title}) - }; -}; - -/* -Checks if a user has permission to access a recipe -*/ -SqlTiddlerDatabase.prototype.hasRecipePermission = function(userId, recipeName, permissionName) { - try { - // check if the user is the owner of the entity - const recipe = this.engine.runStatementGet(` + `, { + $tiddler_id: rowTiddlerId.tiddler_id + }); + return { + bag_name: rowTiddlerId.bag_name, + tiddler_id: rowTiddlerId.tiddler_id, + attachment_blob: rowTiddlerId.attachment_blob, + tiddler: rows.reduce((accumulator, value) => { + accumulator[value["field_name"]] = value.field_value; + return accumulator; + }, { title: title }) + }; + } + + /* + Checks if a user has permission to access a recipe + */ + async hasRecipePermission(userId, recipeName, permissionName) { + try { + // check if the user is the owner of the entity + const recipe = await this.engine.runStatementGet(` SELECT owner_id FROM recipes WHERE recipe_name = $recipe_name `, { - $recipe_name: recipeName - }); + $recipe_name: recipeName + }); + + if (recipe && !!recipe.owner_id && recipe.owner_id === userId) { + return true; + } else { + var permission = this.checkACLPermission(userId, "recipe", recipeName, permissionName, recipe && recipe.owner_id); + return permission; + } + + } catch (error) { + console.error(error); + return false; + } + } - if(!!recipe?.owner_id && recipe?.owner_id === userId) { - return true; - } else { - var permission = this.checkACLPermission(userId, "recipe", recipeName, permissionName, recipe?.owner_id) - return permission; + /* + Checks if a user has permission to access a bag + */ + async hasBagPermission(userId, bagName, permissionName) { + return await this.checkACLPermission(userId, "bag", bagName, permissionName); } - - } catch (error) { - console.error(error) - return false - } -}; - -/* -Checks if a user has permission to access a bag -*/ -SqlTiddlerDatabase.prototype.hasBagPermission = function(userId, bagName, permissionName) { - return this.checkACLPermission(userId, "bag", bagName, permissionName) -}; - -SqlTiddlerDatabase.prototype.getACLByName = function(entityType, entityName, fetchAll) { - const entityInfo = this.entityTypeToTableMap[entityType]; - if (!entityInfo) { - throw new Error("Invalid entity type: " + entityType); - } - // First, check if there's an ACL record for the entity and get the permission_id - var checkACLExistsQuery = ` + async getACLByName(entityType, entityName, fetchAll) { + const entityInfo = this.entityTypeToTableMap[entityType]; + if (!entityInfo) { + throw new Error("Invalid entity type: " + entityType); + } + + // First, check if there's an ACL record for the entity and get the permission_id + var checkACLExistsQuery = ` SELECT acl.*, permissions.permission_name FROM acl LEFT JOIN permissions ON acl.permission_id = permissions.permission_id @@ -546,35 +547,35 @@ SqlTiddlerDatabase.prototype.getACLByName = function(entityType, entityName, fet AND acl.entity_name = $entity_name `; - if (!fetchAll) { - checkACLExistsQuery += ' LIMIT 1' - } - - const aclRecord = this.engine[fetchAll ? 'runStatementGetAll' : 'runStatementGet'](checkACLExistsQuery, { - $entity_type: entityType, - $entity_name: entityName - }); + if (!fetchAll) { + checkACLExistsQuery += " LIMIT 1"; + } - return aclRecord; -} + const aclRecord = await this.engine[fetchAll ? "runStatementGetAll" : "runStatementGet"](checkACLExistsQuery, { + $entity_type: entityType, + $entity_name: entityName + }); -SqlTiddlerDatabase.prototype.checkACLPermission = function(userId, entityType, entityName, permissionName, ownerId) { - try { - // if the entityName starts with "$:/", we'll assume its a system bag/recipe, then grant the user permission - if(entityName.startsWith("$:/")) { - return true; + return aclRecord; } - const aclRecords = this.getACLByName(entityType, entityName, true); - const aclRecord = aclRecords.find(record => record.permission_name === permissionName); + async checkACLPermission(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; + } - // If no ACL record exists, return true for hasPermission - if ((!aclRecord && !ownerId && aclRecords.length === 0) || ((!!aclRecord && !!ownerId) && ownerId === userId)) { - return 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 + if ((!aclRecord && !ownerId && aclRecords.length === 0) || ((!!aclRecord && !!ownerId) && ownerId === userId)) { + return true; + } - // If ACL record exists, check for user permission using the retrieved permission_id - const checkPermissionQuery = ` + // If ACL record exists, check for user permission using the retrieved permission_id + const checkPermissionQuery = ` SELECT * FROM users u JOIN user_roles ur ON u.user_id = ur.user_id @@ -587,58 +588,58 @@ SqlTiddlerDatabase.prototype.checkACLPermission = function(userId, entityType, e LIMIT 1 `; - const result = this.engine.runStatementGet(checkPermissionQuery, { - $user_id: userId, - $entity_type: entityType, - $entity_name: entityName, - $permission_id: aclRecord?.permission_id - }); - - let hasPermission = result !== undefined; - - return hasPermission; - - } catch (error) { - console.error(error); - return false - } -}; + const result = await this.engine.runStatementGet(checkPermissionQuery, { + $user_id: userId, + $entity_type: entityType, + $entity_name: entityName, + $permission_id: aclRecord && aclRecord.permission_id + }); -/** - * Returns the ACL records for an entity (bag or recipe) - */ -SqlTiddlerDatabase.prototype.getEntityAclRecords = function(entityName) { - const checkACLExistsQuery = ` + let hasPermission = result !== undefined; + + return hasPermission; + + } catch (error) { + console.error(error); + return false; + } + } + + /** + * Returns the ACL records for an entity (bag or recipe) + */ + async getEntityAclRecords(entityName) { + const checkACLExistsQuery = ` SELECT * FROM acl WHERE entity_name = $entity_name `; - const aclRecords = this.engine.runStatementGetAll(checkACLExistsQuery, { - $entity_name: entityName - }); - - return aclRecords -} - -/* -Get the entity by name -*/ -SqlTiddlerDatabase.prototype.getEntityByName = function(entityType, entityName) { - const entityInfo = this.entityTypeToTableMap[entityType]; - if (entityInfo) { - return this.engine.runStatementGet(`SELECT * FROM ${entityInfo.table} WHERE ${entityInfo.column} = $entity_name`, { - $entity_name: entityName - }); - } - return null; -} - -/* -Get the titles of the tiddlers in a bag. Returns an empty array for bags that do not exist -*/ -SqlTiddlerDatabase.prototype.getBagTiddlers = function(bag_name) { - const rows = this.engine.runStatementGetAll(` + const aclRecords = await this.engine.runStatementGetAll(checkACLExistsQuery, { + $entity_name: entityName + }); + + return aclRecords; + } + + /* + Get the entity by name + */ + async getEntityByName(entityType, entityName) { + const entityInfo = this.entityTypeToTableMap[entityType]; + if (entityInfo) { + return await this.engine.runStatementGet(`SELECT * FROM ${entityInfo.table} WHERE ${entityInfo.column} = $entity_name`, { + $entity_name: entityName + }); + } + return null; + } + + /* + Get the titles of the tiddlers in a bag. Returns an empty array for bags that do not exist + */ + async getBagTiddlers(bag_name) { + const rows = await this.engine.runStatementGetAll(` SELECT DISTINCT title, tiddler_id FROM tiddlers WHERE bag_id IN ( @@ -648,17 +649,17 @@ SqlTiddlerDatabase.prototype.getBagTiddlers = function(bag_name) { ) AND tiddlers.is_deleted = FALSE ORDER BY title ASC - `,{ - $bag_name: bag_name - }); - return rows; -}; - -/* -Get the tiddler_id of the newest tiddler in a bag. Returns null for bags that do not exist -*/ -SqlTiddlerDatabase.prototype.getBagLastTiddlerId = function(bag_name) { - const row = this.engine.runStatementGet(` + `, { + $bag_name: bag_name + }); + return rows; + } + + /* + Get the tiddler_id of the newest tiddler in a bag. Returns null for bags that do not exist + */ + async getBagLastTiddlerId(bag_name) { + const row = await this.engine.runStatementGet(` SELECT tiddler_id FROM tiddlers WHERE bag_id IN ( @@ -668,51 +669,51 @@ SqlTiddlerDatabase.prototype.getBagLastTiddlerId = function(bag_name) { ) ORDER BY tiddler_id DESC LIMIT 1 - `,{ - $bag_name: bag_name - }); - if(row) { - return row.tiddler_id; - } else { - return null; - } -}; - -/* -Get the metadata of the tiddlers in a recipe as an array [{title:,tiddler_id:,bag_name:,is_deleted:}], -sorted in ascending order of tiddler_id. - -Options include: - -limit: optional maximum number of results to return -last_known_tiddler_id: tiddler_id of the last known update. Only returns tiddlers that have been created, modified or deleted since -include_deleted: boolean, defaults to false + `, { + $bag_name: bag_name + }); + if (row) { + return row.tiddler_id; + } else { + return null; + } + } -Returns null for recipes that do not exist -*/ -SqlTiddlerDatabase.prototype.getRecipeTiddlers = function(recipe_name,options) { - options = options || {}; - // Get the recipe ID - const rowsCheckRecipe = this.engine.runStatementGet(` + /* + Get the metadata of the tiddlers in a recipe as an array [{title:,tiddler_id:,bag_name:,is_deleted:}], + sorted in ascending order of tiddler_id. + + Options include: + + limit: optional maximum number of results to return + last_known_tiddler_id: tiddler_id of the last known update. Only returns tiddlers that have been created, modified or deleted since + include_deleted: boolean, defaults to false + + Returns null for recipes that do not exist + */ + async getRecipeTiddlers(recipe_name, options) { + options = options || {}; + // Get the recipe ID + const rowsCheckRecipe = await this.engine.runStatementGet(` SELECT recipe_id FROM recipes WHERE recipes.recipe_name = $recipe_name - `,{ - $recipe_name: recipe_name - }); - if(!rowsCheckRecipe) { - return null; - } - const recipe_id = rowsCheckRecipe.recipe_id; - // Compose the query to get the tiddlers - const params = { - $recipe_id: recipe_id - } - if(options.limit) { - params.$limit = options.limit.toString(); - } - if(options.last_known_tiddler_id) { - params.$last_known_tiddler_id = options.last_known_tiddler_id; - } - const rows = this.engine.runStatementGetAll(` + `, { + $recipe_name: recipe_name + }); + if (!rowsCheckRecipe) { + return null; + } + const recipe_id = rowsCheckRecipe.recipe_id; + // Compose the query to get the tiddlers + const params = { + $recipe_id: recipe_id + }; + if (options.limit) { + params.$limit = options.limit.toString(); + } + if (options.last_known_tiddler_id) { + params.$last_known_tiddler_id = options.last_known_tiddler_id; + } + const rows = 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 @@ -726,15 +727,15 @@ SqlTiddlerDatabase.prototype.getRecipeTiddlers = function(recipe_name,options) { ORDER BY t.title, tiddler_id DESC ${options.limit ? "LIMIT $limit" : ""} ) - `,params); - return rows; -}; - -/* -Get the tiddler_id of the newest tiddler in a recipe. Returns null for recipes that do not exist -*/ -SqlTiddlerDatabase.prototype.getRecipeLastTiddlerId = function(recipe_name) { - const row = this.engine.runStatementGet(` + `, params); + return rows; + } + + /* + Get the tiddler_id of the newest tiddler in a recipe. Returns null for recipes that do not exist + */ + async getRecipeLastTiddlerId(recipe_name) { + 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 @@ -744,19 +745,19 @@ SqlTiddlerDatabase.prototype.getRecipeLastTiddlerId = function(recipe_name) { GROUP BY t.title ORDER BY t.tiddler_id DESC LIMIT 1 - `,{ - $recipe_name: recipe_name - }); - if(row) { - return row.tiddler_id; - } else { - return null; - } -}; + `, { + $recipe_name: recipe_name + }); + if (row) { + return row.tiddler_id; + } else { + return null; + } + } -SqlTiddlerDatabase.prototype.deleteAllTiddlersInBag = function(bag_name) { - // Delete the fields - this.engine.runStatement(` + async deleteAllTiddlersInBag(bag_name) { + // Delete the fields + await this.engine.runStatement(` DELETE FROM fields WHERE tiddler_id IN ( SELECT tiddler_id @@ -764,25 +765,25 @@ SqlTiddlerDatabase.prototype.deleteAllTiddlersInBag = function(bag_name) { WHERE bag_id = (SELECT bag_id FROM bags WHERE bag_name = $bag_name) AND is_deleted = FALSE ) - `,{ - $bag_name: bag_name - }); - // Mark the tiddlers as deleted - this.engine.runStatement(` + `, { + $bag_name: bag_name + }); + // Mark the tiddlers as deleted + await this.engine.runStatement(` UPDATE tiddlers SET is_deleted = TRUE WHERE bag_id = (SELECT bag_id FROM bags WHERE bag_name = $bag_name) AND is_deleted = FALSE - `,{ - $bag_name: bag_name - }); -}; - -/* -Get the names of the bags in a recipe. Returns an empty array for recipes that do not exist -*/ -SqlTiddlerDatabase.prototype.getRecipeBags = function(recipe_name) { - const rows = this.engine.runStatementGetAll(` + `, { + $bag_name: bag_name + }); + } + + /* + Get the names of the bags in a recipe. Returns an empty array for recipes that do not exist + */ + async getRecipeBags(recipe_name) { + const rows = await this.engine.runStatementGetAll(` SELECT bags.bag_name FROM bags JOIN ( @@ -793,33 +794,33 @@ SqlTiddlerDatabase.prototype.getRecipeBags = function(recipe_name) { ORDER BY rb.position ) AS bag_priority ON bags.bag_id = bag_priority.bag_id ORDER BY position - `,{ - $recipe_name: recipe_name - }); - return rows.map(value => value.bag_name); -}; - -/* -Get the attachment value of a bag, if any exist -*/ -SqlTiddlerDatabase.prototype.getBagTiddlerAttachmentBlob = function(title,bag_name) { - const row = this.engine.runStatementGet(` + `, { + $recipe_name: recipe_name + }); + return rows.map(value => value.bag_name); + } + + /* + Get the attachment value of a bag, if any exist + */ + async getBagTiddlerAttachmentBlob(title, bag_name) { + const row = await this.engine.runStatementGet(` SELECT t.attachment_blob FROM bags AS b INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id WHERE t.title = $title AND b.bag_name = $bag_name AND t.is_deleted = FALSE `, { - $title: title, - $bag_name: bag_name - }); - return row ? row.attachment_blob : null; -}; - -/* -Get the attachment value of a recipe, if any exist -*/ -SqlTiddlerDatabase.prototype.getRecipeTiddlerAttachmentBlob = function(title,recipe_name) { - const row = this.engine.runStatementGet(` + $title: title, + $bag_name: bag_name + }); + return row ? row.attachment_blob : null; + } + + /* + Get the attachment value of a recipe, if any exist + */ + async getRecipeTiddlerAttachmentBlob(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 @@ -829,651 +830,668 @@ SqlTiddlerDatabase.prototype.getRecipeTiddlerAttachmentBlob = function(title,rec ORDER BY rb.position DESC LIMIT 1 `, { - $title: title, - $recipe_name: recipe_name - }); - return row ? row.attachment_blob : null; -}; + $title: title, + $recipe_name: recipe_name + }); + return row ? row.attachment_blob : null; + } -// User CRUD operations -SqlTiddlerDatabase.prototype.createUser = function(username, email, password) { - const result = this.engine.runStatement(` + // User CRUD operations + async createUser(username, email, password) { + const result = await this.engine.runStatement(` INSERT INTO users (username, email, password) VALUES ($username, $email, $password) `, { - $username: username, - $email: email, - $password: password - }); - return result.lastInsertRowid; -}; + $username: username, + $email: email, + $password: password + }); + return result.lastInsertRowid; + } -SqlTiddlerDatabase.prototype.getUser = function(userId) { - return this.engine.runStatementGet(` + async getUser(userId) { + return await this.engine.runStatementGet(` SELECT * FROM users WHERE user_id = $userId `, { - $userId: userId - }); -}; + $userId: userId + }); + } -SqlTiddlerDatabase.prototype.getUserByUsername = function(username) { - return this.engine.runStatementGet(` + async getUserByUsername(username) { + return await this.engine.runStatementGet(` SELECT * FROM users WHERE username = $username `, { - $username: username - }); -}; + $username: username + }); + } -SqlTiddlerDatabase.prototype.getUserByEmail = function(email) { - return this.engine.runStatementGet(` + async getUserByEmail(email) { + return await this.engine.runStatementGet(` SELECT * FROM users WHERE email = $email `, { - $email: email - }); -}; + $email: email + }); + } -SqlTiddlerDatabase.prototype.listUsersByRoleId = function(roleId) { - return this.engine.runStatementGetAll(` + async listUsersByRoleId(roleId) { + return await this.engine.runStatementGetAll(` SELECT u.* FROM users u JOIN user_roles ur ON u.user_id = ur.user_id WHERE ur.role_id = $roleId ORDER BY u.username `, { - $roleId: roleId - }); -}; + $roleId: roleId + }); + } -SqlTiddlerDatabase.prototype.updateUser = function (userId, username, email, roleId) { - const existingUser = this.engine.runStatement(` + async updateUser(userId, username, email, roleId) { + const existingUser = await this.engine.runStatementGet(` SELECT user_id FROM users WHERE email = $email AND user_id != $userId `, { - $email: email, - $userId: userId - }); - - if (existingUser.length > 0) { - return { - success: false, - message: "Email address already in use by another user." - }; - } + $email: email, + $userId: userId + }); + + if (existingUser.length > 0) { + return { + success: false, + message: "Email address already in use by another user." + }; + } - try { - this.engine.transaction(() => { - // Update user information - this.engine.runStatement(` + try { + await this.engine.transaction(async () => { + // Update user information + await this.engine.runStatement(` UPDATE users SET username = $username, email = $email WHERE user_id = $userId `, { - $userId: userId, - $username: username, - $email: email - }); - - if (roleId) { - // Remove all existing roles for the user - this.engine.runStatement(` + $userId: userId, + $username: username, + $email: email + }); + + if (roleId) { + // Remove all existing roles for the user + await this.engine.runStatement(` DELETE FROM user_roles WHERE user_id = $userId `, { - $userId: userId - }); + $userId: userId + }); - // Add the new role - this.engine.runStatement(` + // Add the new role + await this.engine.runStatement(` INSERT INTO user_roles (user_id, role_id) VALUES ($userId, $roleId) `, { - $userId: userId, - $roleId: roleId + $userId: userId, + $roleId: roleId + }); + } }); + + return { + success: true, + message: "User profile and role updated successfully." + }; + } catch (error) { + return { + success: false, + message: "Failed to update user profile: " + error.message + }; } - }); - - return { - success: true, - message: "User profile and role updated successfully." - }; - } catch (error) { - return { - success: false, - message: "Failed to update user profile: " + error.message - }; - } -}; + } -SqlTiddlerDatabase.prototype.updateUserPassword = function (userId, newHash) { - try { - this.engine.runStatement(` + async updateUserPassword(userId, newHash) { + try { + await this.engine.runStatement(` UPDATE users SET password = $newHash WHERE user_id = $userId `, { - $userId: userId, - $newHash: newHash, - }); - - return { - success: true, - message: "Password updated successfully." - }; - } catch (error) { - return { - success: false, - message: "Failed to update password: " + error.message - }; - } -}; + $userId: userId, + $newHash: newHash, + }); -SqlTiddlerDatabase.prototype.deleteUser = function(userId) { - this.engine.runStatement(` + return { + success: true, + message: "Password updated successfully." + }; + } catch (error) { + return { + success: false, + message: "Failed to update password: " + error.message + }; + } + } + + async deleteUser(userId) { + await this.engine.runStatement(` DELETE FROM users WHERE user_id = $userId `, { - $userId: userId - }); -}; + $userId: userId + }); + } -SqlTiddlerDatabase.prototype.listUsers = function() { - return this.engine.runStatementGetAll(` + async listUsers() { + return await this.engine.runStatementGetAll(` SELECT * FROM users ORDER BY username `); -}; + } -SqlTiddlerDatabase.prototype.createOrUpdateUserSession = function(userId, sessionId) { - const currentTimestamp = new Date().toISOString(); + async createOrUpdateUserSession(userId, sessionId) { + const currentTimestamp = new Date().toISOString(); - // First, try to update an existing session - const updateResult = this.engine.runStatement(` + // First, try to update an existing session + const updateResult = await this.engine.runStatement(` UPDATE sessions SET session_id = $sessionId, last_accessed = $timestamp WHERE user_id = $userId `, { - $userId: userId, - $sessionId: sessionId, - $timestamp: currentTimestamp - }); + $userId: userId, + $sessionId: sessionId, + $timestamp: currentTimestamp + }); - // If no existing session was updated, create a new one - if (updateResult.changes === 0) { - this.engine.runStatement(` + // If no existing session was updated, create a new one + if (updateResult.changes === 0) { + await this.engine.runStatement(` INSERT INTO sessions (user_id, session_id, created_at, last_accessed) VALUES ($userId, $sessionId, $timestamp, $timestamp) `, { $userId: userId, $sessionId: sessionId, $timestamp: currentTimestamp - }); - } + }); + } - return sessionId; -}; + return sessionId; + } -SqlTiddlerDatabase.prototype.createUserSession = function(userId, sessionId) { - const currentTimestamp = new Date().toISOString(); - this.engine.runStatement(` + async createUserSession(userId, sessionId) { + const currentTimestamp = new Date().toISOString(); + await this.engine.runStatement(` INSERT INTO sessions (user_id, session_id, created_at, last_accessed) VALUES ($userId, $sessionId, $timestamp, $timestamp) `, { - $userId: userId, - $sessionId: sessionId, - $timestamp: currentTimestamp - }); + $userId: userId, + $sessionId: sessionId, + $timestamp: currentTimestamp + }); - return sessionId; -}; + return sessionId; + } -SqlTiddlerDatabase.prototype.findUserBySessionId = function(sessionId) { - // First, get the user_id from the sessions table - const sessionResult = this.engine.runStatementGet(` + /** + * @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} + */ + async findUserBySessionId(sessionId) { + // First, get the user_id from the sessions table + const sessionResult = await this.engine.runStatementGet(` SELECT user_id, last_accessed FROM sessions WHERE session_id = $sessionId `, { - $sessionId: sessionId - }); + $sessionId: sessionId + }); - if (!sessionResult) { - return null; // Session not found - } + if (!sessionResult) { + return null; // Session not found + } - const lastAccessed = new Date(sessionResult.last_accessed); - const expirationTime = 24 * 60 * 60 * 1000; // 24 hours in milliseconds - if (new Date() - lastAccessed > expirationTime) { - // Session has expired - this.deleteSession(sessionId); - return null; - } + const lastAccessed = new Date(sessionResult.last_accessed); + const expirationTime = 24 * 60 * 60 * 1000; // 24 hours in milliseconds + if (+new Date() - +lastAccessed > expirationTime) { + // Session has expired + await this.deleteSession(sessionId); + return null; + } - // Update the last_accessed timestamp - const currentTimestamp = new Date().toISOString(); - this.engine.runStatement(` + // Update the last_accessed timestamp + const currentTimestamp = new Date().toISOString(); + await this.engine.runStatement(` UPDATE sessions SET last_accessed = $timestamp WHERE session_id = $sessionId `, { - $sessionId: sessionId, - $timestamp: currentTimestamp - }); + $sessionId: sessionId, + $timestamp: currentTimestamp + }); - const userResult = this.engine.runStatementGet(` + /** @type {any} */ + const userResult = await this.engine.runStatementGet(` SELECT * FROM users WHERE user_id = $userId `, { - $userId: sessionResult.user_id - }); + $userId: sessionResult.user_id + }); - if (!userResult) { - return null; - } + if (!userResult) { + return null; + } - return userResult; -}; + /** @type {User} */ + return userResult; + } -SqlTiddlerDatabase.prototype.deleteSession = function(sessionId) { - this.engine.runStatement(` + async deleteSession(sessionId) { + await this.engine.runStatement(` DELETE FROM sessions WHERE session_id = $sessionId `, { - $sessionId: sessionId - }); -}; + $sessionId: sessionId + }); + } -SqlTiddlerDatabase.prototype.deleteUserSessions = function(userId) { - this.engine.runStatement(` + async deleteUserSessions(userId) { + await this.engine.runStatement(` DELETE FROM sessions WHERE user_id = $userId `, { - $userId: userId - }); -}; + $userId: userId + }); + } -// Set the user as an admin -SqlTiddlerDatabase.prototype.setUserAdmin = function(userId) { - var admin = this.getRoleByName("ADMIN"); - if(admin) { - this.addRoleToUser(userId, admin.role_id); - } -}; + // Set the user as an admin + async setUserAdmin(userId) { + var admin = await this.getRoleByName("ADMIN"); + if (admin) { + await this.addRoleToUser(userId, admin.role_id); + } + } -// Group CRUD operations -SqlTiddlerDatabase.prototype.createGroup = function(groupName, description) { - const result = this.engine.runStatement(` + // Group CRUD operations + async createGroup(groupName, description) { + const result = await this.engine.runStatement(` INSERT INTO groups (group_name, description) VALUES ($groupName, $description) `, { - $groupName: groupName, - $description: description - }); - return result.lastInsertRowid; -}; + $groupName: groupName, + $description: description + }); + return result.lastInsertRowid; + } -SqlTiddlerDatabase.prototype.getGroup = function(groupId) { - return this.engine.runStatementGet(` + async getGroup(groupId) { + return await this.engine.runStatementGet(` SELECT * FROM groups WHERE group_id = $groupId `, { - $groupId: groupId - }); -}; + $groupId: groupId + }); + } -SqlTiddlerDatabase.prototype.updateGroup = function(groupId, groupName, description) { - this.engine.runStatement(` + async updateGroup(groupId, groupName, description) { + await this.engine.runStatement(` UPDATE groups SET group_name = $groupName, description = $description WHERE group_id = $groupId `, { - $groupId: groupId, - $groupName: groupName, - $description: description - }); -}; + $groupId: groupId, + $groupName: groupName, + $description: description + }); + } -SqlTiddlerDatabase.prototype.deleteGroup = function(groupId) { - this.engine.runStatement(` + async deleteGroup(groupId) { + await this.engine.runStatement(` DELETE FROM groups WHERE group_id = $groupId `, { - $groupId: groupId - }); -}; + $groupId: groupId + }); + } -SqlTiddlerDatabase.prototype.listGroups = function() { - return this.engine.runStatementGetAll(` + async listGroups() { + 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(` + // Role CRUD operations + async createRole(roleName, description) { + const result = await this.engine.runStatement(` INSERT OR IGNORE INTO roles (role_name, description) VALUES ($roleName, $description) `, { - $roleName: roleName, - $description: description - }); - return result.lastInsertRowid; -}; + $roleName: roleName, + $description: description + }); + return result.lastInsertRowid; + } -SqlTiddlerDatabase.prototype.getRole = function(roleId) { - return this.engine.runStatementGet(` + async getRole(roleId) { + return await this.engine.runStatementGet(` SELECT * FROM roles WHERE role_id = $roleId `, { - $roleId: roleId - }); -}; + $roleId: roleId + }); + } -SqlTiddlerDatabase.prototype.getRoleByName = function(roleName) { - return this.engine.runStatementGet(` + async getRoleByName(roleName) { + return await this.engine.runStatementGet(` SELECT * FROM roles WHERE role_name = $roleName `, { - $roleName: roleName - }); -} + $roleName: roleName + }); + } -SqlTiddlerDatabase.prototype.updateRole = function(roleId, roleName, description) { - this.engine.runStatement(` + async updateRole(roleId, roleName, description) { + await this.engine.runStatement(` UPDATE roles SET role_name = $roleName, description = $description WHERE role_id = $roleId `, { - $roleId: roleId, - $roleName: roleName, - $description: description - }); -}; + $roleId: roleId, + $roleName: roleName, + $description: description + }); + } -SqlTiddlerDatabase.prototype.deleteRole = function(roleId) { - this.engine.runStatement(` + async deleteRole(roleId) { + await this.engine.runStatement(` DELETE FROM roles WHERE role_id = $roleId `, { - $roleId: roleId - }); -}; + $roleId: roleId + }); + } -SqlTiddlerDatabase.prototype.listRoles = function() { - return this.engine.runStatementGetAll(` + async listRoles() { + 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(` + // Permission CRUD operations + async createPermission(permissionName, description) { + const result = await this.engine.runStatement(` INSERT OR IGNORE INTO permissions (permission_name, description) VALUES ($permissionName, $description) `, { - $permissionName: permissionName, - $description: description - }); - return result.lastInsertRowid; -}; + $permissionName: permissionName, + $description: description + }); + return result.lastInsertRowid; + } -SqlTiddlerDatabase.prototype.getPermission = function(permissionId) { - return this.engine.runStatementGet(` + async getPermission(permissionId) { + return await this.engine.runStatementGet(` SELECT * FROM permissions WHERE permission_id = $permissionId `, { - $permissionId: permissionId - }); -}; + $permissionId: permissionId + }); + } -SqlTiddlerDatabase.prototype.getPermissionByName = function(permissionName) { - return this.engine.runStatementGet(` + async getPermissionByName(permissionName) { + return await this.engine.runStatementGet(` SELECT * FROM permissions WHERE permission_name = $permissionName `, { - $permissionName: permissionName - }); -}; + $permissionName: permissionName + }); + } -SqlTiddlerDatabase.prototype.updatePermission = function(permissionId, permissionName, description) { - this.engine.runStatement(` + async updatePermission(permissionId, permissionName, description) { + await this.engine.runStatement(` UPDATE permissions SET permission_name = $permissionName, description = $description WHERE permission_id = $permissionId `, { - $permissionId: permissionId, - $permissionName: permissionName, - $description: description - }); -}; + $permissionId: permissionId, + $permissionName: permissionName, + $description: description + }); + } -SqlTiddlerDatabase.prototype.deletePermission = function(permissionId) { - this.engine.runStatement(` + async deletePermission(permissionId) { + await this.engine.runStatement(` DELETE FROM permissions WHERE permission_id = $permissionId `, { - $permissionId: permissionId - }); -}; + $permissionId: permissionId + }); + } -SqlTiddlerDatabase.prototype.listPermissions = function() { - return this.engine.runStatementGetAll(` + async listPermissions() { + return await this.engine.runStatementGetAll(` SELECT * FROM permissions ORDER BY permission_name `); -}; + } -// ACL CRUD operations -SqlTiddlerDatabase.prototype.createACL = function(entityName, entityType, roleId, permissionId) { - if(!entityName.startsWith("$:/")) { - const result = this.engine.runStatement(` + // ACL CRUD operations + async createACL(entityName, entityType, roleId, permissionId) { + if (!entityName.startsWith("$:/")) { + const result = await this.engine.runStatement(` INSERT OR IGNORE INTO acl (entity_name, entity_type, role_id, permission_id) VALUES ($entityName, $entityType, $roleId, $permissionId) - `, - { - $entityName: entityName, - $entityType: entityType, - $roleId: roleId, - $permissionId: permissionId - }); - return result.lastInsertRowid; - } -}; + `, + { + $entityName: entityName, + $entityType: entityType, + $roleId: roleId, + $permissionId: permissionId + }); + return result.lastInsertRowid; + } + } -SqlTiddlerDatabase.prototype.getACL = function(aclId) { - return this.engine.runStatementGet(` + async getACL(aclId) { + return await this.engine.runStatementGet(` SELECT * FROM acl WHERE acl_id = $aclId `, { - $aclId: aclId - }); -}; + $aclId: aclId + }); + } -SqlTiddlerDatabase.prototype.updateACL = function(aclId, entityId, entityType, roleId, permissionId) { - this.engine.runStatement(` + async updateACL(aclId, entityId, entityType, roleId, permissionId) { + await this.engine.runStatement(` UPDATE acl SET entity_name = $entityId, entity_type = $entityType, role_id = $roleId, permission_id = $permissionId WHERE acl_id = $aclId `, { - $aclId: aclId, - $entityId: entityId, - $entityType: entityType, - $roleId: roleId, - $permissionId: permissionId - }); -}; + $aclId: aclId, + $entityId: entityId, + $entityType: entityType, + $roleId: roleId, + $permissionId: permissionId + }); + } -SqlTiddlerDatabase.prototype.deleteACL = function(aclId) { - this.engine.runStatement(` + async deleteACL(aclId) { + await this.engine.runStatement(` DELETE FROM acl WHERE acl_id = $aclId `, { - $aclId: aclId - }); -}; + $aclId: aclId + }); + } -SqlTiddlerDatabase.prototype.listACLs = function() { - return this.engine.runStatementGetAll(` + async listACLs() { + 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(` + // Association management functions + async addUserToGroup(userId, groupId) { + await this.engine.runStatement(` INSERT OR IGNORE INTO user_groups (user_id, group_id) VALUES ($userId, $groupId) `, { - $userId: userId, - $groupId: groupId - }); -}; + $userId: userId, + $groupId: groupId + }); + } -SqlTiddlerDatabase.prototype.isUserInGroup = function(userId, groupId) { - const result = this.engine.runStatementGet(` + async isUserInGroup(userId, groupId) { + const result = await this.engine.runStatementGet(` SELECT 1 FROM user_groups WHERE user_id = $userId AND group_id = $groupId `, { - $userId: userId, - $groupId: groupId - }); - return result !== undefined; -}; + $userId: userId, + $groupId: groupId + }); + return result !== undefined; + } -SqlTiddlerDatabase.prototype.removeUserFromGroup = function(userId, groupId) { - this.engine.runStatement(` + async removeUserFromGroup(userId, groupId) { + await this.engine.runStatement(` DELETE FROM user_groups WHERE user_id = $userId AND group_id = $groupId `, { - $userId: userId, - $groupId: groupId - }); -}; + $userId: userId, + $groupId: groupId + }); + } -SqlTiddlerDatabase.prototype.addRoleToUser = function(userId, roleId) { - this.engine.runStatement(` + async addRoleToUser(userId, roleId) { + await this.engine.runStatement(` INSERT OR IGNORE INTO user_roles (user_id, role_id) VALUES ($userId, $roleId) `, { - $userId: userId, - $roleId: roleId - }); -}; + $userId: userId, + $roleId: roleId + }); + } -SqlTiddlerDatabase.prototype.removeRoleFromUser = function(userId, roleId) { - this.engine.runStatement(` + async removeRoleFromUser(userId, roleId) { + await this.engine.runStatement(` DELETE FROM user_roles WHERE user_id = $userId AND role_id = $roleId `, { - $userId: userId, - $roleId: roleId - }); -}; + $userId: userId, + $roleId: roleId + }); + } -SqlTiddlerDatabase.prototype.addRoleToGroup = function(groupId, roleId) { - this.engine.runStatement(` + async addRoleToGroup(groupId, roleId) { + await this.engine.runStatement(` INSERT OR IGNORE INTO group_roles (group_id, role_id) VALUES ($groupId, $roleId) `, { - $groupId: groupId, - $roleId: roleId - }); -}; + $groupId: groupId, + $roleId: roleId + }); + } -SqlTiddlerDatabase.prototype.removeRoleFromGroup = function(groupId, roleId) { - this.engine.runStatement(` + async removeRoleFromGroup(groupId, roleId) { + await this.engine.runStatement(` DELETE FROM group_roles WHERE group_id = $groupId AND role_id = $roleId `, { - $groupId: groupId, - $roleId: roleId - }); -}; + $groupId: groupId, + $roleId: roleId + }); + } -SqlTiddlerDatabase.prototype.addPermissionToRole = function(roleId, permissionId) { - this.engine.runStatement(` + async addPermissionToRole(roleId, permissionId) { + await this.engine.runStatement(` INSERT OR IGNORE INTO role_permissions (role_id, permission_id) VALUES ($roleId, $permissionId) `, { - $roleId: roleId, - $permissionId: permissionId - }); -}; + $roleId: roleId, + $permissionId: permissionId + }); + } -SqlTiddlerDatabase.prototype.removePermissionFromRole = function(roleId, permissionId) { - this.engine.runStatement(` + async removePermissionFromRole(roleId, permissionId) { + await this.engine.runStatement(` DELETE FROM role_permissions WHERE role_id = $roleId AND permission_id = $permissionId `, { - $roleId: roleId, - $permissionId: permissionId - }); -}; + $roleId: roleId, + $permissionId: permissionId + }); + } -SqlTiddlerDatabase.prototype.getUserRoles = function(userId) { - const query = ` + async getUserRoles(userId) { + const query = ` SELECT r.role_id, r.role_name FROM user_roles ur JOIN roles r ON ur.role_id = r.role_id WHERE ur.user_id = $userId LIMIT 1 `; - - return this.engine.runStatementGet(query, { $userId: userId }); -}; -SqlTiddlerDatabase.prototype.deleteUserRolesByRoleId = function(roleId) { - this.engine.runStatement(` + return await this.engine.runStatementGet(query, { $userId: userId }); + } + + async deleteUserRolesByRoleId(roleId) { + await this.engine.runStatement(` DELETE FROM user_roles WHERE role_id = $roleId `, { - $roleId: roleId - }); -}; + $roleId: roleId + }); + } -SqlTiddlerDatabase.prototype.deleteUserRolesByUserId = function(userId) { - this.engine.runStatement(` + async deleteUserRolesByUserId(userId) { + await this.engine.runStatement(` DELETE FROM user_roles WHERE user_id = $userId `, { - $userId: userId - }); -}; + $userId: userId + }); + } -SqlTiddlerDatabase.prototype.isRoleInUse = function(roleId) { - // Check if the role is assigned to any users - const userRoleCheck = this.engine.runStatementGet(` + async isRoleInUse(roleId) { + // Check if the role is assigned to any users + const userRoleCheck = await this.engine.runStatementGet(` SELECT 1 FROM user_roles WHERE role_id = $roleId LIMIT 1 `, { - $roleId: roleId - }); + $roleId: roleId + }); - if(userRoleCheck) { - return true; - } + if (userRoleCheck) { + return true; + } - // Check if the role is used in any ACLs - const aclRoleCheck = this.engine.runStatementGet(` + // Check if the role is used in any ACLs + const aclRoleCheck = await this.engine.runStatementGet(` SELECT 1 FROM acl WHERE role_id = $roleId LIMIT 1 `, { - $roleId: roleId - }); + $roleId: roleId + }); - if(aclRoleCheck) { - return true; - } + if (aclRoleCheck) { + return true; + } - // If we've reached this point, the role is not in use - return false; -}; + // If we've reached this point, the role is not in use + return false; + } -SqlTiddlerDatabase.prototype.getRoleById = function(roleId) { - const role = this.engine.runStatementGet(` + async getRoleById(roleId) { + const role = await this.engine.runStatementGet(` SELECT role_id, role_name, description FROM roles WHERE role_id = $roleId `, { - $roleId: roleId - }); + $roleId: roleId + }); - return role; -}; + return role; + } + } -exports.SqlTiddlerDatabase = SqlTiddlerDatabase; + exports.SqlTiddlerDatabase = SqlTiddlerDatabase; })(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js index bb32eba18d3..09edb06c025 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js @@ -13,22 +13,34 @@ This class is largely a wrapper for the sql-tiddler-database.js class, adding th \*/ -(function() { - -/* -Create a tiddler store. Options include: - -databasePath - path to the database file (can be ":memory:" to get a temporary database) -adminWiki - reference to $tw.Wiki object used for configuration -attachmentStore - reference to associated attachment store -engine - wasm | better -*/ -function SqlTiddlerStore(options) { +(function () { + +// /* +// Create a tiddler store. Options include: + +// databasePath - path to the database file (can be ":memory:" to get a temporary database) +// adminWiki - reference to $tw.Wiki object used for configuration +// attachmentStore - reference to associated attachment store +// engine - wasm | better +// */ + +class SqlTiddlerStore { +/** + * @class SqlTiddlerStore + * @param {{ + * databasePath?: String, + * adminWiki?: $TW.Wiki, + * attachmentStore?: import("./attachments").AttachmentStore, + * engine?: String + * }} options + */ +constructor(options) { options = options || {}; this.attachmentStore = options.attachmentStore; this.adminWiki = options.adminWiki || $tw.wiki; this.eventListeners = {}; // Hashmap by type of array of event listener functions this.eventOutstanding = {}; // Hashmap by type of boolean true of outstanding events + // Create the database this.databasePath = options.databasePath || ":memory:"; var SqlTiddlerDatabase = require("$:/plugins/tiddlywiki/multiwikiserver/store/sql-tiddler-database.js").SqlTiddlerDatabase; @@ -36,46 +48,55 @@ function SqlTiddlerStore(options) { databasePath: this.databasePath, engine: options.engine }); - this.sqlTiddlerDatabase.createTables(); + const error = new Error("syncCheck"); + this.syncCheck = setTimeout(() => { + console.error(error); + }); +} + + +async initCheck() { + clearTimeout(this.syncCheck); + this.syncCheck = undefined; + await this.sqlTiddlerDatabase.createTables(); } -SqlTiddlerStore.prototype.addEventListener = function(type,listener) { - this.eventListeners[type] = this.eventListeners[type] || []; +addEventListener(type, listener) { + this.eventListeners[type] = this.eventListeners[type] || []; this.eventListeners[type].push(listener); -}; +} -SqlTiddlerStore.prototype.removeEventListener = function(type,listener) { +removeEventListener(type, listener) { const listeners = this.eventListeners[type]; if(listeners) { var p = listeners.indexOf(listener); if(p !== -1) { - listeners.splice(p,1); + listeners.splice(p, 1); } } -}; +} -SqlTiddlerStore.prototype.dispatchEvent = function(type /*, args */) { +dispatchEvent(type /*, args */) { const self = this; if(!this.eventOutstanding[type]) { - $tw.utils.nextTick(function() { + $tw.utils.nextTick(function () { self.eventOutstanding[type] = false; - const args = Array.prototype.slice.call(arguments,1), - listeners = self.eventListeners[type]; + const args = Array.prototype.slice.call(arguments, 1), listeners = self.eventListeners[type]; if(listeners) { - for(var p=0; p attachmentSizeLimit; - - if(existing_attachment_blob) { - const fileSize = this.attachmentStore.getAttachmentFileSize(existing_attachment_blob); - if(fileSize <= attachmentSizeLimit) { - const existingAttachmentMeta = this.attachmentStore.getAttachmentMetadata(existing_attachment_blob); - const hasCanonicalField = !!tiddlerFields._canonical_uri; - const skipAttachment = hasCanonicalField && (tiddlerFields._canonical_uri === (existingAttachmentMeta ? existingAttachmentMeta._canonical_uri : existing_canonical_uri)); - shouldProcessAttachment = !skipAttachment; - } else { - shouldProcessAttachment = false; - } - } - - if(attachmentsEnabled && isBinary && shouldProcessAttachment) { - const attachment_blob = existing_attachment_blob || this.attachmentStore.saveAttachment({ - text: tiddlerFields.text, - type: tiddlerFields.type, - reference: tiddlerFields.title, - _canonical_uri: tiddlerFields._canonical_uri - }); - - if(tiddlerFields && tiddlerFields._canonical_uri) { - delete tiddlerFields._canonical_uri; - } - - return { - tiddlerFields: Object.assign({}, tiddlerFields, { text: undefined }), - attachment_blob: attachment_blob - }; - } else { - return { - tiddlerFields: tiddlerFields, - attachment_blob: existing_attachment_blob - }; - } -}; - -SqlTiddlerStore.prototype.saveTiddlersFromPath = function(tiddler_files_path,bag_name) { + const attachmentsEnabled = this.adminWiki.getTiddlerText("$:/config/MultiWikiServer/EnableAttachments", "yes") === "yes"; + const contentTypeInfo = $tw.config.contentTypeInfo[tiddlerFields.type || "text/vnd.tiddlywiki"]; + const isBinary = !!contentTypeInfo && contentTypeInfo.encoding === "base64"; + + let shouldProcessAttachment = tiddlerFields.text && tiddlerFields.text.length > attachmentSizeLimit; + + if(existing_attachment_blob) { + const fileSize = this.attachmentStore.getAttachmentFileSize(existing_attachment_blob); + if(fileSize <= attachmentSizeLimit) { + const existingAttachmentMeta = this.attachmentStore.getAttachmentMetadata(existing_attachment_blob); + const hasCanonicalField = !!tiddlerFields._canonical_uri; + const skipAttachment = hasCanonicalField && (tiddlerFields._canonical_uri === (existingAttachmentMeta ? existingAttachmentMeta._canonical_uri : existing_canonical_uri)); + shouldProcessAttachment = !skipAttachment; + } else { + shouldProcessAttachment = false; + } + } + + if(attachmentsEnabled && isBinary && shouldProcessAttachment) { + const attachment_blob = existing_attachment_blob || this.attachmentStore.saveAttachment({ + text: tiddlerFields.text, + type: tiddlerFields.type, + reference: tiddlerFields.title, + _canonical_uri: tiddlerFields._canonical_uri + }); + + if(tiddlerFields && tiddlerFields._canonical_uri) { + delete tiddlerFields._canonical_uri; + } + + return { + tiddlerFields: Object.assign({}, tiddlerFields, { text: undefined }), + attachment_blob: attachment_blob + }; + } else { + return { + tiddlerFields: tiddlerFields, + attachment_blob: existing_attachment_blob + }; + } +} + +async saveTiddlersFromPath(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)); + 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(); -}; +async listBags() { + return await this.sqlTiddlerDatabase.listBags(); +} /* Options include: allowPrivilegedCharacters - allows "$", ":" and "/" to appear in recipe name */ -SqlTiddlerStore.prototype.createBag = function(bag_name,description,options) { +async createBag(bag_name, description, options) { options = options || {}; var self = this; - return this.sqlTiddlerDatabase.transaction(function() { - const validationBagName = self.validateItemName(bag_name,options.allowPrivilegedCharacters); + return await this.sqlTiddlerDatabase.transaction(async function () { + const validationBagName = self.validateItemName(bag_name, options.allowPrivilegedCharacters); if(validationBagName) { - return {message: 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(); -}; +async listRecipes() { + return await this.sqlTiddlerDatabase.listRecipes(); +} /* Returns null on success, or {message:} on error @@ -242,39 +263,39 @@ Options include: allowPrivilegedCharacters - allows "$", ":" and "/" to appear in recipe name */ -SqlTiddlerStore.prototype.createRecipe = function(recipe_name,bag_names,description,options) { +async createRecipe(recipe_name, bag_names, description, options) { bag_names = bag_names || []; description = description || ""; options = options || {}; - const validationRecipeName = this.validateItemName(recipe_name,options.allowPrivilegedCharacters); + const validationRecipeName = this.validateItemName(recipe_name, options.allowPrivilegedCharacters); if(validationRecipeName) { - return {message: validationRecipeName}; + return { message: validationRecipeName }; } if(bag_names.length === 0) { - return {message: "Recipes must contain at least one bag"}; + 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; }); -}; +} /* Returns {tiddler_id:} */ -SqlTiddlerStore.prototype.saveBagTiddler = function(incomingTiddlerFields,bag_name) { +async saveBagTiddler(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` + _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{ tiddlerFields, attachment_blob } = this.processIncomingTiddler(incomingTiddlerFields, existing_attachment_blob, _canonical_uri); + const result = await this.sqlTiddlerDatabase.saveBagTiddler(tiddlerFields, bag_name, attachment_blob); this.dispatchEvent("change"); return result; -}; +} /* Create a tiddler in a bag adopting the specified file as the attachment. The attachment file must be on the same disk as the attachment store @@ -286,50 +307,50 @@ type - content type of file as uploaded Returns {tiddler_id:} */ -SqlTiddlerStore.prototype.saveBagTiddlerWithAttachment = function(incomingTiddlerFields,bag_name,options) { - const attachment_blob = this.attachmentStore.adoptAttachment(options.filepath,options.type,options.hash,options._canonical_uri); +async saveBagTiddlerWithAttachment(incomingTiddlerFields, bag_name, options) { + const attachment_blob = this.attachmentStore.adoptAttachment(options.filepath, options.type, options.hash, options._canonical_uri); if(attachment_blob) { - const result = 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 { return null; } -}; +} /* Returns {tiddler_id:,bag_name:} */ -SqlTiddlerStore.prototype.saveRecipeTiddler = function(incomingTiddlerFields,recipe_name) { - const existing_attachment_blob = this.sqlTiddlerDatabase.getRecipeTiddlerAttachmentBlob(incomingTiddlerFields.title,recipe_name) - const {tiddlerFields, attachment_blob} = this.processIncomingTiddler(incomingTiddlerFields,existing_attachment_blob,incomingTiddlerFields._canonical_uri); - const result = this.sqlTiddlerDatabase.saveRecipeTiddler(tiddlerFields,recipe_name,attachment_blob); +async saveRecipeTiddler(incomingTiddlerFields, recipe_name) { + const existing_attachment_blob = await this.sqlTiddlerDatabase.getRecipeTiddlerAttachmentBlob(incomingTiddlerFields.title, recipe_name); + const{ tiddlerFields, attachment_blob } = await this.processIncomingTiddler(incomingTiddlerFields, existing_attachment_blob, incomingTiddlerFields._canonical_uri); + 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); +async deleteTiddler(title, bag_name) { + const result = await this.sqlTiddlerDatabase.deleteTiddler(title, bag_name); this.dispatchEvent("change"); return result; -}; +} /* returns {tiddler_id:,tiddler:} */ -SqlTiddlerStore.prototype.getBagTiddler = function(title,bag_name) { - var tiddlerInfo = this.sqlTiddlerDatabase.getBagTiddler(title,bag_name); +async getBagTiddler(title, bag_name) { + var tiddlerInfo = await this.sqlTiddlerDatabase.getBagTiddler(title, bag_name); if(tiddlerInfo) { return Object.assign( {}, tiddlerInfo, { - tiddler: this.processOutgoingTiddler(tiddlerInfo.tiddler,tiddlerInfo.tiddler_id,bag_name,tiddlerInfo.attachment_blob) - }); + tiddler: this.processOutgoingTiddler(tiddlerInfo.tiddler, tiddlerInfo.tiddler_id, bag_name, tiddlerInfo.attachment_blob) + }); } else { return null; } -}; +} /* Get an attachment ready to stream. Returns null if there is an error or: @@ -338,8 +359,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); +async getBagTiddlerStream(title, bag_name) { + const tiddlerInfo = await this.sqlTiddlerDatabase.getBagTiddler(title, bag_name); if(tiddlerInfo) { if(tiddlerInfo.attachment_blob) { return $tw.utils.extend( @@ -351,12 +372,12 @@ SqlTiddlerStore.prototype.getBagTiddlerStream = function(title,bag_name) { } ); } else { - const { Readable } = require('stream'); + const{ Readable } = require("stream"); const stream = new Readable(); - stream._read = function() { + stream._read = function () { // Push data const type = tiddlerInfo.tiddler.type || "text/plain"; - stream.push(tiddlerInfo.tiddler.text || "",($tw.config.contentTypeInfo[type] ||{encoding: "utf8"}).encoding); + stream.push(tiddlerInfo.tiddler.text || "", ($tw.config.contentTypeInfo[type] || { encoding: "utf8" }).encoding); // Push null to indicate the end of the stream stream.push(null); }; @@ -365,73 +386,74 @@ SqlTiddlerStore.prototype.getBagTiddlerStream = function(title,bag_name) { bag_name: bag_name, stream: stream, type: tiddlerInfo.tiddler.type || "text/plain" - } + }; } } else { return null; } -}; +} /* Returns {bag_name:, tiddler: {fields}, tiddler_id:} */ -SqlTiddlerStore.prototype.getRecipeTiddler = function(title,recipe_name) { - var tiddlerInfo = this.sqlTiddlerDatabase.getRecipeTiddler(title,recipe_name); +async getRecipeTiddler(title, recipe_name) { + var tiddlerInfo = await this.sqlTiddlerDatabase.getRecipeTiddler(title, recipe_name); if(tiddlerInfo) { return Object.assign( {}, tiddlerInfo, { - tiddler: this.processOutgoingTiddler(tiddlerInfo.tiddler,tiddlerInfo.tiddler_id,tiddlerInfo.bag_name,tiddlerInfo.attachment_blob) + tiddler: this.processOutgoingTiddler(tiddlerInfo.tiddler, tiddlerInfo.tiddler_id, tiddlerInfo.bag_name, tiddlerInfo.attachment_blob) }); } else { return null; } -}; +} /* Get the titles of the tiddlers in a bag. Returns an empty array for bags that do not exist */ -SqlTiddlerStore.prototype.getBagTiddlers = function(bag_name) { - return this.sqlTiddlerDatabase.getBagTiddlers(bag_name); -}; +async getBagTiddlers(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); -}; +async getBagLastTiddlerId(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); -}; +async getRecipeTiddlers(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); -}; +async getRecipeLastTiddlerId(recipe_name) { + return await this.sqlTiddlerDatabase.getRecipeLastTiddlerId(recipe_name); +} -SqlTiddlerStore.prototype.deleteAllTiddlersInBag = function(bag_name) { +async deleteAllTiddlersInBag(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; }); -}; +} /* 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); -}; +async getRecipeBags(recipe_name) { + return await this.sqlTiddlerDatabase.getRecipeBags(recipe_name); +} +} exports.SqlTiddlerStore = SqlTiddlerStore; From 1ec60ed477750d2e6029eee7031e514d59a66e23 Mon Sep 17 00:00:00 2001 From: arlen22 Date: Thu, 9 Jan 2025 12:35:20 -0500 Subject: [PATCH 05/14] add await to tests (first pass) --- .../store/tests-sql-tiddler-database.js | 171 +++++++++--------- .../modules/store/tests-sql-tiddler-store.js | 95 +++++----- .../modules/tests/test-attachment.js | 116 ++++++------ 3 files changed, 192 insertions(+), 190 deletions(-) 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..1eec51bbc8d 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,110 +14,110 @@ 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 () { + void runSqlDatabaseTests("node").catch(console.error); }); -describe("SQL tiddler database with node-sqlite3-wasm", function() { - runSqlDatabaseTests("wasm"); +describe("SQL tiddler database with node-sqlite3-wasm", function () { + void runSqlDatabaseTests("wasm").catch(console.error); }); -describe("SQL tiddler database with better-sqlite3", function() { - runSqlDatabaseTests("better"); +describe("SQL tiddler database with better-sqlite3", function () { + void runSqlDatabaseTests("better").catch(console.error); }); -function runSqlDatabaseTests(engine) { +async function runSqlDatabaseTests(engine) { // Create and initialise the tiddler store var SqlTiddlerDatabase = require("$:/plugins/tiddlywiki/multiwikiserver/store/sql-tiddler-database.js").SqlTiddlerDatabase; const sqlTiddlerDatabase = new SqlTiddlerDatabase({ engine: engine }); - sqlTiddlerDatabase.createTables(); + 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 +125,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..a914fca9987 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,27 @@ function runSqlStoreTests(engine) { var store; - beforeEach(function() { + beforeEach(async function() { store = new SqlTiddlerStore({ databasePath: ":memory:", engine: engine }); + await store.initCheck(); }); - 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 +54,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 +72,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 +83,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 +123,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..39d09c12195 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"); } }); }); From 873fb90dcc2931e89edd562caba0886378414e46 Mon Sep 17 00:00:00 2001 From: arlen22 Date: Thu, 9 Jan 2025 18:05:24 -0500 Subject: [PATCH 06/14] only ignore store folders inside editions --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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/ From f410d95c463304b2cd3370eb75baa1febc022ae2 Mon Sep 17 00:00:00 2001 From: arlen22 Date: Thu, 9 Jan 2025 18:29:38 -0500 Subject: [PATCH 07/14] add eslint rule for ALL promises --- .../multiwikiserver/eslint.config.js | 687 +++++++++--------- 1 file changed, 353 insertions(+), 334 deletions(-) diff --git a/plugins/tiddlywiki/multiwikiserver/eslint.config.js b/plugins/tiddlywiki/multiwikiserver/eslint.config.js index 2ee6d86cf3f..84b85572672 100644 --- a/plugins/tiddlywiki/multiwikiserver/eslint.config.js +++ b/plugins/tiddlywiki/multiwikiserver/eslint.config.js @@ -2,364 +2,383 @@ const globals = require("globals"); const js = require("@eslint/js"); const ts = require("typescript-eslint"); -// const { -// FlatCompat, -// } = require("@eslint/eslintrc"); +const utils_1 = require("@typescript-eslint/utils"); +const tsutils = require("ts-api-utils"); -// const compat = new FlatCompat({ -// baseDirectory: __dirname, -// recommendedConfig: ts.config( -// js.configs.recommended, -// ts.configs.recommended, -// ), -// allConfig: ts.config( -// js.configs.all, -// ts.configs.all, -// ), -// }); - -// import eslint from '@eslint/js'; -// import tseslint from 'typescript-eslint'; +const AlwaysAwaitRule = { + meta: { + type: 'problem', + messages: { + expression: 'Expected non-Promise value or awaited Promise in an expression.', + }, + }, + create(context) { + const services = utils_1.ESLintUtils.getParserServices(context); + const checker = services.program.getTypeChecker(); + const checks = new Set(["AwaitExpression", "VoidExpression"]) + return { + ":expression"(node) { + if (checks.has(node.type)) return; + if (checks.has(node.parent.type)) return; + const tsNode = services.esTreeNodeToTSNodeMap.get(node); + if (isSometimesThenable(checker, tsNode)) { + context.report({ node: node, messageId: 'expression' }); + } + }, + }; + function isSometimesThenable(checker, tsNode) { + 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 = ts.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/", - ] - }, - js.configs.recommended, - ts.configs.base, - { - languageOptions: { - globals: { - ...globals.browser, - ...globals.commonjs, - ...globals.node, - // $tw: "writable", // temporary - }, + { + 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/", + ] + }, + js.configs.recommended, + ts.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", - }, + 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"], + 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, - }], + "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", + "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: "off", - "indent-legacy": "off", - "init-declarations": "off", - "jsx-quotes": "error", - "key-spacing": "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: "off", + "indent-legacy": "off", + "init-declarations": "off", + "jsx-quotes": "error", + "key-spacing": "off", - // "keyword-spacing": ["error", { - // before: true, - // after: false, + // "keyword-spacing": ["error", { + // before: true, + // after: false, - // overrides: { - // case: { - // after: true, - // }, + // overrides: { + // case: { + // after: true, + // }, - // do: { - // after: true, - // }, + // do: { + // after: true, + // }, - // else: { - // after: true, - // }, + // else: { + // after: true, + // }, - // return: { - // after: true, - // }, + // return: { + // after: true, + // }, - // throw: { - // after: true, - // }, + // throw: { + // after: true, + // }, - // try: { - // 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", + "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-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-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-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", + "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, - // }], + // 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", + 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, - }], + "valid-typeof": ["error", { + requireStringLiterals: false, + }], - "vars-on-top": "off", - "wrap-iife": "off", - "wrap-regex": "off", - "yield-star-spacing": "error", - yoda: "off", + "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", + // 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/promise-function-async": ["error", { - allowAny: true, - allowedPromiseNames: [], - checkArrowFunctions: true, - checkFunctionDeclarations: true, - checkFunctionExpressions: true, - checkMethodDeclarations: true, - }], - "@typescript-eslint/no-misused-promises": "error", - "arrow-body-style": "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", { + allowAny: true, + allowedPromiseNames: [], + checkArrowFunctions: true, + checkFunctionDeclarations: true, + checkFunctionExpressions: true, + checkMethodDeclarations: true, + }], + "custom-rules/always-await": "error", + "arrow-body-style": "off", + }, + } ); From 548c7d0eeabc9f2a9542255ddc0a72ccb514c93f Mon Sep 17 00:00:00 2001 From: arlen22 Date: Thu, 9 Jan 2025 18:29:53 -0500 Subject: [PATCH 08/14] tweak NODE_DEV_PATH --- boot/boot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/boot/boot.js b/boot/boot.js index f74b62d81fd..12512e6d3b3 100644 --- a/boot/boot.js +++ b/boot/boot.js @@ -2117,7 +2117,7 @@ $tw.loadPluginFolder = function(filepath,excludeRegExp) { var tiddlers = pluginFiles[f].tiddlers; for(var t=0; t Date: Thu, 9 Jan 2025 18:33:18 -0500 Subject: [PATCH 09/14] more small catches and acl-middleware --- .../tiddlywiki/multiwikiserver/globals.d.ts | 1 + .../modules/commands/mws-save-archive.js | 4 + .../multiwikiserver/modules/mws-server.js | 70 +- .../modules/routes/handlers/delete-acl.js | 2 +- .../routes/handlers/delete-bag-tiddler.js | 2 +- .../routes/handlers/get-bag-tiddler-blob.js | 2 +- .../routes/handlers/get-bag-tiddler.js | 2 +- .../modules/routes/handlers/post-user.js | 2 +- .../modules/routes/helpers/acl-middleware.js | 32 +- .../modules/routes/helpers/multipart-forms.js | 1 + .../modules/store/sql-tiddler-database.js | 2966 +++++++++-------- .../store/tests-sql-tiddler-database.js | 3 + 12 files changed, 1562 insertions(+), 1525 deletions(-) diff --git a/plugins/tiddlywiki/multiwikiserver/globals.d.ts b/plugins/tiddlywiki/multiwikiserver/globals.d.ts index 21f106f0a2c..bc54df250c9 100644 --- a/plugins/tiddlywiki/multiwikiserver/globals.d.ts +++ b/plugins/tiddlywiki/multiwikiserver/globals.d.ts @@ -45,6 +45,7 @@ declare global { handler: ServerRouteHandler; method?: string; useACL?: boolean; + /** this is required if useACL is true */ entityName?: string; csrfDisable?: boolean; bodyFormat?: string; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-save-archive.js b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-save-archive.js index e45e535fe7d..27bb7c7674a 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-save-archive.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-save-archive.js @@ -54,6 +54,10 @@ async function saveArchive(archivePath) { const tiddlerInfo = await $tw.mws.store.getBagTiddler(title,bagInfo.bag_name); const bagPart = $tw.utils.encodeURIComponentExtended(bagInfo.bag_name); const titlePart = $tw.utils.encodeURIComponentExtended(title); + if(!tiddlerInfo) { + $tw.utils.warning(`Missing tiddler ${title} in bag ${bagInfo.bag_name}`); + continue; + } saveJsonFile(`bags/${bagPart}/tiddlers/${titlePart}.json`,tiddlerInfo.tiddler); } } diff --git a/plugins/tiddlywiki/multiwikiserver/modules/mws-server.js b/plugins/tiddlywiki/multiwikiserver/modules/mws-server.js index a95507c79a4..55c8e4f34b2 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/mws-server.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/mws-server.js @@ -27,8 +27,8 @@ if($tw.node) { /* A simple HTTP server with regexp-based routes options: variables - optional hashmap of variables to set (a misnomer - they are really constant parameters) - routes - optional array of routes to use - wiki - reference to wiki object + routes - optional array of routes to use + wiki - reference to wiki object */ /** * @@ -44,7 +44,7 @@ function Server(options) { /** @type {SqlTiddlerDatabase} */ this.sqlTiddlerDatabase = options.sqlTiddlerDatabase || $tw.mws.store.sqlTiddlerDatabase; // Initialise the variables - /** @type {ServerOptions["variables"]} */ + /** @type {ServerOptions["variables"] & {}} */ this.variables = $tw.utils.extend({},this.defaultVariables); if(options.variables) { for(var variable in options.variables) { @@ -64,7 +64,7 @@ function Server(options) { // Initialise authorization var authorizedUserName; if(this.get("username") && this.get("password")) { - authorizedUserName = this.get("username"); + authorizedUserName = this.get("username") || ""; //redundant for type checker } else if(this.get("credentials")) { authorizedUserName = "(authenticated)"; } else { @@ -87,7 +87,7 @@ function Server(options) { self.addRoute(routeDefinition, title); }); // Initialise the http vs https - /** @type {import("https").ServerOptions} */ + /** @type {import("https").ServerOptions | null} */ this.listenOptions = null; /** @type {"http" | "https"} */ this.protocol = "http"; @@ -193,15 +193,15 @@ cbFinished(err) - invoked when the all the form data has been processed * * @param {import("http").IncomingMessage} request * @param {Object} options - * @param {(headers: Object, name: string, filename: string) => void} options.cbPartStart + * @param {(headers: Object, name: string | null, filename: string | null) => void} options.cbPartStart * @param {(chunk: Buffer) => void} options.cbPartChunk * @param {() => void} options.cbPartEnd - * @param {(err: string) => void} options.cbFinished + * @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 @@ -243,7 +243,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"]) { @@ -355,6 +357,7 @@ Server.prototype.get = function(name) { 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); }; @@ -375,7 +378,6 @@ Server.prototype.addAuthenticator = function(AuthenticatorClass) { * @param {*} state * @returns {ServerRoute | null} */ -// eslint-disable-next-line @typescript-eslint/promise-function-async Server.prototype.findMatchingRoute = function findMatchingRoute(request, state) { for(var t = 0; t < this.routes.length; t++) { var potentialRoute = this.routes[t], @@ -433,12 +435,12 @@ Server.prototype.parseCookieString = function(cookieString) { if (typeof cookieString !== 'string') return cookies; cookieString.split(';').forEach(cookie => { - 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; @@ -498,14 +500,14 @@ Server.prototype.makeRequestState = async function(request, response, options) { // Authenticate the user const authenticatedUser = await this.authenticateUser(request, response); const authenticatedUsername = authenticatedUser?.username; - + var state = {}; 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(this,request,response); state.redirect = redirect.bind(this,request,response); @@ -516,7 +518,7 @@ Server.prototype.makeRequestState = async function(request, response, options) { // 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(); @@ -532,8 +534,8 @@ Server.prototype.makeRequestState = async function(request, response, options) { } /** * - * @param {import("http").IncomingMessage} request - * @param {import("http").ServerResponse} response + * @param {IncomingMessage} request + * @param {ServerResponse} response * @param {*} options * @returns */ @@ -554,26 +556,31 @@ Server.prototype.requestHandler = async function(request,response,options) { // Find the route that matches this path var route = this.findMatchingRoute(request,state); + // Return a 404 if we didn't find a route + if(!route) { + 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(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.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 + "'"); @@ -660,9 +667,10 @@ 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); - }); + }); } - void self.requestHandler(request,response,options); + // eslint-disable-next-line custom-rules/always-await + 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() { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-acl.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-acl.js index eb20d57641f..7e170ea5488 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-acl.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-acl.js @@ -32,7 +32,7 @@ POST /admin/delete-acl 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"); await sqlTiddlerDatabase.deleteACL(acl_id); 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 a042de676c5..33bfdf8e009 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-bag-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-bag-tiddler.js @@ -19,7 +19,7 @@ exports.method = "DELETE"; exports.path = /^\/bags\/([^\/]+)\/tiddlers\/(.+)$/; /** @type {ServerRouteHandler} */ exports.handler = async function(request,response,state) { - aclMiddleware(request, response, state, "bag", "WRITE"); + 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]); 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 c3753a4c43e..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 @@ -19,7 +19,7 @@ exports.method = "GET"; exports.path = /^\/bags\/([^\/]+)\/tiddlers\/([^\/]+)\/blob$/; /** @type {ServerRouteHandler} */ exports.handler = async function(request,response,state) { - aclMiddleware(request, response, state, "bag", "READ"); + 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]); 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 657ed72b9d0..7a0f8fb3aa5 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler.js @@ -23,7 +23,7 @@ exports.method = "GET"; exports.path = /^\/bags\/([^\/]+)\/tiddlers\/(.+)$/; /** @type {ServerRouteHandler} */ exports.handler = async function(request,response,state) { - aclMiddleware(request, response, state, "bag", "READ"); + 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]), diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-user.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-user.js index d0a32c647f6..c6902d4c37b 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-user.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-user.js @@ -113,7 +113,7 @@ exports.handler = async function(request, response, state) { 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 { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js index 8c38d8301cf..e52ed5b4ca2 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js @@ -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 7b066c3e1e8..46b535928bb 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/multipart-forms.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/multipart-forms.js @@ -94,6 +94,7 @@ 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, diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js index ecd5c1f2a5b..8bac2992668 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js @@ -12,1486 +12,1490 @@ Validation is for the most part left to the caller (function () { - /* - Create a tiddler store. Options include: - - databasePath - path to the database file (can be ":memory:" to get a temporary database) - engine - wasm | better - */ - class SqlTiddlerDatabase { - constructor(options) { - options = options || {}; - /** @type {typeof import("./sql-engine").SqlEngine} */ - const SqlEngine = require("$:/plugins/tiddlywiki/multiwikiserver/store/sql-engine.js").SqlEngine; - this.engine = new SqlEngine({ - databasePath: options.databasePath, - engine: options.engine - }); - this.entityTypeToTableMap = { - bag: { - table: "bags", - column: "bag_name" - }, - recipe: { - table: "recipes", - column: "recipe_name" - } - }; - } - - async close() { - await this.engine.close(); - } - - async transaction(fn) { - return await this.engine.transaction(fn); - } - - async createTables() { - await this.engine.runStatements([` - -- Users table - CREATE TABLE IF NOT EXISTS users ( - user_id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - email TEXT UNIQUE NOT NULL, - password TEXT NOT NULL, - created_at TEXT DEFAULT (datetime('now')), - last_login TEXT - ) - `, ` - -- User Session table - CREATE TABLE IF NOT EXISTS sessions ( - user_id INTEGER NOT NULL, - session_id TEXT NOT NULL, - created_at TEXT NOT NULL, - last_accessed TEXT NOT NULL, - PRIMARY KEY (session_id), - FOREIGN KEY (user_id) REFERENCES users(user_id) - ) - `, ` - -- Groups table - CREATE TABLE IF NOT EXISTS groups ( - group_id INTEGER PRIMARY KEY AUTOINCREMENT, - group_name TEXT UNIQUE NOT NULL, - description TEXT - ) - `, ` - -- Roles table - CREATE TABLE IF NOT EXISTS roles ( - role_id INTEGER PRIMARY KEY AUTOINCREMENT, - role_name TEXT UNIQUE NOT NULL, - description TEXT - ) - `, ` - -- Permissions table - CREATE TABLE IF NOT EXISTS permissions ( - permission_id INTEGER PRIMARY KEY AUTOINCREMENT, - permission_name TEXT UNIQUE NOT NULL, - description TEXT - ) - `, ` - -- User-Group association table - CREATE TABLE IF NOT EXISTS user_groups ( - user_id INTEGER, - group_id INTEGER, - PRIMARY KEY (user_id, group_id), - FOREIGN KEY (user_id) REFERENCES users(user_id), - FOREIGN KEY (group_id) REFERENCES groups(group_id) - ) - `, ` - -- User-Role association table - CREATE TABLE IF NOT EXISTS user_roles ( - user_id INTEGER, - role_id INTEGER, - PRIMARY KEY (user_id, role_id), - FOREIGN KEY (user_id) REFERENCES users(user_id), - FOREIGN KEY (role_id) REFERENCES roles(role_id) - ) - `, ` - -- Group-Role association table - CREATE TABLE IF NOT EXISTS group_roles ( - group_id INTEGER, - role_id INTEGER, - PRIMARY KEY (group_id, role_id), - FOREIGN KEY (group_id) REFERENCES groups(group_id), - FOREIGN KEY (role_id) REFERENCES roles(role_id) - ) - `, ` - -- Role-Permission association table - CREATE TABLE IF NOT EXISTS role_permissions ( - role_id INTEGER, - permission_id INTEGER, - PRIMARY KEY (role_id, permission_id), - FOREIGN KEY (role_id) REFERENCES roles(role_id), - FOREIGN KEY (permission_id) REFERENCES permissions(permission_id) - ) - `, ` - -- Bags have names and access control settings - CREATE TABLE IF NOT EXISTS bags ( - bag_id INTEGER PRIMARY KEY AUTOINCREMENT, - bag_name TEXT UNIQUE NOT NULL, - accesscontrol TEXT NOT NULL, - description TEXT NOT NULL - ) - `, ` - -- Recipes have names... - CREATE TABLE IF NOT EXISTS recipes ( - recipe_id INTEGER PRIMARY KEY AUTOINCREMENT, - recipe_name TEXT UNIQUE NOT NULL, - description TEXT NOT NULL, - owner_id INTEGER, - FOREIGN KEY (owner_id) REFERENCES users(user_id) - ) - `, ` - -- ...and recipes also have an ordered list of bags - CREATE TABLE IF NOT EXISTS recipe_bags ( - recipe_id INTEGER NOT NULL, - bag_id INTEGER NOT NULL, - position INTEGER NOT NULL, - FOREIGN KEY (recipe_id) REFERENCES recipes(recipe_id) ON UPDATE CASCADE ON DELETE CASCADE, - FOREIGN KEY (bag_id) REFERENCES bags(bag_id) ON UPDATE CASCADE ON DELETE CASCADE, - UNIQUE (recipe_id, bag_id) - ) - `, ` - -- Tiddlers are contained in bags and have titles - CREATE TABLE IF NOT EXISTS tiddlers ( - tiddler_id INTEGER PRIMARY KEY AUTOINCREMENT, - bag_id INTEGER NOT NULL, - title TEXT NOT NULL, - is_deleted BOOLEAN NOT NULL, - attachment_blob TEXT, -- null or the name of an attachment blob - FOREIGN KEY (bag_id) REFERENCES bags(bag_id) ON UPDATE CASCADE ON DELETE CASCADE, - UNIQUE (bag_id, title) - ) - `, ` - -- Tiddlers also have unordered lists of fields, each of which has a name and associated value - CREATE TABLE IF NOT EXISTS fields ( - tiddler_id INTEGER, - field_name TEXT NOT NULL, - field_value TEXT NOT NULL, - FOREIGN KEY (tiddler_id) REFERENCES tiddlers(tiddler_id) ON UPDATE CASCADE ON DELETE CASCADE, - UNIQUE (tiddler_id, field_name) - ) - `, ` - -- ACL table (using bag/recipe ids directly) - CREATE TABLE IF NOT EXISTS acl ( - acl_id INTEGER PRIMARY KEY AUTOINCREMENT, - entity_name TEXT NOT NULL, - entity_type TEXT NOT NULL CHECK (entity_type IN ('bag', 'recipe')), - role_id INTEGER, - permission_id INTEGER, - FOREIGN KEY (role_id) REFERENCES roles(role_id), - FOREIGN KEY (permission_id) REFERENCES permissions(permission_id) - ) - `, ` - -- Indexes for performance (we can add more as needed based on query patterns) - CREATE INDEX IF NOT EXISTS idx_tiddlers_bag_id ON tiddlers(bag_id) - `, ` - CREATE INDEX IF NOT EXISTS idx_fields_tiddler_id ON fields(tiddler_id) - `, ` - CREATE INDEX IF NOT EXISTS idx_recipe_bags_recipe_id ON recipe_bags(recipe_id) - `, ` - CREATE INDEX IF NOT EXISTS idx_acl_entity_id ON acl(entity_name) - `]); - } - - async listBags() { - const rows = await this.engine.runStatementGetAll(` - SELECT bag_name, bag_id, accesscontrol, description - FROM bags - ORDER BY bag_name - `); - return rows; - } - - /* - Create or update a bag - Returns the bag_id of the bag - */ - async createBag(bag_name, description, accesscontrol) { - accesscontrol = accesscontrol || ""; - // Run the queries - var bag = await this.engine.runStatement(` - INSERT OR IGNORE INTO bags (bag_name, accesscontrol, description) - VALUES ($bag_name, '', '') - `, { - $bag_name: bag_name - }); - const updateBags = await this.engine.runStatement(` - UPDATE bags - SET accesscontrol = $accesscontrol, - description = $description - WHERE bag_name = $bag_name - `, { - $bag_name: bag_name, - $accesscontrol: accesscontrol, - $description: description - }); - return updateBags.lastInsertRowid; - } - - /* - Returns array of {recipe_name:,recipe_id:,description:,bag_names: []} - */ - async listRecipes() { - const rows = await this.engine.runStatementGetAll(` - SELECT r.recipe_name, r.recipe_id, r.description, r.owner_id, b.bag_name, rb.position - FROM recipes AS r - JOIN recipe_bags AS rb ON rb.recipe_id = r.recipe_id - JOIN bags AS b ON rb.bag_id = b.bag_id - ORDER BY r.recipe_name, rb.position - `); - const results = []; - let currentRecipeName = null, currentRecipeIndex = -1; - for (const row of rows) { - if (row.recipe_name !== currentRecipeName) { - currentRecipeName = row.recipe_name; - currentRecipeIndex += 1; - results.push({ - recipe_name: row.recipe_name, - recipe_id: row.recipe_id, - description: row.description, - owner_id: row.owner_id, - bag_names: [] - }); - } - results[currentRecipeIndex].bag_names.push(row.bag_name); - } - return results; - } - - /* - Create or update a recipe - Returns the recipe_id of the recipe - */ - async createRecipe(recipe_name, bag_names, description) { - // Run the queries - await this.engine.runStatement(` - -- Delete existing recipe_bags entries for this recipe - DELETE FROM recipe_bags WHERE recipe_id = (SELECT recipe_id FROM recipes WHERE recipe_name = $recipe_name) - `, { - $recipe_name: recipe_name - }); - const updateRecipes = await this.engine.runStatement(` - -- Create the entry in the recipes table if required - INSERT OR REPLACE INTO recipes (recipe_name, description) - VALUES ($recipe_name, $description) - `, { - $recipe_name: recipe_name, - $description: description - }); - await this.engine.runStatement(` - INSERT INTO recipe_bags (recipe_id, bag_id, position) - SELECT r.recipe_id, b.bag_id, j.key as position - FROM recipes r - JOIN bags b - INNER JOIN json_each($bag_names) AS j ON j.value = b.bag_name - WHERE r.recipe_name = $recipe_name - `, { - $recipe_name: recipe_name, - $bag_names: JSON.stringify(bag_names) - }); - - return updateRecipes.lastInsertRowid; - } - - /* - Assign a recipe to a user - */ - async assignRecipeToUser(recipe_name, user_id) { - await this.engine.runStatement(` - UPDATE recipes SET owner_id = $user_id WHERE recipe_name = $recipe_name - `, { - $recipe_name: recipe_name, - $user_id: user_id - }); - } - - /* - Returns {tiddler_id:} - */ - async saveBagTiddler(tiddlerFields, bag_name, attachment_blob) { - attachment_blob = attachment_blob || null; - // Update the tiddlers table - var info = await this.engine.runStatement(` - INSERT OR REPLACE INTO tiddlers (bag_id, title, is_deleted, attachment_blob) - VALUES ( - (SELECT bag_id FROM bags WHERE bag_name = $bag_name), - $title, - FALSE, - $attachment_blob - ) - `, { - $title: tiddlerFields.title, - $attachment_blob: attachment_blob, - $bag_name: bag_name - }); - // Update the fields table - await this.engine.runStatement(` - INSERT OR REPLACE INTO fields (tiddler_id, field_name, field_value) - SELECT - t.tiddler_id, - json_each.key AS field_name, - json_each.value AS field_value - FROM ( - SELECT tiddler_id - FROM tiddlers - WHERE bag_id = ( - SELECT bag_id - FROM bags - WHERE bag_name = $bag_name - ) AND title = $title - ) AS t - JOIN json_each($field_values) AS json_each - `, { - $title: tiddlerFields.title, - $bag_name: bag_name, - $field_values: JSON.stringify(Object.assign({}, tiddlerFields, { title: undefined })) - }); - return { - tiddler_id: info.lastInsertRowid - }; - } - - /* - Returns {tiddler_id:,bag_name:} or null if the recipe is empty - */ - async saveRecipeTiddler(tiddlerFields, recipe_name, attachment_blob) { - // Find the topmost bag in the recipe - var row = await this.engine.runStatementGet(` - SELECT b.bag_name - FROM bags AS b - JOIN ( - SELECT rb.bag_id - FROM recipe_bags AS rb - WHERE rb.recipe_id = ( - SELECT recipe_id - FROM recipes - WHERE recipe_name = $recipe_name - ) - ORDER BY rb.position DESC - LIMIT 1 - ) AS selected_bag - ON b.bag_id = selected_bag.bag_id - `, { - $recipe_name: recipe_name - }); - if (!row) { - return null; - } - // Save the tiddler to the topmost bag - var info = await this.saveBagTiddler(tiddlerFields, row.bag_name, attachment_blob); - return { - tiddler_id: info.tiddler_id, - bag_name: row.bag_name - }; - } - - /* - Returns {tiddler_id:} of the delete marker - */ - async deleteTiddler(title, bag_name) { - // Delete the fields of this tiddler - await this.engine.runStatement(` - DELETE FROM fields - WHERE tiddler_id IN ( - SELECT t.tiddler_id - FROM tiddlers AS t - INNER JOIN bags AS b ON t.bag_id = b.bag_id - WHERE b.bag_name = $bag_name AND t.title = $title - ) - `, { - $title: title, - $bag_name: bag_name - }); - // Mark the tiddler itself as deleted - const rowDeleteMarker = await this.engine.runStatement(` - INSERT OR REPLACE INTO tiddlers (bag_id, title, is_deleted, attachment_blob) - VALUES ( - (SELECT bag_id FROM bags WHERE bag_name = $bag_name), - $title, - TRUE, - NULL - ) - `, { - $title: title, - $bag_name: bag_name - }); - return { tiddler_id: rowDeleteMarker.lastInsertRowid }; - } - - /* - returns {tiddler_id:,tiddler:,attachment_blob:} - */ - async getBagTiddler(title, bag_name) { - const rowTiddler = await this.engine.runStatementGet(` - SELECT t.tiddler_id, t.attachment_blob - FROM bags AS b - INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id - WHERE t.title = $title AND b.bag_name = $bag_name AND t.is_deleted = FALSE - `, { - $title: title, - $bag_name: bag_name - }); - if (!rowTiddler) { - return null; - } - const rows = await this.engine.runStatementGetAll(` - SELECT field_name, field_value, tiddler_id - FROM fields - WHERE tiddler_id = $tiddler_id - `, { - $tiddler_id: rowTiddler.tiddler_id - }); - if (rows.length === 0) { - return null; - } else { - return { - tiddler_id: rows[0].tiddler_id, - attachment_blob: rowTiddler.attachment_blob, - tiddler: rows.reduce((accumulator, value) => { - accumulator[value["field_name"]] = value.field_value; - return accumulator; - }, { title: title }) - }; - } - } - - /* - Returns {bag_name:, tiddler: {fields}, tiddler_id:, attachment_blob:} - */ - async getRecipeTiddler(title, recipe_name) { - const rowTiddlerId = await this.engine.runStatementGet(` - SELECT t.tiddler_id, t.attachment_blob, b.bag_name - FROM bags AS b - INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id - INNER JOIN recipes AS r ON rb.recipe_id = r.recipe_id - INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id - WHERE r.recipe_name = $recipe_name - AND t.title = $title - AND t.is_deleted = FALSE - ORDER BY rb.position DESC - LIMIT 1 - `, { - $title: title, - $recipe_name: recipe_name - }); - if (!rowTiddlerId) { - return null; - } - // Get the fields - const rows = await this.engine.runStatementGetAll(` - SELECT field_name, field_value - FROM fields - WHERE tiddler_id = $tiddler_id - `, { - $tiddler_id: rowTiddlerId.tiddler_id - }); - return { - bag_name: rowTiddlerId.bag_name, - tiddler_id: rowTiddlerId.tiddler_id, - attachment_blob: rowTiddlerId.attachment_blob, - tiddler: rows.reduce((accumulator, value) => { - accumulator[value["field_name"]] = value.field_value; - return accumulator; - }, { title: title }) - }; - } - - /* - Checks if a user has permission to access a recipe - */ - async hasRecipePermission(userId, recipeName, permissionName) { - try { - // check if the user is the owner of the entity - const recipe = await this.engine.runStatementGet(` - SELECT owner_id - FROM recipes - WHERE recipe_name = $recipe_name - `, { - $recipe_name: recipeName - }); - - if (recipe && !!recipe.owner_id && recipe.owner_id === userId) { - return true; - } else { - var permission = this.checkACLPermission(userId, "recipe", recipeName, permissionName, recipe && recipe.owner_id); - return permission; - } - - } catch (error) { - console.error(error); - return false; - } - } - - /* - Checks if a user has permission to access a bag - */ - async hasBagPermission(userId, bagName, permissionName) { - return await this.checkACLPermission(userId, "bag", bagName, permissionName); - } - - async getACLByName(entityType, entityName, fetchAll) { - const entityInfo = this.entityTypeToTableMap[entityType]; - if (!entityInfo) { - throw new Error("Invalid entity type: " + entityType); - } - - // First, check if there's an ACL record for the entity and get the permission_id - var checkACLExistsQuery = ` - SELECT acl.*, permissions.permission_name - FROM acl - LEFT JOIN permissions ON acl.permission_id = permissions.permission_id - WHERE acl.entity_type = $entity_type - AND acl.entity_name = $entity_name - `; - - if (!fetchAll) { - checkACLExistsQuery += " LIMIT 1"; - } - - const aclRecord = await this.engine[fetchAll ? "runStatementGetAll" : "runStatementGet"](checkACLExistsQuery, { - $entity_type: entityType, - $entity_name: entityName - }); - - return aclRecord; - } - - async checkACLPermission(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 = await this.getACLByName(entityType, entityName, true); - const aclRecord = aclRecords.find(record => record.permission_name === permissionName); - - // If no ACL record exists, return true for hasPermission - if ((!aclRecord && !ownerId && aclRecords.length === 0) || ((!!aclRecord && !!ownerId) && ownerId === userId)) { - return true; - } - - // If ACL record exists, check for user permission using the retrieved permission_id - const checkPermissionQuery = ` - SELECT * - FROM users u - JOIN user_roles ur ON u.user_id = ur.user_id - JOIN roles r ON ur.role_id = r.role_id - JOIN acl a ON r.role_id = a.role_id - WHERE u.user_id = $user_id - AND a.entity_type = $entity_type - AND a.entity_name = $entity_name - AND a.permission_id = $permission_id - LIMIT 1 - `; - - const result = await this.engine.runStatementGet(checkPermissionQuery, { - $user_id: userId, - $entity_type: entityType, - $entity_name: entityName, - $permission_id: aclRecord && aclRecord.permission_id - }); - - let hasPermission = result !== undefined; - - return hasPermission; - - } catch (error) { - console.error(error); - return false; - } - } - - /** - * Returns the ACL records for an entity (bag or recipe) - */ - async getEntityAclRecords(entityName) { - const checkACLExistsQuery = ` - SELECT * - FROM acl - WHERE entity_name = $entity_name - `; - - const aclRecords = await this.engine.runStatementGetAll(checkACLExistsQuery, { - $entity_name: entityName - }); - - return aclRecords; - } - - /* - Get the entity by name - */ - async getEntityByName(entityType, entityName) { - const entityInfo = this.entityTypeToTableMap[entityType]; - if (entityInfo) { - return await this.engine.runStatementGet(`SELECT * FROM ${entityInfo.table} WHERE ${entityInfo.column} = $entity_name`, { - $entity_name: entityName - }); - } - return null; - } - - /* - Get the titles of the tiddlers in a bag. Returns an empty array for bags that do not exist - */ - async getBagTiddlers(bag_name) { - const rows = await this.engine.runStatementGetAll(` - SELECT DISTINCT title, tiddler_id - FROM tiddlers - WHERE bag_id IN ( - SELECT bag_id - FROM bags - WHERE bag_name = $bag_name - ) - AND tiddlers.is_deleted = FALSE - ORDER BY title ASC - `, { - $bag_name: bag_name - }); - return rows; - } - - /* - Get the tiddler_id of the newest tiddler in a bag. Returns null for bags that do not exist - */ - async getBagLastTiddlerId(bag_name) { - const row = await this.engine.runStatementGet(` - SELECT tiddler_id - FROM tiddlers - WHERE bag_id IN ( - SELECT bag_id - FROM bags - WHERE bag_name = $bag_name - ) - ORDER BY tiddler_id DESC - LIMIT 1 - `, { - $bag_name: bag_name - }); - if (row) { - return row.tiddler_id; - } else { - return null; - } - } - - /* - Get the metadata of the tiddlers in a recipe as an array [{title:,tiddler_id:,bag_name:,is_deleted:}], - sorted in ascending order of tiddler_id. - - Options include: - - limit: optional maximum number of results to return - last_known_tiddler_id: tiddler_id of the last known update. Only returns tiddlers that have been created, modified or deleted since - include_deleted: boolean, defaults to false - - Returns null for recipes that do not exist - */ - async getRecipeTiddlers(recipe_name, options) { - options = options || {}; - // Get the recipe ID - const rowsCheckRecipe = await this.engine.runStatementGet(` - SELECT recipe_id FROM recipes WHERE recipes.recipe_name = $recipe_name - `, { - $recipe_name: recipe_name - }); - if (!rowsCheckRecipe) { - return null; - } - const recipe_id = rowsCheckRecipe.recipe_id; - // Compose the query to get the tiddlers - const params = { - $recipe_id: recipe_id - }; - if (options.limit) { - params.$limit = options.limit.toString(); - } - if (options.last_known_tiddler_id) { - params.$last_known_tiddler_id = options.last_known_tiddler_id; - } - const rows = await this.engine.runStatementGetAll(` - SELECT title, tiddler_id, is_deleted, bag_name - FROM ( - SELECT t.title, t.tiddler_id, t.is_deleted, b.bag_name, MAX(rb.position) AS position - FROM bags AS b - INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id - INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id - WHERE rb.recipe_id = $recipe_id - ${options.include_deleted ? "" : "AND t.is_deleted = FALSE"} - ${options.last_known_tiddler_id ? "AND tiddler_id > $last_known_tiddler_id" : ""} - GROUP BY t.title - ORDER BY t.title, tiddler_id DESC - ${options.limit ? "LIMIT $limit" : ""} - ) - `, params); - return rows; - } - - /* - Get the tiddler_id of the newest tiddler in a recipe. Returns null for recipes that do not exist - */ - async getRecipeLastTiddlerId(recipe_name) { - const row = await this.engine.runStatementGet(` - SELECT t.title, t.tiddler_id, b.bag_name, MAX(rb.position) AS position - FROM bags AS b - INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id - INNER JOIN recipes AS r ON rb.recipe_id = r.recipe_id - INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id - WHERE r.recipe_name = $recipe_name - GROUP BY t.title - ORDER BY t.tiddler_id DESC - LIMIT 1 - `, { - $recipe_name: recipe_name - }); - if (row) { - return row.tiddler_id; - } else { - return null; - } - } - - async deleteAllTiddlersInBag(bag_name) { - // Delete the fields - await this.engine.runStatement(` - DELETE FROM fields - WHERE tiddler_id IN ( - SELECT tiddler_id - FROM tiddlers - WHERE bag_id = (SELECT bag_id FROM bags WHERE bag_name = $bag_name) - AND is_deleted = FALSE - ) - `, { - $bag_name: bag_name - }); - // Mark the tiddlers as deleted - await this.engine.runStatement(` - UPDATE tiddlers - SET is_deleted = TRUE - WHERE bag_id = (SELECT bag_id FROM bags WHERE bag_name = $bag_name) - AND is_deleted = FALSE - `, { - $bag_name: bag_name - }); - } - - /* - Get the names of the bags in a recipe. Returns an empty array for recipes that do not exist - */ - async getRecipeBags(recipe_name) { - const rows = await this.engine.runStatementGetAll(` - SELECT bags.bag_name - FROM bags - JOIN ( - SELECT rb.bag_id, rb.position as position - FROM recipe_bags AS rb - JOIN recipes AS r ON rb.recipe_id = r.recipe_id - WHERE r.recipe_name = $recipe_name - ORDER BY rb.position - ) AS bag_priority ON bags.bag_id = bag_priority.bag_id - ORDER BY position - `, { - $recipe_name: recipe_name - }); - return rows.map(value => value.bag_name); - } - - /* - Get the attachment value of a bag, if any exist - */ - async getBagTiddlerAttachmentBlob(title, bag_name) { - const row = await this.engine.runStatementGet(` - SELECT t.attachment_blob - FROM bags AS b - INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id - WHERE t.title = $title AND b.bag_name = $bag_name AND t.is_deleted = FALSE - `, { - $title: title, - $bag_name: bag_name - }); - return row ? row.attachment_blob : null; - } - - /* - Get the attachment value of a recipe, if any exist - */ - async getRecipeTiddlerAttachmentBlob(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 - INNER JOIN recipes AS r ON rb.recipe_id = r.recipe_id - INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id - WHERE r.recipe_name = $recipe_name AND t.title = $title AND t.is_deleted = FALSE - ORDER BY rb.position DESC - LIMIT 1 - `, { - $title: title, - $recipe_name: recipe_name - }); - return row ? row.attachment_blob : null; - } - - // User CRUD operations - async createUser(username, email, password) { - const result = await this.engine.runStatement(` - INSERT INTO users (username, email, password) - VALUES ($username, $email, $password) - `, { - $username: username, - $email: email, - $password: password - }); - return result.lastInsertRowid; - } - - async getUser(userId) { - return await this.engine.runStatementGet(` - SELECT * FROM users WHERE user_id = $userId - `, { - $userId: userId - }); - } - - async getUserByUsername(username) { - return await this.engine.runStatementGet(` - SELECT * FROM users WHERE username = $username - `, { - $username: username - }); - } - - async getUserByEmail(email) { - return await this.engine.runStatementGet(` - SELECT * FROM users WHERE email = $email - `, { - $email: email - }); - } - - async listUsersByRoleId(roleId) { - return await this.engine.runStatementGetAll(` - SELECT u.* - FROM users u - JOIN user_roles ur ON u.user_id = ur.user_id - WHERE ur.role_id = $roleId - ORDER BY u.username - `, { - $roleId: roleId - }); - } - - async updateUser(userId, username, email, roleId) { - const existingUser = await this.engine.runStatementGet(` - SELECT user_id FROM users - WHERE email = $email AND user_id != $userId -`, { - $email: email, - $userId: userId - }); - - if (existingUser.length > 0) { - return { - success: false, - message: "Email address already in use by another user." - }; - } - - try { - await this.engine.transaction(async () => { - // Update user information - await this.engine.runStatement(` - UPDATE users - SET username = $username, email = $email - WHERE user_id = $userId - `, { - $userId: userId, - $username: username, - $email: email - }); - - if (roleId) { - // Remove all existing roles for the user - await this.engine.runStatement(` - DELETE FROM user_roles - WHERE user_id = $userId - `, { - $userId: userId - }); - - // Add the new role - await this.engine.runStatement(` - INSERT INTO user_roles (user_id, role_id) - VALUES ($userId, $roleId) - `, { - $userId: userId, - $roleId: roleId - }); - } - }); - - return { - success: true, - message: "User profile and role updated successfully." - }; - } catch (error) { - return { - success: false, - message: "Failed to update user profile: " + error.message - }; - } - } - - async updateUserPassword(userId, newHash) { - try { - await this.engine.runStatement(` - UPDATE users - SET password = $newHash - WHERE user_id = $userId - `, { - $userId: userId, - $newHash: newHash, - }); - - return { - success: true, - message: "Password updated successfully." - }; - } catch (error) { - return { - success: false, - message: "Failed to update password: " + error.message - }; - } - } - - async deleteUser(userId) { - await this.engine.runStatement(` - DELETE FROM users WHERE user_id = $userId - `, { - $userId: userId - }); - } - - async listUsers() { - return await this.engine.runStatementGetAll(` - SELECT * FROM users ORDER BY username - `); - } - - async createOrUpdateUserSession(userId, sessionId) { - const currentTimestamp = new Date().toISOString(); - - // First, try to update an existing session - const updateResult = await this.engine.runStatement(` - UPDATE sessions - SET session_id = $sessionId, last_accessed = $timestamp - WHERE user_id = $userId - `, { - $userId: userId, - $sessionId: sessionId, - $timestamp: currentTimestamp - }); - - // If no existing session was updated, create a new one - if (updateResult.changes === 0) { - await this.engine.runStatement(` - INSERT INTO sessions (user_id, session_id, created_at, last_accessed) - VALUES ($userId, $sessionId, $timestamp, $timestamp) - `, { - $userId: userId, - $sessionId: sessionId, - $timestamp: currentTimestamp - }); - } - - return sessionId; - } - - async createUserSession(userId, sessionId) { - const currentTimestamp = new Date().toISOString(); - await this.engine.runStatement(` - INSERT INTO sessions (user_id, session_id, created_at, last_accessed) - VALUES ($userId, $sessionId, $timestamp, $timestamp) - `, { - $userId: userId, - $sessionId: sessionId, - $timestamp: currentTimestamp - }); - - return sessionId; - } - - /** - * @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} - */ - async findUserBySessionId(sessionId) { - // First, get the user_id from the sessions table - const sessionResult = await this.engine.runStatementGet(` - SELECT user_id, last_accessed - FROM sessions - WHERE session_id = $sessionId - `, { - $sessionId: sessionId - }); - - if (!sessionResult) { - return null; // Session not found - } - - const lastAccessed = new Date(sessionResult.last_accessed); - const expirationTime = 24 * 60 * 60 * 1000; // 24 hours in milliseconds - if (+new Date() - +lastAccessed > expirationTime) { - // Session has expired - await this.deleteSession(sessionId); - return null; - } - - // Update the last_accessed timestamp - const currentTimestamp = new Date().toISOString(); - await this.engine.runStatement(` - UPDATE sessions - SET last_accessed = $timestamp - WHERE session_id = $sessionId - `, { - $sessionId: sessionId, - $timestamp: currentTimestamp - }); - - /** @type {any} */ - const userResult = await this.engine.runStatementGet(` - SELECT * - FROM users - WHERE user_id = $userId - `, { - $userId: sessionResult.user_id - }); - - if (!userResult) { - return null; - } - - /** @type {User} */ - return userResult; - } - - async deleteSession(sessionId) { - await this.engine.runStatement(` - DELETE FROM sessions - WHERE session_id = $sessionId - `, { - $sessionId: sessionId - }); - } - - async deleteUserSessions(userId) { - await this.engine.runStatement(` - DELETE FROM sessions - WHERE user_id = $userId - `, { - $userId: userId - }); - } - - // Set the user as an admin - async setUserAdmin(userId) { - var admin = await this.getRoleByName("ADMIN"); - if (admin) { - await this.addRoleToUser(userId, admin.role_id); - } - } - - // Group CRUD operations - async createGroup(groupName, description) { - const result = await this.engine.runStatement(` - INSERT INTO groups (group_name, description) - VALUES ($groupName, $description) - `, { - $groupName: groupName, - $description: description - }); - return result.lastInsertRowid; - } - - async getGroup(groupId) { - return await this.engine.runStatementGet(` - SELECT * FROM groups WHERE group_id = $groupId - `, { - $groupId: groupId - }); - } - - async updateGroup(groupId, groupName, description) { - await this.engine.runStatement(` - UPDATE groups - SET group_name = $groupName, description = $description - WHERE group_id = $groupId - `, { - $groupId: groupId, - $groupName: groupName, - $description: description - }); - } - - async deleteGroup(groupId) { - await this.engine.runStatement(` - DELETE FROM groups WHERE group_id = $groupId - `, { - $groupId: groupId - }); - } - - async listGroups() { - return await this.engine.runStatementGetAll(` - SELECT * FROM groups ORDER BY group_name - `); - } - - // Role CRUD operations - async createRole(roleName, description) { - const result = await this.engine.runStatement(` - INSERT OR IGNORE INTO roles (role_name, description) - VALUES ($roleName, $description) - `, { - $roleName: roleName, - $description: description - }); - return result.lastInsertRowid; - } - - async getRole(roleId) { - return await this.engine.runStatementGet(` - SELECT * FROM roles WHERE role_id = $roleId - `, { - $roleId: roleId - }); - } - - async getRoleByName(roleName) { - return await this.engine.runStatementGet(` - SELECT * FROM roles WHERE role_name = $roleName - `, { - $roleName: roleName - }); - } - - async updateRole(roleId, roleName, description) { - await this.engine.runStatement(` - UPDATE roles - SET role_name = $roleName, description = $description - WHERE role_id = $roleId - `, { - $roleId: roleId, - $roleName: roleName, - $description: description - }); - } - - async deleteRole(roleId) { - await this.engine.runStatement(` - DELETE FROM roles WHERE role_id = $roleId - `, { - $roleId: roleId - }); - } - - async listRoles() { - return await this.engine.runStatementGetAll(` - SELECT * FROM roles ORDER BY role_name DESC - `); - } - - // Permission CRUD operations - async createPermission(permissionName, description) { - const result = await this.engine.runStatement(` - INSERT OR IGNORE INTO permissions (permission_name, description) - VALUES ($permissionName, $description) - `, { - $permissionName: permissionName, - $description: description - }); - return result.lastInsertRowid; - } - - async getPermission(permissionId) { - return await this.engine.runStatementGet(` - SELECT * FROM permissions WHERE permission_id = $permissionId - `, { - $permissionId: permissionId - }); - } - - async getPermissionByName(permissionName) { - return await this.engine.runStatementGet(` - SELECT * FROM permissions WHERE permission_name = $permissionName - `, { - $permissionName: permissionName - }); - } - - async updatePermission(permissionId, permissionName, description) { - await this.engine.runStatement(` - UPDATE permissions - SET permission_name = $permissionName, description = $description - WHERE permission_id = $permissionId - `, { - $permissionId: permissionId, - $permissionName: permissionName, - $description: description - }); - } - - async deletePermission(permissionId) { - await this.engine.runStatement(` - DELETE FROM permissions WHERE permission_id = $permissionId - `, { - $permissionId: permissionId - }); - } - - async listPermissions() { - return await this.engine.runStatementGetAll(` - SELECT * FROM permissions ORDER BY permission_name - `); - } - - // ACL CRUD operations - async createACL(entityName, entityType, roleId, permissionId) { - if (!entityName.startsWith("$:/")) { - const result = await this.engine.runStatement(` - INSERT OR IGNORE INTO acl (entity_name, entity_type, role_id, permission_id) - VALUES ($entityName, $entityType, $roleId, $permissionId) - `, - { - $entityName: entityName, - $entityType: entityType, - $roleId: roleId, - $permissionId: permissionId - }); - return result.lastInsertRowid; - } - } - - async getACL(aclId) { - return await this.engine.runStatementGet(` - SELECT * FROM acl WHERE acl_id = $aclId - `, { - $aclId: aclId - }); - } - - async updateACL(aclId, entityId, entityType, roleId, permissionId) { - await this.engine.runStatement(` - UPDATE acl - SET entity_name = $entityId, entity_type = $entityType, - role_id = $roleId, permission_id = $permissionId - WHERE acl_id = $aclId - `, { - $aclId: aclId, - $entityId: entityId, - $entityType: entityType, - $roleId: roleId, - $permissionId: permissionId - }); - } - - async deleteACL(aclId) { - await this.engine.runStatement(` - DELETE FROM acl WHERE acl_id = $aclId - `, { - $aclId: aclId - }); - } - - async listACLs() { - return await this.engine.runStatementGetAll(` - SELECT * FROM acl ORDER BY entity_type, entity_name - `); - } - - // Association management functions - async addUserToGroup(userId, groupId) { - await this.engine.runStatement(` - INSERT OR IGNORE INTO user_groups (user_id, group_id) - VALUES ($userId, $groupId) - `, { - $userId: userId, - $groupId: groupId - }); - } - - async isUserInGroup(userId, groupId) { - const result = await this.engine.runStatementGet(` - SELECT 1 FROM user_groups - WHERE user_id = $userId AND group_id = $groupId - `, { - $userId: userId, - $groupId: groupId - }); - return result !== undefined; - } - - async removeUserFromGroup(userId, groupId) { - await this.engine.runStatement(` - DELETE FROM user_groups - WHERE user_id = $userId AND group_id = $groupId - `, { - $userId: userId, - $groupId: groupId - }); - } - - async addRoleToUser(userId, roleId) { - await this.engine.runStatement(` - INSERT OR IGNORE INTO user_roles (user_id, role_id) - VALUES ($userId, $roleId) - `, { - $userId: userId, - $roleId: roleId - }); - } - - async removeRoleFromUser(userId, roleId) { - await this.engine.runStatement(` - DELETE FROM user_roles - WHERE user_id = $userId AND role_id = $roleId - `, { - $userId: userId, - $roleId: roleId - }); - } - - async addRoleToGroup(groupId, roleId) { - await this.engine.runStatement(` - INSERT OR IGNORE INTO group_roles (group_id, role_id) - VALUES ($groupId, $roleId) - `, { - $groupId: groupId, - $roleId: roleId - }); - } - - async removeRoleFromGroup(groupId, roleId) { - await this.engine.runStatement(` - DELETE FROM group_roles - WHERE group_id = $groupId AND role_id = $roleId - `, { - $groupId: groupId, - $roleId: roleId - }); - } - - async addPermissionToRole(roleId, permissionId) { - await this.engine.runStatement(` - INSERT OR IGNORE INTO role_permissions (role_id, permission_id) - VALUES ($roleId, $permissionId) - `, { - $roleId: roleId, - $permissionId: permissionId - }); - } - - async removePermissionFromRole(roleId, permissionId) { - await this.engine.runStatement(` - DELETE FROM role_permissions - WHERE role_id = $roleId AND permission_id = $permissionId - `, { - $roleId: roleId, - $permissionId: permissionId - }); - } - - async getUserRoles(userId) { - const query = ` - SELECT r.role_id, r.role_name - FROM user_roles ur - JOIN roles r ON ur.role_id = r.role_id - WHERE ur.user_id = $userId - LIMIT 1 - `; - - return await this.engine.runStatementGet(query, { $userId: userId }); - } - - async deleteUserRolesByRoleId(roleId) { - await this.engine.runStatement(` - DELETE FROM user_roles - WHERE role_id = $roleId - `, { - $roleId: roleId - }); - } - - async deleteUserRolesByUserId(userId) { - await this.engine.runStatement(` - DELETE FROM user_roles - WHERE user_id = $userId - `, { - $userId: userId - }); - } - - async isRoleInUse(roleId) { - // Check if the role is assigned to any users - const userRoleCheck = await this.engine.runStatementGet(` - SELECT 1 - FROM user_roles - WHERE role_id = $roleId - LIMIT 1 - `, { - $roleId: roleId - }); - - if (userRoleCheck) { - return true; - } - - // Check if the role is used in any ACLs - const aclRoleCheck = await this.engine.runStatementGet(` - SELECT 1 - FROM acl - WHERE role_id = $roleId - LIMIT 1 - `, { - $roleId: roleId - }); - - if (aclRoleCheck) { - return true; - } - - // If we've reached this point, the role is not in use - return false; - } - - async getRoleById(roleId) { - const role = await this.engine.runStatementGet(` - SELECT role_id, role_name, description - FROM roles - WHERE role_id = $roleId - `, { - $roleId: roleId - }); - - return role; - } - } - - exports.SqlTiddlerDatabase = SqlTiddlerDatabase; +/* +Create a tiddler store. Options include: + +databasePath - path to the database file (can be ":memory:" to get a temporary database) +engine - wasm | better +*/ +class SqlTiddlerDatabase { +constructor(options) { + options = options || {}; + /** @type {typeof import("./sql-engine").SqlEngine} */ + const SqlEngine = require("$:/plugins/tiddlywiki/multiwikiserver/store/sql-engine.js").SqlEngine; + this.engine = new SqlEngine({ + databasePath: options.databasePath, + engine: options.engine + }); + this.entityTypeToTableMap = { + bag: { + table: "bags", + column: "bag_name" + }, + recipe: { + table: "recipes", + column: "recipe_name" + } + }; +} + +async close() { + await this.engine.close(); +} + +async transaction(fn) { + return await this.engine.transaction(fn); +} + +async createTables() { + await this.engine.runStatements([` +-- Users table +CREATE TABLE IF NOT EXISTS users ( + user_id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + email TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + created_at TEXT DEFAULT (datetime('now')), + last_login TEXT +) +`, ` +-- User Session table +CREATE TABLE IF NOT EXISTS sessions ( + user_id INTEGER NOT NULL, + session_id TEXT NOT NULL, + created_at TEXT NOT NULL, + last_accessed TEXT NOT NULL, + PRIMARY KEY (session_id), + FOREIGN KEY (user_id) REFERENCES users(user_id) +) +`, ` +-- Groups table +CREATE TABLE IF NOT EXISTS groups ( + group_id INTEGER PRIMARY KEY AUTOINCREMENT, + group_name TEXT UNIQUE NOT NULL, + description TEXT +) +`, ` +-- Roles table +CREATE TABLE IF NOT EXISTS roles ( + role_id INTEGER PRIMARY KEY AUTOINCREMENT, + role_name TEXT UNIQUE NOT NULL, + description TEXT +) +`, ` +-- Permissions table +CREATE TABLE IF NOT EXISTS permissions ( + permission_id INTEGER PRIMARY KEY AUTOINCREMENT, + permission_name TEXT UNIQUE NOT NULL, + description TEXT +) +`, ` +-- User-Group association table +CREATE TABLE IF NOT EXISTS user_groups ( + user_id INTEGER, + group_id INTEGER, + PRIMARY KEY (user_id, group_id), + FOREIGN KEY (user_id) REFERENCES users(user_id), + FOREIGN KEY (group_id) REFERENCES groups(group_id) +) +`, ` +-- User-Role association table +CREATE TABLE IF NOT EXISTS user_roles ( + user_id INTEGER, + role_id INTEGER, + PRIMARY KEY (user_id, role_id), + FOREIGN KEY (user_id) REFERENCES users(user_id), + FOREIGN KEY (role_id) REFERENCES roles(role_id) +) +`, ` +-- Group-Role association table +CREATE TABLE IF NOT EXISTS group_roles ( + group_id INTEGER, + role_id INTEGER, + PRIMARY KEY (group_id, role_id), + FOREIGN KEY (group_id) REFERENCES groups(group_id), + FOREIGN KEY (role_id) REFERENCES roles(role_id) +) +`, ` +-- Role-Permission association table +CREATE TABLE IF NOT EXISTS role_permissions ( + role_id INTEGER, + permission_id INTEGER, + PRIMARY KEY (role_id, permission_id), + FOREIGN KEY (role_id) REFERENCES roles(role_id), + FOREIGN KEY (permission_id) REFERENCES permissions(permission_id) +) +`, ` +-- Bags have names and access control settings +CREATE TABLE IF NOT EXISTS bags ( + bag_id INTEGER PRIMARY KEY AUTOINCREMENT, + bag_name TEXT UNIQUE NOT NULL, + accesscontrol TEXT NOT NULL, + description TEXT NOT NULL +) +`, ` +-- Recipes have names... +CREATE TABLE IF NOT EXISTS recipes ( + recipe_id INTEGER PRIMARY KEY AUTOINCREMENT, + recipe_name TEXT UNIQUE NOT NULL, + description TEXT NOT NULL, + owner_id INTEGER, + FOREIGN KEY (owner_id) REFERENCES users(user_id) +) +`, ` +-- ...and recipes also have an ordered list of bags +CREATE TABLE IF NOT EXISTS recipe_bags ( + recipe_id INTEGER NOT NULL, + bag_id INTEGER NOT NULL, + position INTEGER NOT NULL, + FOREIGN KEY (recipe_id) REFERENCES recipes(recipe_id) ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (bag_id) REFERENCES bags(bag_id) ON UPDATE CASCADE ON DELETE CASCADE, + UNIQUE (recipe_id, bag_id) +) +`, ` +-- Tiddlers are contained in bags and have titles +CREATE TABLE IF NOT EXISTS tiddlers ( + tiddler_id INTEGER PRIMARY KEY AUTOINCREMENT, + bag_id INTEGER NOT NULL, + title TEXT NOT NULL, + is_deleted BOOLEAN NOT NULL, + attachment_blob TEXT, -- null or the name of an attachment blob + FOREIGN KEY (bag_id) REFERENCES bags(bag_id) ON UPDATE CASCADE ON DELETE CASCADE, + UNIQUE (bag_id, title) +) +`, ` +-- Tiddlers also have unordered lists of fields, each of which has a name and associated value +CREATE TABLE IF NOT EXISTS fields ( + tiddler_id INTEGER, + field_name TEXT NOT NULL, + field_value TEXT NOT NULL, + FOREIGN KEY (tiddler_id) REFERENCES tiddlers(tiddler_id) ON UPDATE CASCADE ON DELETE CASCADE, + UNIQUE (tiddler_id, field_name) +) +`, ` +-- ACL table (using bag/recipe ids directly) +CREATE TABLE IF NOT EXISTS acl ( + acl_id INTEGER PRIMARY KEY AUTOINCREMENT, + entity_name TEXT NOT NULL, + entity_type TEXT NOT NULL CHECK (entity_type IN ('bag', 'recipe')), + role_id INTEGER, + permission_id INTEGER, + FOREIGN KEY (role_id) REFERENCES roles(role_id), + FOREIGN KEY (permission_id) REFERENCES permissions(permission_id) +) +`, ` +-- Indexes for performance (we can add more as needed based on query patterns) +CREATE INDEX IF NOT EXISTS idx_tiddlers_bag_id ON tiddlers(bag_id) +`, ` +CREATE INDEX IF NOT EXISTS idx_fields_tiddler_id ON fields(tiddler_id) +`, ` +CREATE INDEX IF NOT EXISTS idx_recipe_bags_recipe_id ON recipe_bags(recipe_id) +`, ` +CREATE INDEX IF NOT EXISTS idx_acl_entity_id ON acl(entity_name) +`]); +} + +async listBags() { + const rows = await this.engine.runStatementGetAll(` +SELECT bag_name, bag_id, accesscontrol, description +FROM bags +ORDER BY bag_name +`); + return rows; +} + +/* +Create or update a bag +Returns the bag_id of the bag +*/ +async createBag(bag_name, description, accesscontrol) { + accesscontrol = accesscontrol || ""; + // Run the queries + var bag = await this.engine.runStatement(` +INSERT OR IGNORE INTO bags (bag_name, accesscontrol, description) +VALUES ($bag_name, '', '') +`, { + $bag_name: bag_name + }); + const updateBags = await this.engine.runStatement(` +UPDATE bags +SET accesscontrol = $accesscontrol, +description = $description +WHERE bag_name = $bag_name +`, { + $bag_name: bag_name, + $accesscontrol: accesscontrol, + $description: description + }); + return updateBags.lastInsertRowid; +} + +/* +Returns array of {recipe_name:,recipe_id:,description:,bag_names: []} +*/ +async listRecipes() { + const rows = await this.engine.runStatementGetAll(` +SELECT r.recipe_name, r.recipe_id, r.description, r.owner_id, b.bag_name, rb.position +FROM recipes AS r +JOIN recipe_bags AS rb ON rb.recipe_id = r.recipe_id +JOIN bags AS b ON rb.bag_id = b.bag_id +ORDER BY r.recipe_name, rb.position +`); + const results = []; + let currentRecipeName = null, currentRecipeIndex = -1; + for (const row of rows) { + if (row.recipe_name !== currentRecipeName) { + currentRecipeName = row.recipe_name; + currentRecipeIndex += 1; + results.push({ + recipe_name: row.recipe_name, + recipe_id: row.recipe_id, + description: row.description, + owner_id: row.owner_id, + /** @type {string[]} */ + bag_names: [] + }); + } + results[currentRecipeIndex].bag_names.push(row.bag_name); + } + return results; +} + +/* +Create or update a recipe +Returns the recipe_id of the recipe +*/ +async createRecipe(recipe_name, bag_names, description) { + // Run the queries + await this.engine.runStatement(` +-- Delete existing recipe_bags entries for this recipe +DELETE FROM recipe_bags WHERE recipe_id = (SELECT recipe_id FROM recipes WHERE recipe_name = $recipe_name) +`, { + $recipe_name: recipe_name + }); + const updateRecipes = await this.engine.runStatement(` +-- Create the entry in the recipes table if required +INSERT OR REPLACE INTO recipes (recipe_name, description) +VALUES ($recipe_name, $description) +`, { + $recipe_name: recipe_name, + $description: description + }); + await this.engine.runStatement(` +INSERT INTO recipe_bags (recipe_id, bag_id, position) +SELECT r.recipe_id, b.bag_id, j.key as position +FROM recipes r +JOIN bags b +INNER JOIN json_each($bag_names) AS j ON j.value = b.bag_name +WHERE r.recipe_name = $recipe_name +`, { + $recipe_name: recipe_name, + $bag_names: JSON.stringify(bag_names) + }); + + return updateRecipes.lastInsertRowid; +} + +/* +Assign a recipe to a user +*/ +async assignRecipeToUser(recipe_name, user_id) { + await this.engine.runStatement(` +UPDATE recipes SET owner_id = $user_id WHERE recipe_name = $recipe_name +`, { + $recipe_name: recipe_name, + $user_id: user_id + }); +} + +/* +Returns {tiddler_id:} +*/ +async saveBagTiddler(tiddlerFields, bag_name, attachment_blob) { + attachment_blob = attachment_blob || null; + // Update the tiddlers table + var info = await this.engine.runStatement(` +INSERT OR REPLACE INTO tiddlers (bag_id, title, is_deleted, attachment_blob) +VALUES ( + (SELECT bag_id FROM bags WHERE bag_name = $bag_name), + $title, + FALSE, + $attachment_blob +) +`, { + $title: tiddlerFields.title, + $attachment_blob: attachment_blob, + $bag_name: bag_name + }); + // Update the fields table + await this.engine.runStatement(` +INSERT OR REPLACE INTO fields (tiddler_id, field_name, field_value) +SELECT + t.tiddler_id, + json_each.key AS field_name, + json_each.value AS field_value +FROM ( + SELECT tiddler_id + FROM tiddlers + WHERE bag_id = ( + SELECT bag_id + FROM bags + WHERE bag_name = $bag_name + ) AND title = $title +) AS t +JOIN json_each($field_values) AS json_each +`, { + $title: tiddlerFields.title, + $bag_name: bag_name, + $field_values: JSON.stringify(Object.assign({}, tiddlerFields, { title: undefined })) + }); + return { + tiddler_id: info.lastInsertRowid + }; +} + +/* +Returns {tiddler_id:,bag_name:} or null if the recipe is empty +*/ +async saveRecipeTiddler(tiddlerFields, recipe_name, attachment_blob) { + // Find the topmost bag in the recipe + var row = await this.engine.runStatementGet(` +SELECT b.bag_name +FROM bags AS b +JOIN ( + SELECT rb.bag_id + FROM recipe_bags AS rb + WHERE rb.recipe_id = ( + SELECT recipe_id + FROM recipes + WHERE recipe_name = $recipe_name + ) + ORDER BY rb.position DESC + LIMIT 1 +) AS selected_bag +ON b.bag_id = selected_bag.bag_id +`, { + $recipe_name: recipe_name + }); + if (!row) { + return null; + } + // Save the tiddler to the topmost bag + var info = await this.saveBagTiddler(tiddlerFields, row.bag_name, attachment_blob); + return { + tiddler_id: info.tiddler_id, + bag_name: row.bag_name + }; +} + +/* +Returns {tiddler_id:} of the delete marker +*/ +async deleteTiddler(title, bag_name) { + // Delete the fields of this tiddler + await this.engine.runStatement(` +DELETE FROM fields +WHERE tiddler_id IN ( + SELECT t.tiddler_id + FROM tiddlers AS t + INNER JOIN bags AS b ON t.bag_id = b.bag_id + WHERE b.bag_name = $bag_name AND t.title = $title +) +`, { + $title: title, + $bag_name: bag_name + }); + // Mark the tiddler itself as deleted + const rowDeleteMarker = await this.engine.runStatement(` +INSERT OR REPLACE INTO tiddlers (bag_id, title, is_deleted, attachment_blob) +VALUES ( + (SELECT bag_id FROM bags WHERE bag_name = $bag_name), + $title, + TRUE, + NULL +) +`, { + $title: title, + $bag_name: bag_name + }); + return { tiddler_id: rowDeleteMarker.lastInsertRowid }; +} + +/* +returns {tiddler_id:,tiddler:,attachment_blob:} +*/ +async getBagTiddler(title, bag_name) { + const rowTiddler = await this.engine.runStatementGet(` +SELECT t.tiddler_id, t.attachment_blob +FROM bags AS b +INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id +WHERE t.title = $title AND b.bag_name = $bag_name AND t.is_deleted = FALSE +`, { + $title: title, + $bag_name: bag_name + }); + if (!rowTiddler) { + return null; + } + const rows = await this.engine.runStatementGetAll(` +SELECT field_name, field_value, tiddler_id +FROM fields +WHERE tiddler_id = $tiddler_id +`, { + $tiddler_id: rowTiddler.tiddler_id + }); + if (rows.length === 0) { + return null; + } else { + return { + tiddler_id: rows[0].tiddler_id, + attachment_blob: rowTiddler.attachment_blob, + tiddler: rows.reduce((accumulator, value) => { + accumulator[value["field_name"]] = value.field_value; + return accumulator; + }, { title: title }) + }; + } +} + +/* +Returns {bag_name:, tiddler: {fields}, tiddler_id:, attachment_blob:} +*/ +async getRecipeTiddler(title, recipe_name) { + const rowTiddlerId = await this.engine.runStatementGet(` +SELECT t.tiddler_id, t.attachment_blob, b.bag_name +FROM bags AS b +INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id +INNER JOIN recipes AS r ON rb.recipe_id = r.recipe_id +INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id +WHERE r.recipe_name = $recipe_name +AND t.title = $title +AND t.is_deleted = FALSE +ORDER BY rb.position DESC +LIMIT 1 +`, { + $title: title, + $recipe_name: recipe_name + }); + if (!rowTiddlerId) { + return null; + } + // Get the fields + const rows = await this.engine.runStatementGetAll(` +SELECT field_name, field_value +FROM fields +WHERE tiddler_id = $tiddler_id +`, { + $tiddler_id: rowTiddlerId.tiddler_id + }); + return { + bag_name: rowTiddlerId.bag_name, + tiddler_id: rowTiddlerId.tiddler_id, + attachment_blob: rowTiddlerId.attachment_blob, + tiddler: rows.reduce((accumulator, value) => { + accumulator[value["field_name"]] = value.field_value; + return accumulator; + }, { title: title }) + }; +} + +/** + * Checks if a user has permission to access a recipe + * @param {number | null | undefined} userId + * @param {string} recipeName + * @param {string} permissionName + */ +async hasRecipePermission(userId, recipeName, permissionName) { + try { + // check if the user is the owner of the entity + const recipe = await this.engine.runStatementGet(` + SELECT owner_id + FROM recipes + WHERE recipe_name = $recipe_name + `, { + $recipe_name: recipeName + }); + + if (recipe && !!recipe.owner_id && recipe.owner_id === userId) { + return true; + } else { + var permission = await this.checkACLPermission(userId, "recipe", recipeName, permissionName, recipe && recipe.owner_id); + return permission; + } + + } catch (error) { + console.error(error); + return false; + } +} + +/* +Checks if a user has permission to access a bag +*/ +async hasBagPermission(userId, bagName, permissionName) { + return await this.checkACLPermission(userId, "bag", bagName, permissionName); +} + +async getACLByName(entityType, entityName, fetchAll) { + const entityInfo = this.entityTypeToTableMap[entityType]; + if (!entityInfo) { + throw new Error("Invalid entity type: " + entityType); + } + + // First, check if there's an ACL record for the entity and get the permission_id + var checkACLExistsQuery = ` +SELECT acl.*, permissions.permission_name +FROM acl +LEFT JOIN permissions ON acl.permission_id = permissions.permission_id +WHERE acl.entity_type = $entity_type +AND acl.entity_name = $entity_name +`; + + if (!fetchAll) { + checkACLExistsQuery += " LIMIT 1"; + } + + const aclRecord = await this.engine[fetchAll ? "runStatementGetAll" : "runStatementGet"](checkACLExistsQuery, { + $entity_type: entityType, + $entity_name: entityName + }); + + return aclRecord; +} + +async checkACLPermission(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 = await this.getACLByName(entityType, entityName, true); + const aclRecord = aclRecords.find(record => record.permission_name === permissionName); + + // If no ACL record exists, return true for hasPermission + if ((!aclRecord && !ownerId && aclRecords.length === 0) || ((!!aclRecord && !!ownerId) && ownerId === userId)) { + return true; + } + + // If ACL record exists, check for user permission using the retrieved permission_id + const checkPermissionQuery = ` + SELECT * + FROM users u + JOIN user_roles ur ON u.user_id = ur.user_id + JOIN roles r ON ur.role_id = r.role_id + JOIN acl a ON r.role_id = a.role_id + WHERE u.user_id = $user_id + AND a.entity_type = $entity_type + AND a.entity_name = $entity_name + AND a.permission_id = $permission_id + LIMIT 1 +`; + + const result = await this.engine.runStatementGet(checkPermissionQuery, { + $user_id: userId, + $entity_type: entityType, + $entity_name: entityName, + $permission_id: aclRecord && aclRecord.permission_id + }); + + let hasPermission = result !== undefined; + + return hasPermission; + + } catch (error) { + console.error(error); + return false; + } +} + +/** + * Returns the ACL records for an entity (bag or recipe) + */ +async getEntityAclRecords(entityName) { + const checkACLExistsQuery = ` +SELECT * +FROM acl +WHERE entity_name = $entity_name +`; + + const aclRecords = await this.engine.runStatementGetAll(checkACLExistsQuery, { + $entity_name: entityName + }); + + return aclRecords; +} + +/* +Get the entity by name +*/ +async getEntityByName(entityType, entityName) { + const entityInfo = this.entityTypeToTableMap[entityType]; + if (entityInfo) { + return await this.engine.runStatementGet(`SELECT * FROM ${entityInfo.table} WHERE ${entityInfo.column} = $entity_name`, { + $entity_name: entityName + }); + } + return null; +} + +/* +Get the titles of the tiddlers in a bag. Returns an empty array for bags that do not exist +*/ +async getBagTiddlers(bag_name) { + const rows = await this.engine.runStatementGetAll(` +SELECT DISTINCT title, tiddler_id +FROM tiddlers +WHERE bag_id IN ( + SELECT bag_id + FROM bags + WHERE bag_name = $bag_name +) +AND tiddlers.is_deleted = FALSE +ORDER BY title ASC +`, { + $bag_name: bag_name + }); + return rows; +} + +/* +Get the tiddler_id of the newest tiddler in a bag. Returns null for bags that do not exist +*/ +async getBagLastTiddlerId(bag_name) { + const row = await this.engine.runStatementGet(` +SELECT tiddler_id +FROM tiddlers +WHERE bag_id IN ( + SELECT bag_id + FROM bags + WHERE bag_name = $bag_name +) +ORDER BY tiddler_id DESC +LIMIT 1 +`, { + $bag_name: bag_name + }); + if (row) { + return row.tiddler_id; + } else { + return null; + } +} + +/* +Get the metadata of the tiddlers in a recipe as an array [{title:,tiddler_id:,bag_name:,is_deleted:}], +sorted in ascending order of tiddler_id. + +Options include: + +limit: optional maximum number of results to return +last_known_tiddler_id: tiddler_id of the last known update. Only returns tiddlers that have been created, modified or deleted since +include_deleted: boolean, defaults to false + +Returns null for recipes that do not exist +*/ +async getRecipeTiddlers(recipe_name, options) { + options = options || {}; + // Get the recipe ID + const rowsCheckRecipe = await this.engine.runStatementGet(` +SELECT recipe_id FROM recipes WHERE recipes.recipe_name = $recipe_name +`, { + $recipe_name: recipe_name + }); + if (!rowsCheckRecipe) { + return null; + } + const recipe_id = rowsCheckRecipe.recipe_id; + // Compose the query to get the tiddlers + const params = { + $recipe_id: recipe_id + }; + if (options.limit) { + params.$limit = options.limit.toString(); + } + if (options.last_known_tiddler_id) { + params.$last_known_tiddler_id = options.last_known_tiddler_id; + } + const rows = await this.engine.runStatementGetAll(` +SELECT title, tiddler_id, is_deleted, bag_name +FROM ( + SELECT t.title, t.tiddler_id, t.is_deleted, b.bag_name, MAX(rb.position) AS position + FROM bags AS b + INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id + INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id + WHERE rb.recipe_id = $recipe_id + ${options.include_deleted ? "" : "AND t.is_deleted = FALSE"} + ${options.last_known_tiddler_id ? "AND tiddler_id > $last_known_tiddler_id" : ""} + GROUP BY t.title + ORDER BY t.title, tiddler_id DESC + ${options.limit ? "LIMIT $limit" : ""} +) +`, params); + return rows; +} + +/* +Get the tiddler_id of the newest tiddler in a recipe. Returns null for recipes that do not exist +*/ +async getRecipeLastTiddlerId(recipe_name) { + const row = await this.engine.runStatementGet(` +SELECT t.title, t.tiddler_id, b.bag_name, MAX(rb.position) AS position +FROM bags AS b +INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id +INNER JOIN recipes AS r ON rb.recipe_id = r.recipe_id +INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id +WHERE r.recipe_name = $recipe_name +GROUP BY t.title +ORDER BY t.tiddler_id DESC +LIMIT 1 +`, { + $recipe_name: recipe_name + }); + if (row) { + return row.tiddler_id; + } else { + return null; + } +} + +async deleteAllTiddlersInBag(bag_name) { + // Delete the fields + await this.engine.runStatement(` +DELETE FROM fields +WHERE tiddler_id IN ( + SELECT tiddler_id + FROM tiddlers + WHERE bag_id = (SELECT bag_id FROM bags WHERE bag_name = $bag_name) + AND is_deleted = FALSE +) +`, { + $bag_name: bag_name + }); + // Mark the tiddlers as deleted + await this.engine.runStatement(` +UPDATE tiddlers +SET is_deleted = TRUE +WHERE bag_id = (SELECT bag_id FROM bags WHERE bag_name = $bag_name) +AND is_deleted = FALSE +`, { + $bag_name: bag_name + }); +} + +/* +Get the names of the bags in a recipe. Returns an empty array for recipes that do not exist +*/ +async getRecipeBags(recipe_name) { + const rows = await this.engine.runStatementGetAll(` +SELECT bags.bag_name +FROM bags +JOIN ( + SELECT rb.bag_id, rb.position as position + FROM recipe_bags AS rb + JOIN recipes AS r ON rb.recipe_id = r.recipe_id + WHERE r.recipe_name = $recipe_name + ORDER BY rb.position +) AS bag_priority ON bags.bag_id = bag_priority.bag_id +ORDER BY position +`, { + $recipe_name: recipe_name + }); + return rows.map(value => value.bag_name); +} + +/* +Get the attachment value of a bag, if any exist +*/ +async getBagTiddlerAttachmentBlob(title, bag_name) { + const row = await this.engine.runStatementGet(` +SELECT t.attachment_blob +FROM bags AS b +INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id +WHERE t.title = $title AND b.bag_name = $bag_name AND t.is_deleted = FALSE +`, { + $title: title, + $bag_name: bag_name + }); + return row ? row.attachment_blob : null; +} + +/* +Get the attachment value of a recipe, if any exist +*/ +async getRecipeTiddlerAttachmentBlob(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 +INNER JOIN recipes AS r ON rb.recipe_id = r.recipe_id +INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id +WHERE r.recipe_name = $recipe_name AND t.title = $title AND t.is_deleted = FALSE +ORDER BY rb.position DESC +LIMIT 1 +`, { + $title: title, + $recipe_name: recipe_name + }); + return row ? row.attachment_blob : null; +} + +// User CRUD operations +async createUser(username, email, password) { + const result = await this.engine.runStatement(` + INSERT INTO users (username, email, password) + VALUES ($username, $email, $password) +`, { + $username: username, + $email: email, + $password: password + }); + return result.lastInsertRowid; +} + +async getUser(userId) { + return await this.engine.runStatementGet(` + SELECT * FROM users WHERE user_id = $userId +`, { + $userId: userId + }); +} + +async getUserByUsername(username) { + return await this.engine.runStatementGet(` + SELECT * FROM users WHERE username = $username +`, { + $username: username + }); +} + +async getUserByEmail(email) { + return await this.engine.runStatementGet(` + SELECT * FROM users WHERE email = $email +`, { + $email: email + }); +} + +async listUsersByRoleId(roleId) { + return await this.engine.runStatementGetAll(` + SELECT u.* + FROM users u + JOIN user_roles ur ON u.user_id = ur.user_id + WHERE ur.role_id = $roleId + ORDER BY u.username +`, { + $roleId: roleId + }); +} + +async updateUser(userId, username, email, roleId) { + const existingUser = await this.engine.runStatementGet(` +SELECT user_id FROM users +WHERE email = $email AND user_id != $userId +`, { + $email: email, + $userId: userId + }); + + if (existingUser.length > 0) { + return { + success: false, + message: "Email address already in use by another user." + }; + } + + try { + await this.engine.transaction(async () => { + // Update user information + await this.engine.runStatement(` + UPDATE users + SET username = $username, email = $email + WHERE user_id = $userId + `, { + $userId: userId, + $username: username, + $email: email + }); + + if (roleId) { + // Remove all existing roles for the user + await this.engine.runStatement(` + DELETE FROM user_roles + WHERE user_id = $userId + `, { + $userId: userId + }); + + // Add the new role + await this.engine.runStatement(` + INSERT INTO user_roles (user_id, role_id) + VALUES ($userId, $roleId) + `, { + $userId: userId, + $roleId: roleId + }); + } + }); + + return { + success: true, + message: "User profile and role updated successfully." + }; + } catch (error) { + return { + success: false, + message: "Failed to update user profile: " + error.message + }; + } +} + +async updateUserPassword(userId, newHash) { + try { + await this.engine.runStatement(` + UPDATE users + SET password = $newHash + WHERE user_id = $userId +`, { + $userId: userId, + $newHash: newHash, + }); + + return { + success: true, + message: "Password updated successfully." + }; + } catch (error) { + return { + success: false, + message: "Failed to update password: " + error.message + }; + } +} + +async deleteUser(userId) { + await this.engine.runStatement(` + DELETE FROM users WHERE user_id = $userId +`, { + $userId: userId + }); +} + +async listUsers() { + return await this.engine.runStatementGetAll(` + SELECT * FROM users ORDER BY username +`); +} + +async createOrUpdateUserSession(userId, sessionId) { + const currentTimestamp = new Date().toISOString(); + + // First, try to update an existing session + const updateResult = await this.engine.runStatement(` + UPDATE sessions + SET session_id = $sessionId, last_accessed = $timestamp + WHERE user_id = $userId +`, { + $userId: userId, + $sessionId: sessionId, + $timestamp: currentTimestamp + }); + + // If no existing session was updated, create a new one + if (updateResult.changes === 0) { + await this.engine.runStatement(` + INSERT INTO sessions (user_id, session_id, created_at, last_accessed) + VALUES ($userId, $sessionId, $timestamp, $timestamp) + `, { + $userId: userId, + $sessionId: sessionId, + $timestamp: currentTimestamp + }); + } + + return sessionId; +} + +async createUserSession(userId, sessionId) { + const currentTimestamp = new Date().toISOString(); + await this.engine.runStatement(` + INSERT INTO sessions (user_id, session_id, created_at, last_accessed) + VALUES ($userId, $sessionId, $timestamp, $timestamp) +`, { + $userId: userId, + $sessionId: sessionId, + $timestamp: currentTimestamp + }); + + return sessionId; +} + +/** + * @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} + */ +async findUserBySessionId(sessionId) { + // First, get the user_id from the sessions table + const sessionResult = await this.engine.runStatementGet(` + SELECT user_id, last_accessed + FROM sessions + WHERE session_id = $sessionId +`, { + $sessionId: sessionId + }); + + if (!sessionResult) { + return null; // Session not found + } + + const lastAccessed = new Date(sessionResult.last_accessed); + const expirationTime = 24 * 60 * 60 * 1000; // 24 hours in milliseconds + if (+new Date() - +lastAccessed > expirationTime) { + // Session has expired + await this.deleteSession(sessionId); + return null; + } + + // Update the last_accessed timestamp + const currentTimestamp = new Date().toISOString(); + await this.engine.runStatement(` + UPDATE sessions + SET last_accessed = $timestamp + WHERE session_id = $sessionId +`, { + $sessionId: sessionId, + $timestamp: currentTimestamp + }); + + /** @type {any} */ + const userResult = await this.engine.runStatementGet(` + SELECT * + FROM users + WHERE user_id = $userId +`, { + $userId: sessionResult.user_id + }); + + if (!userResult) { + return null; + } + + /** @type {User} */ + return userResult; +} + +async deleteSession(sessionId) { + await this.engine.runStatement(` + DELETE FROM sessions + WHERE session_id = $sessionId +`, { + $sessionId: sessionId + }); +} + +async deleteUserSessions(userId) { + await this.engine.runStatement(` + DELETE FROM sessions + WHERE user_id = $userId +`, { + $userId: userId + }); +} + +// Set the user as an admin +async setUserAdmin(userId) { + var admin = await this.getRoleByName("ADMIN"); + if (admin) { + await this.addRoleToUser(userId, admin.role_id); + } +} + +// Group CRUD operations +async createGroup(groupName, description) { + const result = await this.engine.runStatement(` + INSERT INTO groups (group_name, description) + VALUES ($groupName, $description) +`, { + $groupName: groupName, + $description: description + }); + return result.lastInsertRowid; +} + +async getGroup(groupId) { + return await this.engine.runStatementGet(` + SELECT * FROM groups WHERE group_id = $groupId +`, { + $groupId: groupId + }); +} + +async updateGroup(groupId, groupName, description) { + await this.engine.runStatement(` + UPDATE groups + SET group_name = $groupName, description = $description + WHERE group_id = $groupId +`, { + $groupId: groupId, + $groupName: groupName, + $description: description + }); +} + +async deleteGroup(groupId) { + await this.engine.runStatement(` + DELETE FROM groups WHERE group_id = $groupId +`, { + $groupId: groupId + }); +} + +async listGroups() { + return await this.engine.runStatementGetAll(` + SELECT * FROM groups ORDER BY group_name +`); +} + +// Role CRUD operations +async createRole(roleName, description) { + const result = await this.engine.runStatement(` + INSERT OR IGNORE INTO roles (role_name, description) + VALUES ($roleName, $description) +`, { + $roleName: roleName, + $description: description + }); + return result.lastInsertRowid; +} + +async getRole(roleId) { + return await this.engine.runStatementGet(` + SELECT * FROM roles WHERE role_id = $roleId +`, { + $roleId: roleId + }); +} + +async getRoleByName(roleName) { + return await this.engine.runStatementGet(` + SELECT * FROM roles WHERE role_name = $roleName +`, { + $roleName: roleName + }); +} + +async updateRole(roleId, roleName, description) { + await this.engine.runStatement(` + UPDATE roles + SET role_name = $roleName, description = $description + WHERE role_id = $roleId +`, { + $roleId: roleId, + $roleName: roleName, + $description: description + }); +} + +async deleteRole(roleId) { + await this.engine.runStatement(` + DELETE FROM roles WHERE role_id = $roleId +`, { + $roleId: roleId + }); +} + +async listRoles() { + return await this.engine.runStatementGetAll(` + SELECT * FROM roles ORDER BY role_name DESC +`); +} + +// Permission CRUD operations +async createPermission(permissionName, description) { + const result = await this.engine.runStatement(` +INSERT OR IGNORE INTO permissions (permission_name, description) +VALUES ($permissionName, $description) +`, { + $permissionName: permissionName, + $description: description + }); + return result.lastInsertRowid; +} + +async getPermission(permissionId) { + return await this.engine.runStatementGet(` + SELECT * FROM permissions WHERE permission_id = $permissionId +`, { + $permissionId: permissionId + }); +} + +async getPermissionByName(permissionName) { + return await this.engine.runStatementGet(` + SELECT * FROM permissions WHERE permission_name = $permissionName +`, { + $permissionName: permissionName + }); +} + +async updatePermission(permissionId, permissionName, description) { + await this.engine.runStatement(` + UPDATE permissions + SET permission_name = $permissionName, description = $description + WHERE permission_id = $permissionId +`, { + $permissionId: permissionId, + $permissionName: permissionName, + $description: description + }); +} + +async deletePermission(permissionId) { + await this.engine.runStatement(` + DELETE FROM permissions WHERE permission_id = $permissionId +`, { + $permissionId: permissionId + }); +} + +async listPermissions() { + return await this.engine.runStatementGetAll(` + SELECT * FROM permissions ORDER BY permission_name +`); +} + +// ACL CRUD operations +async createACL(entityName, entityType, roleId, permissionId) { + if (!entityName.startsWith("$:/")) { + const result = await this.engine.runStatement(` + INSERT OR IGNORE INTO acl (entity_name, entity_type, role_id, permission_id) + VALUES ($entityName, $entityType, $roleId, $permissionId) +`, + { + $entityName: entityName, + $entityType: entityType, + $roleId: roleId, + $permissionId: permissionId + }); + return result.lastInsertRowid; + } +} + +async getACL(aclId) { + return await this.engine.runStatementGet(` + SELECT * FROM acl WHERE acl_id = $aclId +`, { + $aclId: aclId + }); +} + +async updateACL(aclId, entityId, entityType, roleId, permissionId) { + await this.engine.runStatement(` + UPDATE acl + SET entity_name = $entityId, entity_type = $entityType, + role_id = $roleId, permission_id = $permissionId + WHERE acl_id = $aclId +`, { + $aclId: aclId, + $entityId: entityId, + $entityType: entityType, + $roleId: roleId, + $permissionId: permissionId + }); +} + +async deleteACL(aclId) { + await this.engine.runStatement(` + DELETE FROM acl WHERE acl_id = $aclId +`, { + $aclId: aclId + }); +} + +async listACLs() { + return await this.engine.runStatementGetAll(` + SELECT * FROM acl ORDER BY entity_type, entity_name +`); +} + +// Association management functions +async addUserToGroup(userId, groupId) { + await this.engine.runStatement(` + INSERT OR IGNORE INTO user_groups (user_id, group_id) + VALUES ($userId, $groupId) +`, { + $userId: userId, + $groupId: groupId + }); +} + +async isUserInGroup(userId, groupId) { + const result = await this.engine.runStatementGet(` + SELECT 1 FROM user_groups + WHERE user_id = $userId AND group_id = $groupId +`, { + $userId: userId, + $groupId: groupId + }); + return result !== undefined; +} + +async removeUserFromGroup(userId, groupId) { + await this.engine.runStatement(` + DELETE FROM user_groups + WHERE user_id = $userId AND group_id = $groupId +`, { + $userId: userId, + $groupId: groupId + }); +} + +async addRoleToUser(userId, roleId) { + await this.engine.runStatement(` + INSERT OR IGNORE INTO user_roles (user_id, role_id) + VALUES ($userId, $roleId) +`, { + $userId: userId, + $roleId: roleId + }); +} + +async removeRoleFromUser(userId, roleId) { + await this.engine.runStatement(` + DELETE FROM user_roles + WHERE user_id = $userId AND role_id = $roleId +`, { + $userId: userId, + $roleId: roleId + }); +} + +async addRoleToGroup(groupId, roleId) { + await this.engine.runStatement(` + INSERT OR IGNORE INTO group_roles (group_id, role_id) + VALUES ($groupId, $roleId) +`, { + $groupId: groupId, + $roleId: roleId + }); +} + +async removeRoleFromGroup(groupId, roleId) { + await this.engine.runStatement(` + DELETE FROM group_roles + WHERE group_id = $groupId AND role_id = $roleId +`, { + $groupId: groupId, + $roleId: roleId + }); +} + +async addPermissionToRole(roleId, permissionId) { + await this.engine.runStatement(` + INSERT OR IGNORE INTO role_permissions (role_id, permission_id) + VALUES ($roleId, $permissionId) +`, { + $roleId: roleId, + $permissionId: permissionId + }); +} + +async removePermissionFromRole(roleId, permissionId) { + await this.engine.runStatement(` + DELETE FROM role_permissions + WHERE role_id = $roleId AND permission_id = $permissionId +`, { + $roleId: roleId, + $permissionId: permissionId + }); +} + +async getUserRoles(userId) { + const query = ` + SELECT r.role_id, r.role_name + FROM user_roles ur + JOIN roles r ON ur.role_id = r.role_id + WHERE ur.user_id = $userId + LIMIT 1 +`; + + return await this.engine.runStatementGet(query, { $userId: userId }); +} + +async deleteUserRolesByRoleId(roleId) { + await this.engine.runStatement(` + DELETE FROM user_roles + WHERE role_id = $roleId +`, { + $roleId: roleId + }); +} + +async deleteUserRolesByUserId(userId) { + await this.engine.runStatement(` + DELETE FROM user_roles + WHERE user_id = $userId +`, { + $userId: userId + }); +} + +async isRoleInUse(roleId) { + // Check if the role is assigned to any users + const userRoleCheck = await this.engine.runStatementGet(` +SELECT 1 +FROM user_roles +WHERE role_id = $roleId +LIMIT 1 +`, { + $roleId: roleId + }); + + if (userRoleCheck) { + return true; + } + + // Check if the role is used in any ACLs + const aclRoleCheck = await this.engine.runStatementGet(` +SELECT 1 +FROM acl +WHERE role_id = $roleId +LIMIT 1 +`, { + $roleId: roleId + }); + + if (aclRoleCheck) { + return true; + } + + // If we've reached this point, the role is not in use + return false; +} + +async getRoleById(roleId) { + const role = await this.engine.runStatementGet(` +SELECT role_id, role_name, description +FROM roles +WHERE role_id = $roleId +`, { + $roleId: roleId + }); + + return role; +} +} + +exports.SqlTiddlerDatabase = SqlTiddlerDatabase; })(); 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 1eec51bbc8d..8821522c0aa 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/tests-sql-tiddler-database.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/tests-sql-tiddler-database.js @@ -15,14 +15,17 @@ if($tw.node) { "use strict"; describe("SQL tiddler database with node built-in sqlite", function () { + // eslint-disable-next-line custom-rules/always-await void runSqlDatabaseTests("node").catch(console.error); }); describe("SQL tiddler database with node-sqlite3-wasm", function () { + // eslint-disable-next-line custom-rules/always-await void runSqlDatabaseTests("wasm").catch(console.error); }); describe("SQL tiddler database with better-sqlite3", function () { + // eslint-disable-next-line custom-rules/always-await void runSqlDatabaseTests("better").catch(console.error); }); From 0794b0a8ca50c3245c1d247093b9c6e0c108eac8 Mon Sep 17 00:00:00 2001 From: arlen22 Date: Thu, 9 Jan 2025 18:38:29 -0500 Subject: [PATCH 10/14] slightly complicated logical trees --- .../modules/routes/handlers/get-acl.js | 15 +++- .../modules/routes/handlers/get-index.js | 74 ++++++++++++++++--- 2 files changed, 76 insertions(+), 13 deletions(-) diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-acl.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-acl.js index 4cddbe6f39a..3494bdcdac6 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-acl.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-acl.js @@ -39,10 +39,17 @@ exports.handler = async function (request, response, state) { 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 && !await 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 diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-index.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-index.js index 9984fbeece0..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 @@ -31,21 +32,43 @@ exports.handler = async function(request,response,state) { "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 = async 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; +} }()); From dd94354f70c0fa285ebd459e855a5aada70405fe Mon Sep 17 00:00:00 2001 From: arlen22 Date: Fri, 10 Jan 2025 12:04:42 -0500 Subject: [PATCH 11/14] cleanup formatting --- package-lock.json | 85 +- package.json | 6 +- .../multiwikiserver/eslint.config.js | 747 ++--- .../multiwikiserver/modules/mws-server.js | 14 +- .../multiwikiserver/modules/startup.js | 2 +- .../modules/store/sql-engine.js | 130 +- .../modules/store/sql-tiddler-database.js | 2713 ++++++++--------- .../modules/store/sql-tiddler-store.js | 327 +- .../store/tests-sql-tiddler-database.js | 3 - 9 files changed, 2026 insertions(+), 2001 deletions(-) diff --git a/package-lock.json b/package-lock.json index a4518f68bab..983925266e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,10 +16,10 @@ "tiddlywiki": "tiddlywiki.js" }, "devDependencies": { - "@eslint/js": "^9.17.0", + "@eslint/js": "^9.12.0", "@playwright/test": "^1.47.2", "@types/jest": "^29.5.14", - "eslint": "^9.17.0", + "eslint": "^9.12.0", "playwright": "^1.47.2", "typescript": "^5.7.2", "typescript-eslint": "^8.19.1" @@ -91,12 +91,12 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", - "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", "dev": true, "dependencies": { - "@eslint/object-schema": "^2.1.5", + "@eslint/object-schema": "^2.1.4", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -105,13 +105,10 @@ } }, "node_modules/@eslint/core": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz", - "integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz", + "integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==", "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.15" - }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -140,9 +137,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz", - "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==", + "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" @@ -191,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", @@ -218,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" @@ -921,31 +905,31 @@ } }, "node_modules/eslint": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz", - "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==", + "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/config-array": "^0.19.0", - "@eslint/core": "^0.9.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.17.0", - "@eslint/plugin-kit": "^0.2.3", - "@humanfs/node": "^0.16.6", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/core": "^0.6.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.12.0", + "@eslint/plugin-kit": "^0.2.0", + "@humanfs/node": "^0.16.5", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", + "@humanwhocodes/retry": "^0.3.1", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", + "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", @@ -959,7 +943,8 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3" + "optionator": "^0.9.3", + "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" @@ -2119,6 +2104,12 @@ "node": ">=6" } }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "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", diff --git a/package.json b/package.json index d686d1ec27a..fb1f19fe76a 100644 --- a/package.json +++ b/package.json @@ -24,10 +24,10 @@ "wiki" ], "devDependencies": { - "@eslint/js": "^9.17.0", + "@eslint/js": "^9.12.0", "@playwright/test": "^1.47.2", "@types/jest": "^29.5.14", - "eslint": "^9.17.0", + "eslint": "^9.12.0", "playwright": "^1.47.2", "typescript": "^5.7.2", "typescript-eslint": "^8.19.1" @@ -37,7 +37,7 @@ "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 index 84b85572672..3d7a02d32bc 100644 --- a/plugins/tiddlywiki/multiwikiserver/eslint.config.js +++ b/plugins/tiddlywiki/multiwikiserver/eslint.config.js @@ -1,384 +1,425 @@ //@ts-check const globals = require("globals"); -const js = require("@eslint/js"); -const ts = require("typescript-eslint"); +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: 'problem', - messages: { - expression: 'Expected non-Promise value or awaited Promise in an expression.', - }, - }, - create(context) { - const services = utils_1.ESLintUtils.getParserServices(context); - const checker = services.program.getTypeChecker(); - const checks = new Set(["AwaitExpression", "VoidExpression"]) - return { - ":expression"(node) { - if (checks.has(node.type)) return; - if (checks.has(node.parent.type)) return; - const tsNode = services.esTreeNodeToTSNodeMap.get(node); - if (isSometimesThenable(checker, tsNode)) { - context.report({ node: node, messageId: 'expression' }); - } - }, - }; - function isSometimesThenable(checker, tsNode) { - const type = checker.getTypeAtLocation(tsNode); - for (const subType of tsutils.unionTypeParts(checker.getApparentType(type))) { - if (tsutils.isThenableType(checker, tsNode, subType)) { - return true; - } - } - return false; - } - }, + 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 = ts.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/", - ] - }, - js.configs.recommended, - ts.configs.base, - { plugins: { "custom-rules": { rules: { "always-await": AlwaysAwaitRule } } } }, - { - languageOptions: { - globals: { - ...globals.browser, - ...globals.commonjs, - ...globals.node, - // $tw: "writable", // temporary - }, +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", - }, + 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"], + 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, - }], + "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", + "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: "off", - "indent-legacy": "off", - "init-declarations": "off", - "jsx-quotes": "error", - "key-spacing": "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": "off", + // "indent": ["warn", "tab", { + // "outerIIFEBody": 0 + // }], + "indent-legacy": "off", + "init-declarations": "off", + "jsx-quotes": "error", + "key-spacing": "off", - // "keyword-spacing": ["error", { - // before: true, - // after: false, + // "keyword-spacing": ["error", { + // before: true, + // after: false, - // overrides: { - // case: { - // after: true, - // }, + // overrides: { + // case: { + // after: true, + // }, - // do: { - // after: true, - // }, + // do: { + // after: true, + // }, - // else: { - // after: true, - // }, + // else: { + // after: true, + // }, - // return: { - // after: true, - // }, + // return: { + // after: true, + // }, - // throw: { - // after: true, - // }, + // throw: { + // after: true, + // }, - // try: { - // 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", + "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-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-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-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", + "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, - // }], + // 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", + 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, - }], + "valid-typeof": ["error", { + requireStringLiterals: false, + }], - "vars-on-top": "off", - "wrap-iife": "off", - "wrap-regex": "off", - "yield-star-spacing": "error", - yoda: "off", + "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", + // 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", { - allowAny: true, - allowedPromiseNames: [], - checkArrowFunctions: true, - checkFunctionDeclarations: true, - checkFunctionExpressions: true, - checkMethodDeclarations: true, - }], - "custom-rules/always-await": "error", - "arrow-body-style": "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/modules/mws-server.js b/plugins/tiddlywiki/multiwikiserver/modules/mws-server.js index 55c8e4f34b2..94efb1bdcff 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/mws-server.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/mws-server.js @@ -20,6 +20,7 @@ if($tw.node) { querystring = require("querystring"), crypto = require("crypto"), zlib = require("zlib"), + {ok} = require("assert"), aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/routes/helpers/acl-middleware.js").middleware; } @@ -354,7 +355,11 @@ Server.prototype.defaultVariables = { Server.prototype.get = function(name) { return this.variables[name]; }; - +/** + * + * @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); @@ -602,6 +607,7 @@ Server.prototype.requestHandler = async function(request,response,options) { data += chunk.toString(); }); request.on("end", function () { + ok(route); if (route.bodyFormat === "www-form-urlencoded") { data = queryString.parse(data); } @@ -637,8 +643,6 @@ prefix: optional prefix (falls back to value of "path-prefix" variable) callback: optional callback(err) to be invoked when the listener is up and running */ Server.prototype.listen = function(port,host,prefix,options) { - const { ok } = require("assert"); - var self = this; // Handle defaults for port and host port = port || this.get("port"); @@ -661,7 +665,6 @@ Server.prototype.listen = function(port,host,prefix,options) { $tw.utils.warning(error); } // Create the server - require("https").createServer var server = this.transport.createServer(this.listenOptions || {},function(request,response,options) { if(self.get("debug-level") !== "none") { var start = $tw.utils.timer(); @@ -669,7 +672,6 @@ Server.prototype.listen = function(port,host,prefix,options) { console.log("Response time:",request.method,request.url,$tw.utils.timer() - start); }); } - // eslint-disable-next-line custom-rules/always-await 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) @@ -680,7 +682,7 @@ Server.prototype.listen = function(port,host,prefix,options) { }); // Log listening details var address = server.address(); - ok(typeof address === "object", "Expected server.address() to return an object"); + 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"); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/startup.js b/plugins/tiddlywiki/multiwikiserver/modules/startup.js index 01dfef6242e..39220b268c6 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/startup.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/startup.js @@ -32,7 +32,7 @@ exports.startup = async function() { engine: $tw.wiki.getTiddlerText("$:/config/MultiWikiServer/Engine", "better"), // better || wasm attachmentStore: attachmentStore }); - await store.initCheck(); + await store.init(); const { ServerManager } = require("$:/plugins/tiddlywiki/multiwikiserver/mws-server.js"); const serverManager = new ServerManager(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-engine.js b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-engine.js index 9421caa428c..b9b3926cd5a 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-engine.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-engine.js @@ -9,26 +9,23 @@ This class is intended to encapsulate all engine-specific logic. \*/ -(function () { - +(function() { -/* -Create a database engine. Options include: - -databasePath - path to the database file (can be ":memory:" or missing to get a temporary database) -engine - wasm | better -*/ -class SqlEngine { -constructor(options) { +/** + * 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); @@ -46,29 +43,43 @@ constructor(options) { case "better": Database = require("better-sqlite3"); break; + default: + throw new Error("Unknown database engine " + this.engine); } - this.db = new Database(databasePath, { + this.db = new Database(databasePath,{ verbose: undefined && console.log }); + const _syncError = new Error("init was not immediately called on SqlEngine") + /** @type {any} */ + this._syncCheck = setTimeout(() => { + $tw.utils.warning(_syncError); + }); + +} + +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"); } } -async close() { +SqlEngine.prototype.close = async function() { for(const sql in this.statements) { if(this.statements[sql].finalize) { await this.statements[sql].finalize(); } } this.statements = Object.create(null); - this.db.close(); + await this.db.close(); this.db = undefined; -} +}; -normaliseParams(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) { @@ -79,54 +90,45 @@ normaliseParams(params) { } } return result; -} +}; -async prepareStatement(sql) { +/** + * + * @param {string} sql + */ +SqlEngine.prototype.prepareStatement = async function(sql) { if(!(sql in this.statements)) { - // node:sqlite supports bigint, causing an error here this.statements[sql] = await this.db.prepare(sql); } - return this.statements[sql]; -} + return /** @type {ReturnType} */(this.statements[sql]); +}; -/** - * @returns {Promise} - */ -async runStatement(sql, params) { - params = this.normaliseParams(params); +SqlEngine.prototype.runStatement = async function(sql,params) { + params = await this.normaliseParams(params); const statement = await this.prepareStatement(sql); return await statement.run(params); -} +}; -/** - * @param {string} sql - * @returns {Promise} - */ -async runStatementGet(sql, params) { - params = this.normaliseParams(params); +SqlEngine.prototype.runStatementGet = async function(sql,params) { + params = await this.normaliseParams(params); const statement = await this.prepareStatement(sql); - return await statement.get(params); -} + return /** @type {Record} */(await statement.get(params)); +}; -/** - * @returns {Promise} - */ -async runStatementGetAll(sql, params) { - params = this.normaliseParams(params); +SqlEngine.prototype.runStatementGetAll = async function(sql,params) { + params = await this.normaliseParams(params); const statement = await this.prepareStatement(sql); - return await statement.all(params); -} - -/** - * @returns {Promise} - */ -async runStatements(sqlArray) { - const res = []; - for(const sql of sqlArray) { - res.push(await this.runStatement(sql)); + return /** @type {Record[]} */(await statement.all(params)); +}; + +SqlEngine.prototype.runStatements = async function(sqlArray) { + /** @type {Awaited>[]} */ + const results = new Array(sqlArray.length); + for(let t=0; t Promise} fn - function to execute in the transaction @returns {Promise} - the result -@template T + */ -async transaction(fn) { +SqlEngine.prototype.transaction = async function(fn) { const alreadyInTransaction = this.transactionDepth > 0; this.transactionDepth++; - try { + try { if(alreadyInTransaction) { return await fn(); } else { - await this.runStatement("BEGIN TRANSACTION"); + await this.runStatement(`BEGIN TRANSACTION`); try { var result = await fn(); - await this.runStatement("COMMIT TRANSACTION"); + await this.runStatement(`COMMIT TRANSACTION`); } catch(e) { - await this.runStatement("ROLLBACK TRANSACTION"); - throw (e); + await this.runStatement(`ROLLBACK TRANSACTION`); + throw(e); } return result; } - } finally{ + } finally { this.transactionDepth--; } -} -} +}; exports.SqlEngine = SqlEngine; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js index 8bac2992668..b2af90f07a1 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js @@ -10,7 +10,7 @@ Validation is for the most part left to the caller \*/ -(function () { +(function() { /* Create a tiddler store. Options include: @@ -18,670 +18,671 @@ Create a tiddler store. Options include: databasePath - path to the database file (can be ":memory:" to get a temporary database) engine - wasm | better */ -class SqlTiddlerDatabase { -constructor(options) { - options = options || {}; - /** @type {typeof import("./sql-engine").SqlEngine} */ - const SqlEngine = require("$:/plugins/tiddlywiki/multiwikiserver/store/sql-engine.js").SqlEngine; - this.engine = new SqlEngine({ - databasePath: options.databasePath, - engine: options.engine - }); - this.entityTypeToTableMap = { - bag: { - table: "bags", - column: "bag_name" - }, - recipe: { - table: "recipes", - column: "recipe_name" - } - }; -} - -async close() { - await this.engine.close(); -} - -async transaction(fn) { - return await this.engine.transaction(fn); -} - -async createTables() { - await this.engine.runStatements([` --- Users table -CREATE TABLE IF NOT EXISTS users ( - user_id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - email TEXT UNIQUE NOT NULL, - password TEXT NOT NULL, - created_at TEXT DEFAULT (datetime('now')), - last_login TEXT -) -`, ` --- User Session table -CREATE TABLE IF NOT EXISTS sessions ( - user_id INTEGER NOT NULL, - session_id TEXT NOT NULL, - created_at TEXT NOT NULL, - last_accessed TEXT NOT NULL, - PRIMARY KEY (session_id), - FOREIGN KEY (user_id) REFERENCES users(user_id) -) -`, ` --- Groups table -CREATE TABLE IF NOT EXISTS groups ( - group_id INTEGER PRIMARY KEY AUTOINCREMENT, - group_name TEXT UNIQUE NOT NULL, - description TEXT -) -`, ` --- Roles table -CREATE TABLE IF NOT EXISTS roles ( - role_id INTEGER PRIMARY KEY AUTOINCREMENT, - role_name TEXT UNIQUE NOT NULL, - description TEXT -) -`, ` --- Permissions table -CREATE TABLE IF NOT EXISTS permissions ( - permission_id INTEGER PRIMARY KEY AUTOINCREMENT, - permission_name TEXT UNIQUE NOT NULL, - description TEXT -) -`, ` --- User-Group association table -CREATE TABLE IF NOT EXISTS user_groups ( - user_id INTEGER, - group_id INTEGER, - PRIMARY KEY (user_id, group_id), - FOREIGN KEY (user_id) REFERENCES users(user_id), - FOREIGN KEY (group_id) REFERENCES groups(group_id) -) -`, ` --- User-Role association table -CREATE TABLE IF NOT EXISTS user_roles ( - user_id INTEGER, - role_id INTEGER, - PRIMARY KEY (user_id, role_id), - FOREIGN KEY (user_id) REFERENCES users(user_id), - FOREIGN KEY (role_id) REFERENCES roles(role_id) -) -`, ` --- Group-Role association table -CREATE TABLE IF NOT EXISTS group_roles ( - group_id INTEGER, - role_id INTEGER, - PRIMARY KEY (group_id, role_id), - FOREIGN KEY (group_id) REFERENCES groups(group_id), - FOREIGN KEY (role_id) REFERENCES roles(role_id) -) -`, ` --- Role-Permission association table -CREATE TABLE IF NOT EXISTS role_permissions ( - role_id INTEGER, - permission_id INTEGER, - PRIMARY KEY (role_id, permission_id), - FOREIGN KEY (role_id) REFERENCES roles(role_id), - FOREIGN KEY (permission_id) REFERENCES permissions(permission_id) -) -`, ` --- Bags have names and access control settings -CREATE TABLE IF NOT EXISTS bags ( - bag_id INTEGER PRIMARY KEY AUTOINCREMENT, - bag_name TEXT UNIQUE NOT NULL, - accesscontrol TEXT NOT NULL, - description TEXT NOT NULL -) -`, ` --- Recipes have names... -CREATE TABLE IF NOT EXISTS recipes ( - recipe_id INTEGER PRIMARY KEY AUTOINCREMENT, - recipe_name TEXT UNIQUE NOT NULL, - description TEXT NOT NULL, - owner_id INTEGER, - FOREIGN KEY (owner_id) REFERENCES users(user_id) -) -`, ` --- ...and recipes also have an ordered list of bags -CREATE TABLE IF NOT EXISTS recipe_bags ( - recipe_id INTEGER NOT NULL, - bag_id INTEGER NOT NULL, - position INTEGER NOT NULL, - FOREIGN KEY (recipe_id) REFERENCES recipes(recipe_id) ON UPDATE CASCADE ON DELETE CASCADE, - FOREIGN KEY (bag_id) REFERENCES bags(bag_id) ON UPDATE CASCADE ON DELETE CASCADE, - UNIQUE (recipe_id, bag_id) -) -`, ` --- Tiddlers are contained in bags and have titles -CREATE TABLE IF NOT EXISTS tiddlers ( - tiddler_id INTEGER PRIMARY KEY AUTOINCREMENT, - bag_id INTEGER NOT NULL, - title TEXT NOT NULL, - is_deleted BOOLEAN NOT NULL, - attachment_blob TEXT, -- null or the name of an attachment blob - FOREIGN KEY (bag_id) REFERENCES bags(bag_id) ON UPDATE CASCADE ON DELETE CASCADE, - UNIQUE (bag_id, title) -) -`, ` --- Tiddlers also have unordered lists of fields, each of which has a name and associated value -CREATE TABLE IF NOT EXISTS fields ( - tiddler_id INTEGER, - field_name TEXT NOT NULL, - field_value TEXT NOT NULL, - FOREIGN KEY (tiddler_id) REFERENCES tiddlers(tiddler_id) ON UPDATE CASCADE ON DELETE CASCADE, - UNIQUE (tiddler_id, field_name) -) -`, ` --- ACL table (using bag/recipe ids directly) -CREATE TABLE IF NOT EXISTS acl ( - acl_id INTEGER PRIMARY KEY AUTOINCREMENT, - entity_name TEXT NOT NULL, - entity_type TEXT NOT NULL CHECK (entity_type IN ('bag', 'recipe')), - role_id INTEGER, - permission_id INTEGER, - FOREIGN KEY (role_id) REFERENCES roles(role_id), - FOREIGN KEY (permission_id) REFERENCES permissions(permission_id) -) -`, ` --- Indexes for performance (we can add more as needed based on query patterns) -CREATE INDEX IF NOT EXISTS idx_tiddlers_bag_id ON tiddlers(bag_id) -`, ` -CREATE INDEX IF NOT EXISTS idx_fields_tiddler_id ON fields(tiddler_id) -`, ` -CREATE INDEX IF NOT EXISTS idx_recipe_bags_recipe_id ON recipe_bags(recipe_id) -`, ` -CREATE INDEX IF NOT EXISTS idx_acl_entity_id ON acl(entity_name) -`]); -} - -async listBags() { - const rows = await this.engine.runStatementGetAll(` -SELECT bag_name, bag_id, accesscontrol, description -FROM bags -ORDER BY bag_name -`); - return rows; -} +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 + }); + this.entityTypeToTableMap = { + bag: { + table: "bags", + column: "bag_name" + }, + recipe: { + table: "recipes", + column: "recipe_name" + } + }; +} + +SqlTiddlerDatabase.prototype.init = async function() { + await this.engine.init(); +}; + +SqlTiddlerDatabase.prototype.close = async function() { + await this.engine.close(); +}; + + +SqlTiddlerDatabase.prototype.transaction = async function(fn) { + return await this.engine.transaction(fn); +}; + +SqlTiddlerDatabase.prototype.createTables = async function() { + await this.engine.runStatements([` + -- Users table + CREATE TABLE IF NOT EXISTS users ( + user_id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + email TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + created_at TEXT DEFAULT (datetime('now')), + last_login TEXT + ) + `,` + -- User Session table + CREATE TABLE IF NOT EXISTS sessions ( + user_id INTEGER NOT NULL, + session_id TEXT NOT NULL, + created_at TEXT NOT NULL, + last_accessed TEXT NOT NULL, + PRIMARY KEY (session_id), + FOREIGN KEY (user_id) REFERENCES users(user_id) + ) + `,` + -- Groups table + CREATE TABLE IF NOT EXISTS groups ( + group_id INTEGER PRIMARY KEY AUTOINCREMENT, + group_name TEXT UNIQUE NOT NULL, + description TEXT + ) + `,` + -- Roles table + CREATE TABLE IF NOT EXISTS roles ( + role_id INTEGER PRIMARY KEY AUTOINCREMENT, + role_name TEXT UNIQUE NOT NULL, + description TEXT + ) + `,` + -- Permissions table + CREATE TABLE IF NOT EXISTS permissions ( + permission_id INTEGER PRIMARY KEY AUTOINCREMENT, + permission_name TEXT UNIQUE NOT NULL, + description TEXT + ) + `,` + -- User-Group association table + CREATE TABLE IF NOT EXISTS user_groups ( + user_id INTEGER, + group_id INTEGER, + PRIMARY KEY (user_id, group_id), + FOREIGN KEY (user_id) REFERENCES users(user_id), + FOREIGN KEY (group_id) REFERENCES groups(group_id) + ) + `,` + -- User-Role association table + CREATE TABLE IF NOT EXISTS user_roles ( + user_id INTEGER, + role_id INTEGER, + PRIMARY KEY (user_id, role_id), + FOREIGN KEY (user_id) REFERENCES users(user_id), + FOREIGN KEY (role_id) REFERENCES roles(role_id) + ) + `,` + -- Group-Role association table + CREATE TABLE IF NOT EXISTS group_roles ( + group_id INTEGER, + role_id INTEGER, + PRIMARY KEY (group_id, role_id), + FOREIGN KEY (group_id) REFERENCES groups(group_id), + FOREIGN KEY (role_id) REFERENCES roles(role_id) + ) + `,` + -- Role-Permission association table + CREATE TABLE IF NOT EXISTS role_permissions ( + role_id INTEGER, + permission_id INTEGER, + PRIMARY KEY (role_id, permission_id), + FOREIGN KEY (role_id) REFERENCES roles(role_id), + FOREIGN KEY (permission_id) REFERENCES permissions(permission_id) + ) + `,` + -- Bags have names and access control settings + CREATE TABLE IF NOT EXISTS bags ( + bag_id INTEGER PRIMARY KEY AUTOINCREMENT, + bag_name TEXT UNIQUE NOT NULL, + accesscontrol TEXT NOT NULL, + description TEXT NOT NULL + ) + `,` + -- Recipes have names... + CREATE TABLE IF NOT EXISTS recipes ( + recipe_id INTEGER PRIMARY KEY AUTOINCREMENT, + recipe_name TEXT UNIQUE NOT NULL, + description TEXT NOT NULL, + owner_id INTEGER, + FOREIGN KEY (owner_id) REFERENCES users(user_id) + ) + `,` + -- ...and recipes also have an ordered list of bags + CREATE TABLE IF NOT EXISTS recipe_bags ( + recipe_id INTEGER NOT NULL, + bag_id INTEGER NOT NULL, + position INTEGER NOT NULL, + FOREIGN KEY (recipe_id) REFERENCES recipes(recipe_id) ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (bag_id) REFERENCES bags(bag_id) ON UPDATE CASCADE ON DELETE CASCADE, + UNIQUE (recipe_id, bag_id) + ) + `,` + -- Tiddlers are contained in bags and have titles + CREATE TABLE IF NOT EXISTS tiddlers ( + tiddler_id INTEGER PRIMARY KEY AUTOINCREMENT, + bag_id INTEGER NOT NULL, + title TEXT NOT NULL, + is_deleted BOOLEAN NOT NULL, + attachment_blob TEXT, -- null or the name of an attachment blob + FOREIGN KEY (bag_id) REFERENCES bags(bag_id) ON UPDATE CASCADE ON DELETE CASCADE, + UNIQUE (bag_id, title) + ) + `,` + -- Tiddlers also have unordered lists of fields, each of which has a name and associated value + CREATE TABLE IF NOT EXISTS fields ( + tiddler_id INTEGER, + field_name TEXT NOT NULL, + field_value TEXT NOT NULL, + FOREIGN KEY (tiddler_id) REFERENCES tiddlers(tiddler_id) ON UPDATE CASCADE ON DELETE CASCADE, + UNIQUE (tiddler_id, field_name) + ) + `,` + -- ACL table (using bag/recipe ids directly) + CREATE TABLE IF NOT EXISTS acl ( + acl_id INTEGER PRIMARY KEY AUTOINCREMENT, + entity_name TEXT NOT NULL, + entity_type TEXT NOT NULL CHECK (entity_type IN ('bag', 'recipe')), + role_id INTEGER, + permission_id INTEGER, + FOREIGN KEY (role_id) REFERENCES roles(role_id), + FOREIGN KEY (permission_id) REFERENCES permissions(permission_id) + ) + `,` + -- Indexes for performance (we can add more as needed based on query patterns) + CREATE INDEX IF NOT EXISTS idx_tiddlers_bag_id ON tiddlers(bag_id) + `,` + CREATE INDEX IF NOT EXISTS idx_fields_tiddler_id ON fields(tiddler_id) + `,` + CREATE INDEX IF NOT EXISTS idx_recipe_bags_recipe_id ON recipe_bags(recipe_id) + `,` + CREATE INDEX IF NOT EXISTS idx_acl_entity_id ON acl(entity_name) + `]); +}; + +SqlTiddlerDatabase.prototype.listBags = async function() { + const rows = await this.engine.runStatementGetAll(` + SELECT bag_name, bag_id, accesscontrol, description + FROM bags + ORDER BY bag_name + `); + return rows; +}; /* Create or update a bag Returns the bag_id of the bag */ -async createBag(bag_name, description, accesscontrol) { - accesscontrol = accesscontrol || ""; - // Run the queries - var bag = await this.engine.runStatement(` -INSERT OR IGNORE INTO bags (bag_name, accesscontrol, description) -VALUES ($bag_name, '', '') -`, { - $bag_name: bag_name - }); - const updateBags = await this.engine.runStatement(` -UPDATE bags -SET accesscontrol = $accesscontrol, -description = $description -WHERE bag_name = $bag_name -`, { - $bag_name: bag_name, - $accesscontrol: accesscontrol, - $description: description - }); - return updateBags.lastInsertRowid; -} +SqlTiddlerDatabase.prototype.createBag = async function(bag_name,description,accesscontrol) { + accesscontrol = accesscontrol || ""; + // Run the queries + var bag = await this.engine.runStatement(` + INSERT OR IGNORE INTO bags (bag_name, accesscontrol, description) + VALUES ($bag_name, '', '') + `,{ + $bag_name: bag_name + }); + const updateBags = await this.engine.runStatement(` + UPDATE bags + SET accesscontrol = $accesscontrol, + description = $description + WHERE bag_name = $bag_name + `,{ + $bag_name: bag_name, + $accesscontrol: accesscontrol, + $description: description + }); + return updateBags.lastInsertRowid; +}; /* Returns array of {recipe_name:,recipe_id:,description:,bag_names: []} */ -async listRecipes() { - const rows = await this.engine.runStatementGetAll(` -SELECT r.recipe_name, r.recipe_id, r.description, r.owner_id, b.bag_name, rb.position -FROM recipes AS r -JOIN recipe_bags AS rb ON rb.recipe_id = r.recipe_id -JOIN bags AS b ON rb.bag_id = b.bag_id -ORDER BY r.recipe_name, rb.position -`); - const results = []; - let currentRecipeName = null, currentRecipeIndex = -1; - for (const row of rows) { - if (row.recipe_name !== currentRecipeName) { - currentRecipeName = row.recipe_name; - currentRecipeIndex += 1; - results.push({ - recipe_name: row.recipe_name, - recipe_id: row.recipe_id, - description: row.description, - owner_id: row.owner_id, +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 + JOIN bags AS b ON rb.bag_id = b.bag_id + ORDER BY r.recipe_name, rb.position + `); + const results = []; + let currentRecipeName = null, currentRecipeIndex = -1; + for(const row of rows) { + if(row.recipe_name !== currentRecipeName) { + currentRecipeName = row.recipe_name; + currentRecipeIndex += 1; + results.push({ + recipe_name: row.recipe_name, + recipe_id: row.recipe_id, + description: row.description, + owner_id: row.owner_id, /** @type {string[]} */ - bag_names: [] - }); - } - results[currentRecipeIndex].bag_names.push(row.bag_name); - } - return results; -} + bag_names: [] + }); + } + results[currentRecipeIndex].bag_names.push(row.bag_name); + } + return results; +}; /* Create or update a recipe Returns the recipe_id of the recipe */ -async createRecipe(recipe_name, bag_names, description) { - // Run the queries - await this.engine.runStatement(` --- Delete existing recipe_bags entries for this recipe -DELETE FROM recipe_bags WHERE recipe_id = (SELECT recipe_id FROM recipes WHERE recipe_name = $recipe_name) -`, { - $recipe_name: recipe_name - }); - const updateRecipes = await this.engine.runStatement(` --- Create the entry in the recipes table if required -INSERT OR REPLACE INTO recipes (recipe_name, description) -VALUES ($recipe_name, $description) -`, { - $recipe_name: recipe_name, - $description: description - }); - await this.engine.runStatement(` -INSERT INTO recipe_bags (recipe_id, bag_id, position) -SELECT r.recipe_id, b.bag_id, j.key as position -FROM recipes r -JOIN bags b -INNER JOIN json_each($bag_names) AS j ON j.value = b.bag_name -WHERE r.recipe_name = $recipe_name -`, { - $recipe_name: recipe_name, - $bag_names: JSON.stringify(bag_names) - }); - - return updateRecipes.lastInsertRowid; -} +SqlTiddlerDatabase.prototype.createRecipe = async function(recipe_name,bag_names,description) { + // Run the queries + await this.engine.runStatement(` + -- Delete existing recipe_bags entries for this recipe + DELETE FROM recipe_bags WHERE recipe_id = (SELECT recipe_id FROM recipes WHERE recipe_name = $recipe_name) + `,{ + $recipe_name: recipe_name + }); + const updateRecipes = await this.engine.runStatement(` + -- Create the entry in the recipes table if required + INSERT OR REPLACE INTO recipes (recipe_name, description) + VALUES ($recipe_name, $description) + `,{ + $recipe_name: recipe_name, + $description: description + }); + await this.engine.runStatement(` + INSERT INTO recipe_bags (recipe_id, bag_id, position) + SELECT r.recipe_id, b.bag_id, j.key as position + FROM recipes r + JOIN bags b + INNER JOIN json_each($bag_names) AS j ON j.value = b.bag_name + WHERE r.recipe_name = $recipe_name + `,{ + $recipe_name: recipe_name, + $bag_names: JSON.stringify(bag_names) + }); + + return updateRecipes.lastInsertRowid; +}; /* Assign a recipe to a user */ -async assignRecipeToUser(recipe_name, user_id) { - await this.engine.runStatement(` -UPDATE recipes SET owner_id = $user_id WHERE recipe_name = $recipe_name -`, { - $recipe_name: recipe_name, - $user_id: user_id - }); -} +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, + $user_id: user_id + }); +}; /* Returns {tiddler_id:} */ -async saveBagTiddler(tiddlerFields, bag_name, attachment_blob) { - attachment_blob = attachment_blob || null; - // Update the tiddlers table - var info = await this.engine.runStatement(` -INSERT OR REPLACE INTO tiddlers (bag_id, title, is_deleted, attachment_blob) -VALUES ( - (SELECT bag_id FROM bags WHERE bag_name = $bag_name), - $title, - FALSE, - $attachment_blob -) -`, { - $title: tiddlerFields.title, - $attachment_blob: attachment_blob, - $bag_name: bag_name - }); - // Update the fields table - await this.engine.runStatement(` -INSERT OR REPLACE INTO fields (tiddler_id, field_name, field_value) -SELECT - t.tiddler_id, - json_each.key AS field_name, - json_each.value AS field_value -FROM ( - SELECT tiddler_id - FROM tiddlers - WHERE bag_id = ( - SELECT bag_id - FROM bags - WHERE bag_name = $bag_name - ) AND title = $title -) AS t -JOIN json_each($field_values) AS json_each -`, { - $title: tiddlerFields.title, - $bag_name: bag_name, - $field_values: JSON.stringify(Object.assign({}, tiddlerFields, { title: undefined })) - }); - return { - tiddler_id: info.lastInsertRowid - }; -} +SqlTiddlerDatabase.prototype.saveBagTiddler = async function(tiddlerFields,bag_name,attachment_blob) { + attachment_blob = attachment_blob || null; + // Update the tiddlers table + var info = await this.engine.runStatement(` + INSERT OR REPLACE INTO tiddlers (bag_id, title, is_deleted, attachment_blob) + VALUES ( + (SELECT bag_id FROM bags WHERE bag_name = $bag_name), + $title, + FALSE, + $attachment_blob + ) + `,{ + $title: tiddlerFields.title, + $attachment_blob: attachment_blob, + $bag_name: bag_name + }); + // Update the fields table + await this.engine.runStatement(` + INSERT OR REPLACE INTO fields (tiddler_id, field_name, field_value) + SELECT + t.tiddler_id, + json_each.key AS field_name, + json_each.value AS field_value + FROM ( + SELECT tiddler_id + FROM tiddlers + WHERE bag_id = ( + SELECT bag_id + FROM bags + WHERE bag_name = $bag_name + ) AND title = $title + ) AS t + JOIN json_each($field_values) AS json_each + `,{ + $title: tiddlerFields.title, + $bag_name: bag_name, + $field_values: JSON.stringify(Object.assign({},tiddlerFields,{title: undefined})) + }); + return { + tiddler_id: info.lastInsertRowid + } +}; /* Returns {tiddler_id:,bag_name:} or null if the recipe is empty */ -async saveRecipeTiddler(tiddlerFields, recipe_name, attachment_blob) { - // Find the topmost bag in the recipe - var row = await this.engine.runStatementGet(` -SELECT b.bag_name -FROM bags AS b -JOIN ( - SELECT rb.bag_id - FROM recipe_bags AS rb - WHERE rb.recipe_id = ( - SELECT recipe_id - FROM recipes - WHERE recipe_name = $recipe_name - ) - ORDER BY rb.position DESC - LIMIT 1 -) AS selected_bag -ON b.bag_id = selected_bag.bag_id -`, { - $recipe_name: recipe_name - }); - if (!row) { - return null; - } - // Save the tiddler to the topmost bag - var info = await this.saveBagTiddler(tiddlerFields, row.bag_name, attachment_blob); - return { - tiddler_id: info.tiddler_id, - bag_name: row.bag_name - }; -} +SqlTiddlerDatabase.prototype.saveRecipeTiddler = async function(tiddlerFields,recipe_name,attachment_blob) { + // Find the topmost bag in the recipe + var row = await this.engine.runStatementGet(` + SELECT b.bag_name + FROM bags AS b + JOIN ( + SELECT rb.bag_id + FROM recipe_bags AS rb + WHERE rb.recipe_id = ( + SELECT recipe_id + FROM recipes + WHERE recipe_name = $recipe_name + ) + ORDER BY rb.position DESC + LIMIT 1 + ) AS selected_bag + ON b.bag_id = selected_bag.bag_id + `,{ + $recipe_name: recipe_name + }); + if(!row) { + return null; + } + // Save the tiddler to the topmost bag + var info = await this.saveBagTiddler(tiddlerFields,row.bag_name,attachment_blob); + return { + tiddler_id: info.tiddler_id, + bag_name: row.bag_name + }; +}; /* Returns {tiddler_id:} of the delete marker */ -async deleteTiddler(title, bag_name) { - // Delete the fields of this tiddler - await this.engine.runStatement(` -DELETE FROM fields -WHERE tiddler_id IN ( - SELECT t.tiddler_id - FROM tiddlers AS t - INNER JOIN bags AS b ON t.bag_id = b.bag_id - WHERE b.bag_name = $bag_name AND t.title = $title -) -`, { - $title: title, - $bag_name: bag_name - }); - // Mark the tiddler itself as deleted - const rowDeleteMarker = await this.engine.runStatement(` -INSERT OR REPLACE INTO tiddlers (bag_id, title, is_deleted, attachment_blob) -VALUES ( - (SELECT bag_id FROM bags WHERE bag_name = $bag_name), - $title, - TRUE, - NULL -) -`, { - $title: title, - $bag_name: bag_name - }); - return { tiddler_id: rowDeleteMarker.lastInsertRowid }; -} +SqlTiddlerDatabase.prototype.deleteTiddler = async function(title,bag_name) { + // Delete the fields of this tiddler + await this.engine.runStatement(` + DELETE FROM fields + WHERE tiddler_id IN ( + SELECT t.tiddler_id + FROM tiddlers AS t + INNER JOIN bags AS b ON t.bag_id = b.bag_id + WHERE b.bag_name = $bag_name AND t.title = $title + ) + `,{ + $title: title, + $bag_name: bag_name + }); + // Mark the tiddler itself as deleted + const rowDeleteMarker = await this.engine.runStatement(` + INSERT OR REPLACE INTO tiddlers (bag_id, title, is_deleted, attachment_blob) + VALUES ( + (SELECT bag_id FROM bags WHERE bag_name = $bag_name), + $title, + TRUE, + NULL + ) + `,{ + $title: title, + $bag_name: bag_name + }); + return {tiddler_id: rowDeleteMarker.lastInsertRowid}; +}; /* returns {tiddler_id:,tiddler:,attachment_blob:} */ -async getBagTiddler(title, bag_name) { - const rowTiddler = await this.engine.runStatementGet(` -SELECT t.tiddler_id, t.attachment_blob -FROM bags AS b -INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id -WHERE t.title = $title AND b.bag_name = $bag_name AND t.is_deleted = FALSE -`, { - $title: title, - $bag_name: bag_name - }); - if (!rowTiddler) { - return null; - } - const rows = await this.engine.runStatementGetAll(` -SELECT field_name, field_value, tiddler_id -FROM fields -WHERE tiddler_id = $tiddler_id -`, { - $tiddler_id: rowTiddler.tiddler_id - }); - if (rows.length === 0) { - return null; - } else { - return { - tiddler_id: rows[0].tiddler_id, - attachment_blob: rowTiddler.attachment_blob, - tiddler: rows.reduce((accumulator, value) => { - accumulator[value["field_name"]] = value.field_value; - return accumulator; - }, { title: title }) - }; - } -} +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 + WHERE t.title = $title AND b.bag_name = $bag_name AND t.is_deleted = FALSE + `,{ + $title: title, + $bag_name: bag_name + }); + if(!rowTiddler) { + return null; + } + const rows = await this.engine.runStatementGetAll(` + SELECT field_name, field_value, tiddler_id + FROM fields + WHERE tiddler_id = $tiddler_id + `,{ + $tiddler_id: rowTiddler.tiddler_id + }); + if(rows.length === 0) { + return null; + } else { + return { + tiddler_id: rows[0].tiddler_id, + attachment_blob: rowTiddler.attachment_blob, + tiddler: rows.reduce((accumulator,value) => { + accumulator[value["field_name"]] = value.field_value; + return accumulator; + },{title: title}) + }; + } +}; /* Returns {bag_name:, tiddler: {fields}, tiddler_id:, attachment_blob:} */ -async getRecipeTiddler(title, recipe_name) { - const rowTiddlerId = await this.engine.runStatementGet(` -SELECT t.tiddler_id, t.attachment_blob, b.bag_name -FROM bags AS b -INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id -INNER JOIN recipes AS r ON rb.recipe_id = r.recipe_id -INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id -WHERE r.recipe_name = $recipe_name -AND t.title = $title -AND t.is_deleted = FALSE -ORDER BY rb.position DESC -LIMIT 1 -`, { - $title: title, - $recipe_name: recipe_name - }); - if (!rowTiddlerId) { - return null; - } - // Get the fields - const rows = await this.engine.runStatementGetAll(` -SELECT field_name, field_value -FROM fields -WHERE tiddler_id = $tiddler_id -`, { - $tiddler_id: rowTiddlerId.tiddler_id - }); - return { - bag_name: rowTiddlerId.bag_name, - tiddler_id: rowTiddlerId.tiddler_id, - attachment_blob: rowTiddlerId.attachment_blob, - tiddler: rows.reduce((accumulator, value) => { - accumulator[value["field_name"]] = value.field_value; - return accumulator; - }, { title: title }) - }; -} +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 + INNER JOIN recipes AS r ON rb.recipe_id = r.recipe_id + INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id + WHERE r.recipe_name = $recipe_name + AND t.title = $title + AND t.is_deleted = FALSE + ORDER BY rb.position DESC + LIMIT 1 + `,{ + $title: title, + $recipe_name: recipe_name + }); + if(!rowTiddlerId) { + return null; + } + // Get the fields + const rows = await this.engine.runStatementGetAll(` + SELECT field_name, field_value + FROM fields + WHERE tiddler_id = $tiddler_id + `,{ + $tiddler_id: rowTiddlerId.tiddler_id + }); + return { + bag_name: rowTiddlerId.bag_name, + tiddler_id: rowTiddlerId.tiddler_id, + attachment_blob: rowTiddlerId.attachment_blob, + tiddler: rows.reduce((accumulator,value) => { + accumulator[value["field_name"]] = value.field_value; + return accumulator; + },{title: title}) + }; +}; -/** - * Checks if a user has permission to access a recipe - * @param {number | null | undefined} userId - * @param {string} recipeName - * @param {string} permissionName - */ -async hasRecipePermission(userId, recipeName, permissionName) { - try { - // check if the user is the owner of the entity - const recipe = await this.engine.runStatementGet(` - SELECT owner_id - FROM recipes - WHERE recipe_name = $recipe_name - `, { - $recipe_name: recipeName - }); - - if (recipe && !!recipe.owner_id && recipe.owner_id === userId) { - return true; - } else { - var permission = await this.checkACLPermission(userId, "recipe", recipeName, permissionName, recipe && recipe.owner_id); - return permission; - } - - } catch (error) { - console.error(error); - return false; - } -} +/* +Checks if a user has permission to access a recipe +*/ +SqlTiddlerDatabase.prototype.hasRecipePermission = async function(userId, recipeName, permissionName) { + try { + // check if the user is the owner of the entity + const recipe = await this.engine.runStatementGet(` + SELECT owner_id + FROM recipes + WHERE recipe_name = $recipe_name + `, { + $recipe_name: recipeName + }); + + if(!!recipe?.owner_id && recipe?.owner_id === userId) { + return true; + } else { + var permission = await this.checkACLPermission(userId, "recipe", recipeName, permissionName, recipe?.owner_id) + return permission; + } + + } catch (error) { + console.error(error) + return false + } +}; /* Checks if a user has permission to access a bag */ -async hasBagPermission(userId, bagName, permissionName) { - return await this.checkACLPermission(userId, "bag", bagName, permissionName); -} - -async getACLByName(entityType, entityName, fetchAll) { - const entityInfo = this.entityTypeToTableMap[entityType]; - if (!entityInfo) { - throw new Error("Invalid entity type: " + entityType); - } - - // First, check if there's an ACL record for the entity and get the permission_id - var checkACLExistsQuery = ` -SELECT acl.*, permissions.permission_name -FROM acl -LEFT JOIN permissions ON acl.permission_id = permissions.permission_id -WHERE acl.entity_type = $entity_type -AND acl.entity_name = $entity_name -`; - - if (!fetchAll) { - checkACLExistsQuery += " LIMIT 1"; - } - - const aclRecord = await this.engine[fetchAll ? "runStatementGetAll" : "runStatementGet"](checkACLExistsQuery, { - $entity_type: entityType, - $entity_name: entityName - }); - - return aclRecord; -} - -async checkACLPermission(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 = await this.getACLByName(entityType, entityName, true); - const aclRecord = aclRecords.find(record => record.permission_name === permissionName); - - // If no ACL record exists, return true for hasPermission - if ((!aclRecord && !ownerId && aclRecords.length === 0) || ((!!aclRecord && !!ownerId) && ownerId === userId)) { - return true; - } - - // If ACL record exists, check for user permission using the retrieved permission_id - const checkPermissionQuery = ` - SELECT * - FROM users u - JOIN user_roles ur ON u.user_id = ur.user_id - JOIN roles r ON ur.role_id = r.role_id - JOIN acl a ON r.role_id = a.role_id - WHERE u.user_id = $user_id - AND a.entity_type = $entity_type - AND a.entity_name = $entity_name - AND a.permission_id = $permission_id - LIMIT 1 -`; - - const result = await this.engine.runStatementGet(checkPermissionQuery, { - $user_id: userId, - $entity_type: entityType, - $entity_name: entityName, - $permission_id: aclRecord && aclRecord.permission_id - }); - - let hasPermission = result !== undefined; - - return hasPermission; - - } catch (error) { - console.error(error); - return false; - } -} +SqlTiddlerDatabase.prototype.hasBagPermission = async function(userId, bagName, permissionName) { + return await this.checkACLPermission(userId, "bag", bagName, permissionName) +}; + +SqlTiddlerDatabase.prototype.getACLByName = async function(entityType, entityName, fetchAll) { + const entityInfo = this.entityTypeToTableMap[entityType]; + if (!entityInfo) { + throw new Error("Invalid entity type: " + entityType); + } + + // First, check if there's an ACL record for the entity and get the permission_id + var checkACLExistsQuery = ` + SELECT acl.*, permissions.permission_name + FROM acl + LEFT JOIN permissions ON acl.permission_id = permissions.permission_id + WHERE acl.entity_type = $entity_type + AND acl.entity_name = $entity_name + `; + + if (!fetchAll) { + checkACLExistsQuery += ' LIMIT 1' + } + + const aclRecord = await this.engine[fetchAll ? 'runStatementGetAll' : 'runStatementGet'](checkACLExistsQuery, { + $entity_type: entityType, + $entity_name: entityName + }); + + return aclRecord; +} + +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 = await this.getACLByName(entityType, entityName, true); + const aclRecord = aclRecords.find(record => record.permission_name === permissionName); + + // If no ACL record exists, return true for hasPermission + if ((!aclRecord && !ownerId && aclRecords.length === 0) || ((!!aclRecord && !!ownerId) && ownerId === userId)) { + return true; + } + + // If ACL record exists, check for user permission using the retrieved permission_id + const checkPermissionQuery = ` + SELECT * + FROM users u + JOIN user_roles ur ON u.user_id = ur.user_id + JOIN roles r ON ur.role_id = r.role_id + JOIN acl a ON r.role_id = a.role_id + WHERE u.user_id = $user_id + AND a.entity_type = $entity_type + AND a.entity_name = $entity_name + AND a.permission_id = $permission_id + LIMIT 1 + `; + + const result = await this.engine.runStatementGet(checkPermissionQuery, { + $user_id: userId, + $entity_type: entityType, + $entity_name: entityName, + $permission_id: aclRecord?.permission_id + }); + + let hasPermission = result !== undefined; + + return hasPermission; + + } catch (error) { + console.error(error); + return false + } +}; /** * Returns the ACL records for an entity (bag or recipe) */ -async getEntityAclRecords(entityName) { - const checkACLExistsQuery = ` -SELECT * -FROM acl -WHERE entity_name = $entity_name -`; - - const aclRecords = await this.engine.runStatementGetAll(checkACLExistsQuery, { - $entity_name: entityName - }); - - return aclRecords; +SqlTiddlerDatabase.prototype.getEntityAclRecords = async function(entityName) { + const checkACLExistsQuery = ` + SELECT * + FROM acl + WHERE entity_name = $entity_name + `; + + const aclRecords = await this.engine.runStatementGetAll(checkACLExistsQuery, { + $entity_name: entityName + }); + + return aclRecords } /* Get the entity by name */ -async getEntityByName(entityType, entityName) { - const entityInfo = this.entityTypeToTableMap[entityType]; - if (entityInfo) { - return await this.engine.runStatementGet(`SELECT * FROM ${entityInfo.table} WHERE ${entityInfo.column} = $entity_name`, { - $entity_name: entityName - }); - } - return null; +SqlTiddlerDatabase.prototype.getEntityByName = async function(entityType, entityName) { + const entityInfo = this.entityTypeToTableMap[entityType]; + if (entityInfo) { + return await this.engine.runStatementGet(`SELECT * FROM ${entityInfo.table} WHERE ${entityInfo.column} = $entity_name`, { + $entity_name: entityName + }); + } + return null; } /* Get the titles of the tiddlers in a bag. Returns an empty array for bags that do not exist */ -async getBagTiddlers(bag_name) { - const rows = await this.engine.runStatementGetAll(` -SELECT DISTINCT title, tiddler_id -FROM tiddlers -WHERE bag_id IN ( - SELECT bag_id - FROM bags - WHERE bag_name = $bag_name -) -AND tiddlers.is_deleted = FALSE -ORDER BY title ASC -`, { - $bag_name: bag_name - }); - return rows; -} +SqlTiddlerDatabase.prototype.getBagTiddlers = async function(bag_name) { + const rows = await this.engine.runStatementGetAll(` + SELECT DISTINCT title, tiddler_id + FROM tiddlers + WHERE bag_id IN ( + SELECT bag_id + FROM bags + WHERE bag_name = $bag_name + ) + AND tiddlers.is_deleted = FALSE + ORDER BY title ASC + `,{ + $bag_name: bag_name + }); + return rows; +}; /* Get the tiddler_id of the newest tiddler in a bag. Returns null for bags that do not exist */ -async getBagLastTiddlerId(bag_name) { - const row = await this.engine.runStatementGet(` -SELECT tiddler_id -FROM tiddlers -WHERE bag_id IN ( - SELECT bag_id - FROM bags - WHERE bag_name = $bag_name -) -ORDER BY tiddler_id DESC -LIMIT 1 -`, { - $bag_name: bag_name - }); - if (row) { - return row.tiddler_id; - } else { - return null; - } -} +SqlTiddlerDatabase.prototype.getBagLastTiddlerId = async function(bag_name) { + const row = await this.engine.runStatementGet(` + SELECT tiddler_id + FROM tiddlers + WHERE bag_id IN ( + SELECT bag_id + FROM bags + WHERE bag_name = $bag_name + ) + ORDER BY tiddler_id DESC + LIMIT 1 + `,{ + $bag_name: bag_name + }); + if(row) { + return row.tiddler_id; + } else { + return null; + } +}; /* Get the metadata of the tiddlers in a recipe as an array [{title:,tiddler_id:,bag_name:,is_deleted:}], @@ -695,340 +696,341 @@ include_deleted: boolean, defaults to false Returns null for recipes that do not exist */ -async getRecipeTiddlers(recipe_name, options) { - options = options || {}; - // Get the recipe ID - const rowsCheckRecipe = await this.engine.runStatementGet(` -SELECT recipe_id FROM recipes WHERE recipes.recipe_name = $recipe_name -`, { - $recipe_name: recipe_name - }); - if (!rowsCheckRecipe) { - return null; - } - const recipe_id = rowsCheckRecipe.recipe_id; - // Compose the query to get the tiddlers - const params = { - $recipe_id: recipe_id - }; - if (options.limit) { - params.$limit = options.limit.toString(); - } - if (options.last_known_tiddler_id) { - params.$last_known_tiddler_id = options.last_known_tiddler_id; - } - const rows = await this.engine.runStatementGetAll(` -SELECT title, tiddler_id, is_deleted, bag_name -FROM ( - SELECT t.title, t.tiddler_id, t.is_deleted, b.bag_name, MAX(rb.position) AS position - FROM bags AS b - INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id - INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id - WHERE rb.recipe_id = $recipe_id - ${options.include_deleted ? "" : "AND t.is_deleted = FALSE"} - ${options.last_known_tiddler_id ? "AND tiddler_id > $last_known_tiddler_id" : ""} - GROUP BY t.title - ORDER BY t.title, tiddler_id DESC - ${options.limit ? "LIMIT $limit" : ""} -) -`, params); - return rows; -} +SqlTiddlerDatabase.prototype.getRecipeTiddlers = async function(recipe_name,options) { + options = options || {}; + // Get the recipe ID + const rowsCheckRecipe = await this.engine.runStatementGet(` + SELECT recipe_id FROM recipes WHERE recipes.recipe_name = $recipe_name + `,{ + $recipe_name: recipe_name + }); + if(!rowsCheckRecipe) { + return null; + } + const recipe_id = rowsCheckRecipe.recipe_id; + // Compose the query to get the tiddlers + const params = { + $recipe_id: recipe_id + } + if(options.limit) { + params.$limit = options.limit.toString(); + } + if(options.last_known_tiddler_id) { + params.$last_known_tiddler_id = options.last_known_tiddler_id; + } + const rows = await this.engine.runStatementGetAll(` + SELECT title, tiddler_id, is_deleted, bag_name + FROM ( + SELECT t.title, t.tiddler_id, t.is_deleted, b.bag_name, MAX(rb.position) AS position + FROM bags AS b + INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id + INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id + WHERE rb.recipe_id = $recipe_id + ${options.include_deleted ? "" : "AND t.is_deleted = FALSE"} + ${options.last_known_tiddler_id ? "AND tiddler_id > $last_known_tiddler_id" : ""} + GROUP BY t.title + ORDER BY t.title, tiddler_id DESC + ${options.limit ? "LIMIT $limit" : ""} + ) + `,params); + return rows; +}; /* Get the tiddler_id of the newest tiddler in a recipe. Returns null for recipes that do not exist */ -async getRecipeLastTiddlerId(recipe_name) { - const row = await this.engine.runStatementGet(` -SELECT t.title, t.tiddler_id, b.bag_name, MAX(rb.position) AS position -FROM bags AS b -INNER JOIN recipe_bags AS rb ON b.bag_id = rb.bag_id -INNER JOIN recipes AS r ON rb.recipe_id = r.recipe_id -INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id -WHERE r.recipe_name = $recipe_name -GROUP BY t.title -ORDER BY t.tiddler_id DESC -LIMIT 1 -`, { - $recipe_name: recipe_name - }); - if (row) { - return row.tiddler_id; - } else { - return null; - } -} - -async deleteAllTiddlersInBag(bag_name) { - // Delete the fields - await this.engine.runStatement(` -DELETE FROM fields -WHERE tiddler_id IN ( - SELECT tiddler_id - FROM tiddlers - WHERE bag_id = (SELECT bag_id FROM bags WHERE bag_name = $bag_name) - AND is_deleted = FALSE -) -`, { - $bag_name: bag_name - }); - // Mark the tiddlers as deleted - await this.engine.runStatement(` -UPDATE tiddlers -SET is_deleted = TRUE -WHERE bag_id = (SELECT bag_id FROM bags WHERE bag_name = $bag_name) -AND is_deleted = FALSE -`, { - $bag_name: bag_name - }); -} +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 + INNER JOIN recipes AS r ON rb.recipe_id = r.recipe_id + INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id + WHERE r.recipe_name = $recipe_name + GROUP BY t.title + ORDER BY t.tiddler_id DESC + LIMIT 1 + `,{ + $recipe_name: recipe_name + }); + if(row) { + return row.tiddler_id; + } else { + return null; + } +}; + +SqlTiddlerDatabase.prototype.deleteAllTiddlersInBag = async function(bag_name) { + // Delete the fields + await this.engine.runStatement(` + DELETE FROM fields + WHERE tiddler_id IN ( + SELECT tiddler_id + FROM tiddlers + WHERE bag_id = (SELECT bag_id FROM bags WHERE bag_name = $bag_name) + AND is_deleted = FALSE + ) + `,{ + $bag_name: bag_name + }); + // Mark the tiddlers as deleted + await this.engine.runStatement(` + UPDATE tiddlers + SET is_deleted = TRUE + WHERE bag_id = (SELECT bag_id FROM bags WHERE bag_name = $bag_name) + AND is_deleted = FALSE + `,{ + $bag_name: bag_name + }); +}; /* Get the names of the bags in a recipe. Returns an empty array for recipes that do not exist */ -async getRecipeBags(recipe_name) { - const rows = await this.engine.runStatementGetAll(` -SELECT bags.bag_name -FROM bags -JOIN ( - SELECT rb.bag_id, rb.position as position - FROM recipe_bags AS rb - JOIN recipes AS r ON rb.recipe_id = r.recipe_id - WHERE r.recipe_name = $recipe_name - ORDER BY rb.position -) AS bag_priority ON bags.bag_id = bag_priority.bag_id -ORDER BY position -`, { - $recipe_name: recipe_name - }); - return rows.map(value => value.bag_name); -} +SqlTiddlerDatabase.prototype.getRecipeBags = async function(recipe_name) { + const rows = await this.engine.runStatementGetAll(` + SELECT bags.bag_name + FROM bags + JOIN ( + SELECT rb.bag_id, rb.position as position + FROM recipe_bags AS rb + JOIN recipes AS r ON rb.recipe_id = r.recipe_id + WHERE r.recipe_name = $recipe_name + ORDER BY rb.position + ) AS bag_priority ON bags.bag_id = bag_priority.bag_id + ORDER BY position + `,{ + $recipe_name: recipe_name + }); + return rows.map(value => value.bag_name); +}; /* Get the attachment value of a bag, if any exist */ -async getBagTiddlerAttachmentBlob(title, bag_name) { - const row = await this.engine.runStatementGet(` -SELECT t.attachment_blob -FROM bags AS b -INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id -WHERE t.title = $title AND b.bag_name = $bag_name AND t.is_deleted = FALSE -`, { - $title: title, - $bag_name: bag_name - }); - return row ? row.attachment_blob : null; -} +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 + WHERE t.title = $title AND b.bag_name = $bag_name AND t.is_deleted = FALSE + `, { + $title: title, + $bag_name: bag_name + }); + return row ? row.attachment_blob : null; +}; /* Get the attachment value of a recipe, if any exist */ -async getRecipeTiddlerAttachmentBlob(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 -INNER JOIN recipes AS r ON rb.recipe_id = r.recipe_id -INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id -WHERE r.recipe_name = $recipe_name AND t.title = $title AND t.is_deleted = FALSE -ORDER BY rb.position DESC -LIMIT 1 -`, { - $title: title, - $recipe_name: recipe_name - }); - return row ? row.attachment_blob : null; -} +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 + INNER JOIN recipes AS r ON rb.recipe_id = r.recipe_id + INNER JOIN tiddlers AS t ON b.bag_id = t.bag_id + WHERE r.recipe_name = $recipe_name AND t.title = $title AND t.is_deleted = FALSE + ORDER BY rb.position DESC + LIMIT 1 + `, { + $title: title, + $recipe_name: recipe_name + }); + return row ? row.attachment_blob : null; +}; // User CRUD operations -async createUser(username, email, password) { - const result = await this.engine.runStatement(` - INSERT INTO users (username, email, password) - VALUES ($username, $email, $password) -`, { - $username: username, - $email: email, - $password: password - }); - return result.lastInsertRowid; -} +SqlTiddlerDatabase.prototype.createUser = async function(username, email, password) { + const result = await this.engine.runStatement(` + INSERT INTO users (username, email, password) + VALUES ($username, $email, $password) + `, { + $username: username, + $email: email, + $password: password + }); + return result.lastInsertRowid; +}; + +SqlTiddlerDatabase.prototype.getUser = async function(userId) { + return await this.engine.runStatementGet(` + SELECT * FROM users WHERE user_id = $userId + `, { + $userId: userId + }); +}; + +SqlTiddlerDatabase.prototype.getUserByUsername = async function(username) { + return await this.engine.runStatementGet(` + SELECT * FROM users WHERE username = $username + `, { + $username: username + }); +}; + +SqlTiddlerDatabase.prototype.getUserByEmail = async function(email) { + return await this.engine.runStatementGet(` + SELECT * FROM users WHERE email = $email + `, { + $email: email + }); +}; + +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 + WHERE ur.role_id = $roleId + ORDER BY u.username + `, { + $roleId: roleId + }); +}; + +SqlTiddlerDatabase.prototype.updateUser = async function (userId, username, email, roleId) { + const existingUser = await this.engine.runStatement(` + SELECT user_id FROM users + WHERE email = $email AND user_id != $userId +`, { + $email: email, + $userId: userId + }); + + if (existingUser.length > 0) { + return { + success: false, + message: "Email address already in use by another user." + }; + } + + try { + await this.engine.transaction(async () => { + // Update user information + await this.engine.runStatement(` + UPDATE users + SET username = $username, email = $email + WHERE user_id = $userId + `, { + $userId: userId, + $username: username, + $email: email + }); + + if (roleId) { + // Remove all existing roles for the user + await this.engine.runStatement(` + DELETE FROM user_roles + WHERE user_id = $userId + `, { + $userId: userId + }); + + // Add the new role + await this.engine.runStatement(` + INSERT INTO user_roles (user_id, role_id) + VALUES ($userId, $roleId) + `, { + $userId: userId, + $roleId: roleId + }); + } + }); + + return { + success: true, + message: "User profile and role updated successfully." + }; + } catch (error) { + return { + success: false, + message: "Failed to update user profile: " + error.message + }; + } +}; + +SqlTiddlerDatabase.prototype.updateUserPassword = async function (userId, newHash) { + try { + await this.engine.runStatement(` + UPDATE users + SET password = $newHash + WHERE user_id = $userId + `, { + $userId: userId, + $newHash: newHash, + }); + + return { + success: true, + message: "Password updated successfully." + }; + } catch (error) { + return { + success: false, + message: "Failed to update password: " + error.message + }; + } +}; + +SqlTiddlerDatabase.prototype.deleteUser = async function(userId) { + await this.engine.runStatement(` + DELETE FROM users WHERE user_id = $userId + `, { + $userId: userId + }); +}; + +SqlTiddlerDatabase.prototype.listUsers = async function() { + return await this.engine.runStatementGetAll(` + SELECT * FROM users ORDER BY username + `); +}; + +SqlTiddlerDatabase.prototype.createOrUpdateUserSession = async function(userId, sessionId) { + const currentTimestamp = new Date().toISOString(); + + // First, try to update an existing session + const updateResult = await this.engine.runStatement(` + UPDATE sessions + SET session_id = $sessionId, last_accessed = $timestamp + WHERE user_id = $userId + `, { + $userId: userId, + $sessionId: sessionId, + $timestamp: currentTimestamp + }); + + // If no existing session was updated, create a new one + if (updateResult.changes === 0) { + await this.engine.runStatement(` + INSERT INTO sessions (user_id, session_id, created_at, last_accessed) + VALUES ($userId, $sessionId, $timestamp, $timestamp) + `, { + $userId: userId, + $sessionId: sessionId, + $timestamp: currentTimestamp + }); + } + + return sessionId; +}; + +SqlTiddlerDatabase.prototype.createUserSession = async function(userId, sessionId) { + const currentTimestamp = new Date().toISOString(); + await this.engine.runStatement(` + INSERT INTO sessions (user_id, session_id, created_at, last_accessed) + VALUES ($userId, $sessionId, $timestamp, $timestamp) + `, { + $userId: userId, + $sessionId: sessionId, + $timestamp: currentTimestamp + }); + + return sessionId; +}; -async getUser(userId) { - return await this.engine.runStatementGet(` - SELECT * FROM users WHERE user_id = $userId -`, { - $userId: userId - }); -} - -async getUserByUsername(username) { - return await this.engine.runStatementGet(` - SELECT * FROM users WHERE username = $username -`, { - $username: username - }); -} - -async getUserByEmail(email) { - return await this.engine.runStatementGet(` - SELECT * FROM users WHERE email = $email -`, { - $email: email - }); -} - -async listUsersByRoleId(roleId) { - return await this.engine.runStatementGetAll(` - SELECT u.* - FROM users u - JOIN user_roles ur ON u.user_id = ur.user_id - WHERE ur.role_id = $roleId - ORDER BY u.username -`, { - $roleId: roleId - }); -} - -async updateUser(userId, username, email, roleId) { - const existingUser = await this.engine.runStatementGet(` -SELECT user_id FROM users -WHERE email = $email AND user_id != $userId -`, { - $email: email, - $userId: userId - }); - - if (existingUser.length > 0) { - return { - success: false, - message: "Email address already in use by another user." - }; - } - - try { - await this.engine.transaction(async () => { - // Update user information - await this.engine.runStatement(` - UPDATE users - SET username = $username, email = $email - WHERE user_id = $userId - `, { - $userId: userId, - $username: username, - $email: email - }); - - if (roleId) { - // Remove all existing roles for the user - await this.engine.runStatement(` - DELETE FROM user_roles - WHERE user_id = $userId - `, { - $userId: userId - }); - - // Add the new role - await this.engine.runStatement(` - INSERT INTO user_roles (user_id, role_id) - VALUES ($userId, $roleId) - `, { - $userId: userId, - $roleId: roleId - }); - } - }); - - return { - success: true, - message: "User profile and role updated successfully." - }; - } catch (error) { - return { - success: false, - message: "Failed to update user profile: " + error.message - }; - } -} - -async updateUserPassword(userId, newHash) { - try { - await this.engine.runStatement(` - UPDATE users - SET password = $newHash - WHERE user_id = $userId -`, { - $userId: userId, - $newHash: newHash, - }); - - return { - success: true, - message: "Password updated successfully." - }; - } catch (error) { - return { - success: false, - message: "Failed to update password: " + error.message - }; - } -} - -async deleteUser(userId) { - await this.engine.runStatement(` - DELETE FROM users WHERE user_id = $userId -`, { - $userId: userId - }); -} - -async listUsers() { - return await this.engine.runStatementGetAll(` - SELECT * FROM users ORDER BY username -`); -} - -async createOrUpdateUserSession(userId, sessionId) { - const currentTimestamp = new Date().toISOString(); - - // First, try to update an existing session - const updateResult = await this.engine.runStatement(` - UPDATE sessions - SET session_id = $sessionId, last_accessed = $timestamp - WHERE user_id = $userId -`, { - $userId: userId, - $sessionId: sessionId, - $timestamp: currentTimestamp - }); - - // If no existing session was updated, create a new one - if (updateResult.changes === 0) { - await this.engine.runStatement(` - INSERT INTO sessions (user_id, session_id, created_at, last_accessed) - VALUES ($userId, $sessionId, $timestamp, $timestamp) - `, { - $userId: userId, - $sessionId: sessionId, - $timestamp: currentTimestamp - }); - } - - return sessionId; -} - -async createUserSession(userId, sessionId) { - const currentTimestamp = new Date().toISOString(); - await this.engine.runStatement(` - INSERT INTO sessions (user_id, session_id, created_at, last_accessed) - VALUES ($userId, $sessionId, $timestamp, $timestamp) -`, { - $userId: userId, - $sessionId: sessionId, - $timestamp: currentTimestamp - }); - - return sessionId; -} /** * @typedef {Object} User @@ -1044,457 +1046,454 @@ async createUserSession(userId, sessionId) { * @param {any} sessionId * @returns {Promise} */ -async findUserBySessionId(sessionId) { - // First, get the user_id from the sessions table - const sessionResult = await this.engine.runStatementGet(` - SELECT user_id, last_accessed - FROM sessions - WHERE session_id = $sessionId -`, { - $sessionId: sessionId - }); - - if (!sessionResult) { - return null; // Session not found - } - - const lastAccessed = new Date(sessionResult.last_accessed); - const expirationTime = 24 * 60 * 60 * 1000; // 24 hours in milliseconds - if (+new Date() - +lastAccessed > expirationTime) { - // Session has expired - await this.deleteSession(sessionId); - return null; - } - - // Update the last_accessed timestamp - const currentTimestamp = new Date().toISOString(); - await this.engine.runStatement(` - UPDATE sessions - SET last_accessed = $timestamp - WHERE session_id = $sessionId -`, { - $sessionId: sessionId, - $timestamp: currentTimestamp - }); - - /** @type {any} */ - const userResult = await this.engine.runStatementGet(` - SELECT * - FROM users - WHERE user_id = $userId -`, { - $userId: sessionResult.user_id - }); - - if (!userResult) { - return null; - } - - /** @type {User} */ - return userResult; -} - -async deleteSession(sessionId) { - await this.engine.runStatement(` - DELETE FROM sessions - WHERE session_id = $sessionId -`, { - $sessionId: sessionId - }); -} - -async deleteUserSessions(userId) { - await this.engine.runStatement(` - DELETE FROM sessions - WHERE user_id = $userId -`, { - $userId: userId - }); -} +SqlTiddlerDatabase.prototype.findUserBySessionId = async function(sessionId) { + // First, get the user_id from the sessions table + const sessionResult = await this.engine.runStatementGet(` + SELECT user_id, last_accessed + FROM sessions + WHERE session_id = $sessionId + `, { + $sessionId: sessionId + }); + + if (!sessionResult) { + return null; // Session not found + } + + const lastAccessed = new Date(sessionResult.last_accessed); + const expirationTime = 24 * 60 * 60 * 1000; // 24 hours in milliseconds + if (new Date() - lastAccessed > expirationTime) { + // Session has expired + await this.deleteSession(sessionId); + return null; + } + + // Update the last_accessed timestamp + const currentTimestamp = new Date().toISOString(); + await this.engine.runStatement(` + UPDATE sessions + SET last_accessed = $timestamp + WHERE session_id = $sessionId + `, { + $sessionId: sessionId, + $timestamp: currentTimestamp + }); + + const userResult = await this.engine.runStatementGet(` + SELECT * + FROM users + WHERE user_id = $userId + `, { + $userId: sessionResult.user_id + }); + + if (!userResult) { + return null; + } + + return userResult; +}; + +SqlTiddlerDatabase.prototype.deleteSession = async function(sessionId) { + await this.engine.runStatement(` + DELETE FROM sessions + WHERE session_id = $sessionId + `, { + $sessionId: sessionId + }); +}; + +SqlTiddlerDatabase.prototype.deleteUserSessions = async function(userId) { + await this.engine.runStatement(` + DELETE FROM sessions + WHERE user_id = $userId + `, { + $userId: userId + }); +}; // Set the user as an admin -async setUserAdmin(userId) { - var admin = await this.getRoleByName("ADMIN"); - if (admin) { - await this.addRoleToUser(userId, admin.role_id); - } -} +SqlTiddlerDatabase.prototype.setUserAdmin = async function(userId) { + var admin = await this.getRoleByName("ADMIN"); + if(admin) { + await this.addRoleToUser(userId, admin.role_id); + } +}; // Group CRUD operations -async createGroup(groupName, description) { - const result = await this.engine.runStatement(` - INSERT INTO groups (group_name, description) - VALUES ($groupName, $description) -`, { - $groupName: groupName, - $description: description - }); - return result.lastInsertRowid; -} - -async getGroup(groupId) { - return await this.engine.runStatementGet(` - SELECT * FROM groups WHERE group_id = $groupId -`, { - $groupId: groupId - }); -} - -async updateGroup(groupId, groupName, description) { - await this.engine.runStatement(` - UPDATE groups - SET group_name = $groupName, description = $description - WHERE group_id = $groupId -`, { - $groupId: groupId, - $groupName: groupName, - $description: description - }); -} - -async deleteGroup(groupId) { - await this.engine.runStatement(` - DELETE FROM groups WHERE group_id = $groupId -`, { - $groupId: groupId - }); -} - -async listGroups() { - return await this.engine.runStatementGetAll(` - SELECT * FROM groups ORDER BY group_name -`); -} +SqlTiddlerDatabase.prototype.createGroup = async function(groupName, description) { + const result = await this.engine.runStatement(` + INSERT INTO groups (group_name, description) + VALUES ($groupName, $description) + `, { + $groupName: groupName, + $description: description + }); + return result.lastInsertRowid; +}; + +SqlTiddlerDatabase.prototype.getGroup = async function(groupId) { + return await this.engine.runStatementGet(` + SELECT * FROM groups WHERE group_id = $groupId + `, { + $groupId: groupId + }); +}; + +SqlTiddlerDatabase.prototype.updateGroup = async function(groupId, groupName, description) { + await this.engine.runStatement(` + UPDATE groups + SET group_name = $groupName, description = $description + WHERE group_id = $groupId + `, { + $groupId: groupId, + $groupName: groupName, + $description: description + }); +}; + +SqlTiddlerDatabase.prototype.deleteGroup = async function(groupId) { + await this.engine.runStatement(` + DELETE FROM groups WHERE group_id = $groupId + `, { + $groupId: groupId + }); +}; + +SqlTiddlerDatabase.prototype.listGroups = async function() { + return await this.engine.runStatementGetAll(` + SELECT * FROM groups ORDER BY group_name + `); +}; // Role CRUD operations -async createRole(roleName, description) { - const result = await this.engine.runStatement(` - INSERT OR IGNORE INTO roles (role_name, description) - VALUES ($roleName, $description) -`, { - $roleName: roleName, - $description: description - }); - return result.lastInsertRowid; -} - -async getRole(roleId) { - return await this.engine.runStatementGet(` - SELECT * FROM roles WHERE role_id = $roleId -`, { - $roleId: roleId - }); -} - -async getRoleByName(roleName) { - return await this.engine.runStatementGet(` - SELECT * FROM roles WHERE role_name = $roleName -`, { - $roleName: roleName - }); -} - -async updateRole(roleId, roleName, description) { - await this.engine.runStatement(` - UPDATE roles - SET role_name = $roleName, description = $description - WHERE role_id = $roleId -`, { - $roleId: roleId, - $roleName: roleName, - $description: description - }); -} - -async deleteRole(roleId) { - await this.engine.runStatement(` - DELETE FROM roles WHERE role_id = $roleId -`, { - $roleId: roleId - }); -} - -async listRoles() { - return await this.engine.runStatementGetAll(` - SELECT * FROM roles ORDER BY role_name DESC -`); -} +SqlTiddlerDatabase.prototype.createRole = async function(roleName, description) { + const result = await this.engine.runStatement(` + INSERT OR IGNORE INTO roles (role_name, description) + VALUES ($roleName, $description) + `, { + $roleName: roleName, + $description: description + }); + return result.lastInsertRowid; +}; + +SqlTiddlerDatabase.prototype.getRole = async function(roleId) { + return await this.engine.runStatementGet(` + SELECT * FROM roles WHERE role_id = $roleId + `, { + $roleId: roleId + }); +}; + +SqlTiddlerDatabase.prototype.getRoleByName = async function(roleName) { + return await this.engine.runStatementGet(` + SELECT * FROM roles WHERE role_name = $roleName + `, { + $roleName: roleName + }); +} + +SqlTiddlerDatabase.prototype.updateRole = async function(roleId, roleName, description) { + await this.engine.runStatement(` + UPDATE roles + SET role_name = $roleName, description = $description + WHERE role_id = $roleId + `, { + $roleId: roleId, + $roleName: roleName, + $description: description + }); +}; + +SqlTiddlerDatabase.prototype.deleteRole = async function(roleId) { + await this.engine.runStatement(` + DELETE FROM roles WHERE role_id = $roleId + `, { + $roleId: roleId + }); +}; + +SqlTiddlerDatabase.prototype.listRoles = async function() { + return await this.engine.runStatementGetAll(` + SELECT * FROM roles ORDER BY role_name DESC + `); +}; // Permission CRUD operations -async createPermission(permissionName, description) { - const result = await this.engine.runStatement(` -INSERT OR IGNORE INTO permissions (permission_name, description) -VALUES ($permissionName, $description) -`, { - $permissionName: permissionName, - $description: description - }); - return result.lastInsertRowid; -} - -async getPermission(permissionId) { - return await this.engine.runStatementGet(` - SELECT * FROM permissions WHERE permission_id = $permissionId -`, { - $permissionId: permissionId - }); -} - -async getPermissionByName(permissionName) { - return await this.engine.runStatementGet(` - SELECT * FROM permissions WHERE permission_name = $permissionName -`, { - $permissionName: permissionName - }); -} - -async updatePermission(permissionId, permissionName, description) { - await this.engine.runStatement(` - UPDATE permissions - SET permission_name = $permissionName, description = $description - WHERE permission_id = $permissionId -`, { - $permissionId: permissionId, - $permissionName: permissionName, - $description: description - }); -} - -async deletePermission(permissionId) { - await this.engine.runStatement(` - DELETE FROM permissions WHERE permission_id = $permissionId -`, { - $permissionId: permissionId - }); -} - -async listPermissions() { - return await this.engine.runStatementGetAll(` - SELECT * FROM permissions ORDER BY permission_name -`); -} +SqlTiddlerDatabase.prototype.createPermission = async function(permissionName, description) { + const result = await this.engine.runStatement(` + INSERT OR IGNORE INTO permissions (permission_name, description) + VALUES ($permissionName, $description) + `, { + $permissionName: permissionName, + $description: description + }); + return result.lastInsertRowid; +}; + +SqlTiddlerDatabase.prototype.getPermission = async function(permissionId) { + return await this.engine.runStatementGet(` + SELECT * FROM permissions WHERE permission_id = $permissionId + `, { + $permissionId: permissionId + }); +}; + +SqlTiddlerDatabase.prototype.getPermissionByName = async function(permissionName) { + return await this.engine.runStatementGet(` + SELECT * FROM permissions WHERE permission_name = $permissionName + `, { + $permissionName: permissionName + }); +}; + +SqlTiddlerDatabase.prototype.updatePermission = async function(permissionId, permissionName, description) { + await this.engine.runStatement(` + UPDATE permissions + SET permission_name = $permissionName, description = $description + WHERE permission_id = $permissionId + `, { + $permissionId: permissionId, + $permissionName: permissionName, + $description: description + }); +}; + +SqlTiddlerDatabase.prototype.deletePermission = async function(permissionId) { + await this.engine.runStatement(` + DELETE FROM permissions WHERE permission_id = $permissionId + `, { + $permissionId: permissionId + }); +}; + +SqlTiddlerDatabase.prototype.listPermissions = async function() { + return await this.engine.runStatementGetAll(` + SELECT * FROM permissions ORDER BY permission_name + `); +}; // ACL CRUD operations -async createACL(entityName, entityType, roleId, permissionId) { - if (!entityName.startsWith("$:/")) { - const result = await this.engine.runStatement(` - INSERT OR IGNORE INTO acl (entity_name, entity_type, role_id, permission_id) - VALUES ($entityName, $entityType, $roleId, $permissionId) -`, - { - $entityName: entityName, - $entityType: entityType, - $roleId: roleId, - $permissionId: permissionId - }); - return result.lastInsertRowid; - } -} - -async getACL(aclId) { - return await this.engine.runStatementGet(` - SELECT * FROM acl WHERE acl_id = $aclId -`, { - $aclId: aclId - }); -} - -async updateACL(aclId, entityId, entityType, roleId, permissionId) { - await this.engine.runStatement(` - UPDATE acl - SET entity_name = $entityId, entity_type = $entityType, - role_id = $roleId, permission_id = $permissionId - WHERE acl_id = $aclId -`, { - $aclId: aclId, - $entityId: entityId, - $entityType: entityType, - $roleId: roleId, - $permissionId: permissionId - }); -} - -async deleteACL(aclId) { - await this.engine.runStatement(` - DELETE FROM acl WHERE acl_id = $aclId -`, { - $aclId: aclId - }); -} - -async listACLs() { - return await this.engine.runStatementGetAll(` - SELECT * FROM acl ORDER BY entity_type, entity_name -`); -} +SqlTiddlerDatabase.prototype.createACL = async function(entityName, entityType, roleId, permissionId) { + if(!entityName.startsWith("$:/")) { + const result = await this.engine.runStatement(` + INSERT OR IGNORE INTO acl (entity_name, entity_type, role_id, permission_id) + VALUES ($entityName, $entityType, $roleId, $permissionId) + `, + { + $entityName: entityName, + $entityType: entityType, + $roleId: roleId, + $permissionId: permissionId + }); + return result.lastInsertRowid; + } +}; + +SqlTiddlerDatabase.prototype.getACL = async function(aclId) { + return await this.engine.runStatementGet(` + SELECT * FROM acl WHERE acl_id = $aclId + `, { + $aclId: aclId + }); +}; + +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 + WHERE acl_id = $aclId + `, { + $aclId: aclId, + $entityId: entityId, + $entityType: entityType, + $roleId: roleId, + $permissionId: permissionId + }); +}; + +SqlTiddlerDatabase.prototype.deleteACL = async function(aclId) { + await this.engine.runStatement(` + DELETE FROM acl WHERE acl_id = $aclId + `, { + $aclId: aclId + }); +}; + +SqlTiddlerDatabase.prototype.listACLs = async function() { + return await this.engine.runStatementGetAll(` + SELECT * FROM acl ORDER BY entity_type, entity_name + `); +}; // Association management functions -async addUserToGroup(userId, groupId) { - await this.engine.runStatement(` - INSERT OR IGNORE INTO user_groups (user_id, group_id) - VALUES ($userId, $groupId) -`, { - $userId: userId, - $groupId: groupId - }); -} - -async isUserInGroup(userId, groupId) { - const result = await this.engine.runStatementGet(` - SELECT 1 FROM user_groups - WHERE user_id = $userId AND group_id = $groupId -`, { - $userId: userId, - $groupId: groupId - }); - return result !== undefined; -} - -async removeUserFromGroup(userId, groupId) { - await this.engine.runStatement(` - DELETE FROM user_groups - WHERE user_id = $userId AND group_id = $groupId -`, { - $userId: userId, - $groupId: groupId - }); -} - -async addRoleToUser(userId, roleId) { - await this.engine.runStatement(` - INSERT OR IGNORE INTO user_roles (user_id, role_id) - VALUES ($userId, $roleId) -`, { - $userId: userId, - $roleId: roleId - }); -} - -async removeRoleFromUser(userId, roleId) { - await this.engine.runStatement(` - DELETE FROM user_roles - WHERE user_id = $userId AND role_id = $roleId -`, { - $userId: userId, - $roleId: roleId - }); -} - -async addRoleToGroup(groupId, roleId) { - await this.engine.runStatement(` - INSERT OR IGNORE INTO group_roles (group_id, role_id) - VALUES ($groupId, $roleId) -`, { - $groupId: groupId, - $roleId: roleId - }); -} - -async removeRoleFromGroup(groupId, roleId) { - await this.engine.runStatement(` - DELETE FROM group_roles - WHERE group_id = $groupId AND role_id = $roleId -`, { - $groupId: groupId, - $roleId: roleId - }); -} - -async addPermissionToRole(roleId, permissionId) { - await this.engine.runStatement(` - INSERT OR IGNORE INTO role_permissions (role_id, permission_id) - VALUES ($roleId, $permissionId) -`, { - $roleId: roleId, - $permissionId: permissionId - }); -} - -async removePermissionFromRole(roleId, permissionId) { - await this.engine.runStatement(` - DELETE FROM role_permissions - WHERE role_id = $roleId AND permission_id = $permissionId -`, { - $roleId: roleId, - $permissionId: permissionId - }); -} - -async getUserRoles(userId) { - const query = ` - SELECT r.role_id, r.role_name - FROM user_roles ur - JOIN roles r ON ur.role_id = r.role_id - WHERE ur.user_id = $userId - LIMIT 1 -`; - - return await this.engine.runStatementGet(query, { $userId: userId }); -} - -async deleteUserRolesByRoleId(roleId) { - await this.engine.runStatement(` - DELETE FROM user_roles - WHERE role_id = $roleId -`, { - $roleId: roleId - }); -} - -async deleteUserRolesByUserId(userId) { - await this.engine.runStatement(` - DELETE FROM user_roles - WHERE user_id = $userId -`, { - $userId: userId - }); -} - -async isRoleInUse(roleId) { - // Check if the role is assigned to any users - const userRoleCheck = await this.engine.runStatementGet(` -SELECT 1 -FROM user_roles -WHERE role_id = $roleId -LIMIT 1 -`, { - $roleId: roleId - }); - - if (userRoleCheck) { - return true; - } - - // Check if the role is used in any ACLs - const aclRoleCheck = await this.engine.runStatementGet(` -SELECT 1 -FROM acl -WHERE role_id = $roleId -LIMIT 1 -`, { - $roleId: roleId - }); - - if (aclRoleCheck) { - return true; - } - - // If we've reached this point, the role is not in use - return false; -} - -async getRoleById(roleId) { - const role = await this.engine.runStatementGet(` -SELECT role_id, role_name, description -FROM roles -WHERE role_id = $roleId -`, { - $roleId: roleId - }); - - return role; -} -} +SqlTiddlerDatabase.prototype.addUserToGroup = async function(userId, groupId) { + await this.engine.runStatement(` + INSERT OR IGNORE INTO user_groups (user_id, group_id) + VALUES ($userId, $groupId) + `, { + $userId: userId, + $groupId: groupId + }); +}; + +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 + `, { + $userId: userId, + $groupId: groupId + }); + return result !== undefined; +}; + +SqlTiddlerDatabase.prototype.removeUserFromGroup = async function(userId, groupId) { + await this.engine.runStatement(` + DELETE FROM user_groups + WHERE user_id = $userId AND group_id = $groupId + `, { + $userId: userId, + $groupId: groupId + }); +}; + +SqlTiddlerDatabase.prototype.addRoleToUser = async function(userId, roleId) { + await this.engine.runStatement(` + INSERT OR IGNORE INTO user_roles (user_id, role_id) + VALUES ($userId, $roleId) + `, { + $userId: userId, + $roleId: roleId + }); +}; + +SqlTiddlerDatabase.prototype.removeRoleFromUser = async function(userId, roleId) { + await this.engine.runStatement(` + DELETE FROM user_roles + WHERE user_id = $userId AND role_id = $roleId + `, { + $userId: userId, + $roleId: roleId + }); +}; + +SqlTiddlerDatabase.prototype.addRoleToGroup = async function(groupId, roleId) { + await this.engine.runStatement(` + INSERT OR IGNORE INTO group_roles (group_id, role_id) + VALUES ($groupId, $roleId) + `, { + $groupId: groupId, + $roleId: roleId + }); +}; + +SqlTiddlerDatabase.prototype.removeRoleFromGroup = async function(groupId, roleId) { + await this.engine.runStatement(` + DELETE FROM group_roles + WHERE group_id = $groupId AND role_id = $roleId + `, { + $groupId: groupId, + $roleId: roleId + }); +}; + +SqlTiddlerDatabase.prototype.addPermissionToRole = async function(roleId, permissionId) { + await this.engine.runStatement(` + INSERT OR IGNORE INTO role_permissions (role_id, permission_id) + VALUES ($roleId, $permissionId) + `, { + $roleId: roleId, + $permissionId: permissionId + }); +}; + +SqlTiddlerDatabase.prototype.removePermissionFromRole = async function(roleId, permissionId) { + await this.engine.runStatement(` + DELETE FROM role_permissions + WHERE role_id = $roleId AND permission_id = $permissionId + `, { + $roleId: roleId, + $permissionId: permissionId + }); +}; + +SqlTiddlerDatabase.prototype.getUserRoles = async function(userId) { + const query = ` + SELECT r.role_id, r.role_name + FROM user_roles ur + JOIN roles r ON ur.role_id = r.role_id + WHERE ur.user_id = $userId + LIMIT 1 + `; + + return await this.engine.runStatementGet(query, { $userId: userId }); +}; + +SqlTiddlerDatabase.prototype.deleteUserRolesByRoleId = async function(roleId) { + await this.engine.runStatement(` + DELETE FROM user_roles + WHERE role_id = $roleId + `, { + $roleId: roleId + }); +}; + +SqlTiddlerDatabase.prototype.deleteUserRolesByUserId = async function(userId) { + await this.engine.runStatement(` + DELETE FROM user_roles + WHERE user_id = $userId + `, { + $userId: userId + }); +}; + +SqlTiddlerDatabase.prototype.isRoleInUse = async function(roleId) { + // Check if the role is assigned to any users + const userRoleCheck = await this.engine.runStatementGet(` + SELECT 1 + FROM user_roles + WHERE role_id = $roleId + LIMIT 1 + `, { + $roleId: roleId + }); + + if(userRoleCheck) { + return true; + } + + // Check if the role is used in any ACLs + const aclRoleCheck = await this.engine.runStatementGet(` + SELECT 1 + FROM acl + WHERE role_id = $roleId + LIMIT 1 + `, { + $roleId: roleId + }); + + if(aclRoleCheck) { + return true; + } + + // If we've reached this point, the role is not in use + return false; +}; + +SqlTiddlerDatabase.prototype.getRoleById = async function(roleId) { + const role = await this.engine.runStatementGet(` + SELECT role_id, role_name, description + FROM roles + WHERE role_id = $roleId + `, { + $roleId: roleId + }); + + return role; +}; exports.SqlTiddlerDatabase = SqlTiddlerDatabase; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js index 09edb06c025..5d94fc3dfa1 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js @@ -13,34 +13,32 @@ This class is largely a wrapper for the sql-tiddler-database.js class, adding th \*/ -(function () { +(function() { -// /* -// Create a tiddler store. Options include: - -// databasePath - path to the database file (can be ":memory:" to get a temporary database) -// adminWiki - reference to $tw.Wiki object used for configuration -// attachmentStore - reference to associated attachment store -// engine - wasm | better -// */ +/* +Create a tiddler store. Options include: -class SqlTiddlerStore { +databasePath - path to the database file (can be ":memory:" to get a temporary database) +adminWiki - reference to $tw.Wiki object used for configuration +attachmentStore - reference to associated attachment store +engine - wasm | better +*/ /** - * @class SqlTiddlerStore - * @param {{ - * databasePath?: String, - * adminWiki?: $TW.Wiki, - * attachmentStore?: import("./attachments").AttachmentStore, - * engine?: String - * }} options + * + * @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" */ -constructor(options) { - options = options || {}; +function SqlTiddlerStore(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 this.eventOutstanding = {}; // Hashmap by type of boolean true of outstanding events - // Create the database this.databasePath = options.databasePath || ":memory:"; var SqlTiddlerDatabase = require("$:/plugins/tiddlywiki/multiwikiserver/store/sql-tiddler-database.js").SqlTiddlerDatabase; @@ -48,55 +46,51 @@ constructor(options) { databasePath: this.databasePath, engine: options.engine }); - const error = new Error("syncCheck"); - this.syncCheck = setTimeout(() => { - console.error(error); - }); + } - -async initCheck() { - clearTimeout(this.syncCheck); - this.syncCheck = undefined; +SqlTiddlerStore.prototype.init = async function() { + await this.sqlTiddlerDatabase.init(); await this.sqlTiddlerDatabase.createTables(); -} +}; -addEventListener(type, listener) { - this.eventListeners[type] = this.eventListeners[type] || []; +SqlTiddlerStore.prototype.addEventListener = function(type,listener) { + this.eventListeners[type] = this.eventListeners[type] || []; this.eventListeners[type].push(listener); -} +}; -removeEventListener(type, listener) { +SqlTiddlerStore.prototype.removeEventListener = function(type,listener) { const listeners = this.eventListeners[type]; if(listeners) { var p = listeners.indexOf(listener); if(p !== -1) { - listeners.splice(p, 1); + listeners.splice(p,1); } } -} +}; -dispatchEvent(type /*, args */) { +SqlTiddlerStore.prototype.dispatchEvent = function(type /*, args */) { const self = this; if(!this.eventOutstanding[type]) { - $tw.utils.nextTick(function () { + $tw.utils.nextTick(function() { self.eventOutstanding[type] = false; - const args = Array.prototype.slice.call(arguments, 1), listeners = self.eventListeners[type]; + const args = Array.prototype.slice.call(arguments,1), + listeners = self.eventListeners[type]; if(listeners) { - for(var p = 0; p < listeners.length; p++) { + for(var p=0; p attachmentSizeLimit; - - if(existing_attachment_blob) { - const fileSize = this.attachmentStore.getAttachmentFileSize(existing_attachment_blob); - if(fileSize <= attachmentSizeLimit) { - const existingAttachmentMeta = this.attachmentStore.getAttachmentMetadata(existing_attachment_blob); - const hasCanonicalField = !!tiddlerFields._canonical_uri; - const skipAttachment = hasCanonicalField && (tiddlerFields._canonical_uri === (existingAttachmentMeta ? existingAttachmentMeta._canonical_uri : existing_canonical_uri)); - shouldProcessAttachment = !skipAttachment; - } else { - shouldProcessAttachment = false; - } - } - - if(attachmentsEnabled && isBinary && shouldProcessAttachment) { - const attachment_blob = existing_attachment_blob || this.attachmentStore.saveAttachment({ - text: tiddlerFields.text, - type: tiddlerFields.type, - reference: tiddlerFields.title, - _canonical_uri: tiddlerFields._canonical_uri - }); - - if(tiddlerFields && tiddlerFields._canonical_uri) { - delete tiddlerFields._canonical_uri; - } - - return { - tiddlerFields: Object.assign({}, tiddlerFields, { text: undefined }), - attachment_blob: attachment_blob - }; - } else { - return { - tiddlerFields: tiddlerFields, - attachment_blob: existing_attachment_blob - }; - } -} - -async saveTiddlersFromPath(tiddler_files_path, bag_name) { + const attachmentsEnabled = this.adminWiki.getTiddlerText("$:/config/MultiWikiServer/EnableAttachments", "yes") === "yes"; + const contentTypeInfo = $tw.config.contentTypeInfo[tiddlerFields.type || "text/vnd.tiddlywiki"]; + const isBinary = !!contentTypeInfo && contentTypeInfo.encoding === "base64"; + + let shouldProcessAttachment = tiddlerFields.text && tiddlerFields.text.length > attachmentSizeLimit; + + if(existing_attachment_blob) { + const fileSize = this.attachmentStore.getAttachmentFileSize(existing_attachment_blob); + if(fileSize <= attachmentSizeLimit) { + const existingAttachmentMeta = this.attachmentStore.getAttachmentMetadata(existing_attachment_blob); + const hasCanonicalField = !!tiddlerFields._canonical_uri; + const skipAttachment = hasCanonicalField && (tiddlerFields._canonical_uri === (existingAttachmentMeta ? existingAttachmentMeta._canonical_uri : existing_canonical_uri)); + shouldProcessAttachment = !skipAttachment; + } else { + shouldProcessAttachment = false; + } + } + + if(attachmentsEnabled && isBinary && shouldProcessAttachment) { + const attachment_blob = existing_attachment_blob || this.attachmentStore.saveAttachment({ + text: tiddlerFields.text, + type: tiddlerFields.type, + reference: tiddlerFields.title, + _canonical_uri: tiddlerFields._canonical_uri + }); + + if(tiddlerFields && tiddlerFields._canonical_uri) { + delete tiddlerFields._canonical_uri; + } + + return { + tiddlerFields: Object.assign({}, tiddlerFields, { text: undefined }), + attachment_blob: attachment_blob + }; + } else { + return { + tiddlerFields: tiddlerFields, + attachment_blob: existing_attachment_blob + }; + } +}; + +SqlTiddlerStore.prototype.saveTiddlersFromPath = async function(tiddler_files_path,bag_name) { var self = this; - await this.sqlTiddlerDatabase.transaction(async function () { + await this.sqlTiddlerDatabase.transaction(async function() { // Clear out the bag 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)); + var tiddlersFromPath = $tw.loadTiddlersFromPath(path.resolve($tw.boot.corePath,$tw.config.editionsPath,tiddler_files_path)); // Save the tiddlers for(const tiddlersFromFile of tiddlersFromPath) { for(const tiddler of tiddlersFromFile.tiddlers) { - await self.saveBagTiddler(tiddler, bag_name); + await self.saveBagTiddler(tiddler,bag_name,null); } } }); self.dispatchEvent("change"); -} +}; -async listBags() { +SqlTiddlerStore.prototype.listBags = async function() { return await this.sqlTiddlerDatabase.listBags(); -} +}; /* Options include: allowPrivilegedCharacters - allows "$", ":" and "/" to appear in recipe name */ -async createBag(bag_name, description, options) { +SqlTiddlerStore.prototype.createBag = async function(bag_name,description,options) { options = options || {}; var self = this; - return await this.sqlTiddlerDatabase.transaction(async function () { - const validationBagName = self.validateItemName(bag_name, options.allowPrivilegedCharacters); + return await this.sqlTiddlerDatabase.transaction(async function() { + const validationBagName = self.validateItemName(bag_name,options.allowPrivilegedCharacters); if(validationBagName) { - return { message: validationBagName }; + return {message: validationBagName}; } - await self.sqlTiddlerDatabase.createBag(bag_name, description); + await self.sqlTiddlerDatabase.createBag(bag_name,description); self.dispatchEvent("change"); return null; }); -} +}; -async listRecipes() { +SqlTiddlerStore.prototype.listRecipes = async function() { return await this.sqlTiddlerDatabase.listRecipes(); -} +}; /* Returns null on success, or {message:} on error @@ -263,39 +257,39 @@ Options include: allowPrivilegedCharacters - allows "$", ":" and "/" to appear in recipe name */ -async createRecipe(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 || {}; - const validationRecipeName = this.validateItemName(recipe_name, options.allowPrivilegedCharacters); + const validationRecipeName = this.validateItemName(recipe_name,options.allowPrivilegedCharacters); if(validationRecipeName) { - return { message: validationRecipeName }; + return {message: validationRecipeName}; } if(bag_names.length === 0) { - return { message: "Recipes must contain at least one bag" }; + return {message: "Recipes must contain at least one bag"}; } var self = this; - return await this.sqlTiddlerDatabase.transaction(async function () { - await 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; }); -} +}; /* Returns {tiddler_id:} */ -async saveBagTiddler(incomingTiddlerFields, bag_name) { +SqlTiddlerStore.prototype.saveBagTiddler = async function(incomingTiddlerFields,bag_name) { let _canonical_uri; - const existing_attachment_blob = await 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`; + _canonical_uri = `/bags/${$tw.utils.encodeURIComponentExtended(bag_name)}/tiddlers/${$tw.utils.encodeURIComponentExtended(incomingTiddlerFields.title)}/blob` } - const{ tiddlerFields, attachment_blob } = this.processIncomingTiddler(incomingTiddlerFields, existing_attachment_blob, _canonical_uri); - const result = await this.sqlTiddlerDatabase.saveBagTiddler(tiddlerFields, bag_name, attachment_blob); + const {tiddlerFields, attachment_blob} = this.processIncomingTiddler(incomingTiddlerFields,existing_attachment_blob,_canonical_uri); + const result = await this.sqlTiddlerDatabase.saveBagTiddler(tiddlerFields,bag_name,attachment_blob); this.dispatchEvent("change"); return result; -} +}; /* Create a tiddler in a bag adopting the specified file as the attachment. The attachment file must be on the same disk as the attachment store @@ -307,50 +301,50 @@ type - content type of file as uploaded Returns {tiddler_id:} */ -async saveBagTiddlerWithAttachment(incomingTiddlerFields, bag_name, options) { - const attachment_blob = this.attachmentStore.adoptAttachment(options.filepath, options.type, options.hash, options._canonical_uri); +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 = await 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 { return null; } -} +}; /* Returns {tiddler_id:,bag_name:} */ -async saveRecipeTiddler(incomingTiddlerFields, recipe_name) { - const existing_attachment_blob = await this.sqlTiddlerDatabase.getRecipeTiddlerAttachmentBlob(incomingTiddlerFields.title, recipe_name); - const{ tiddlerFields, attachment_blob } = await this.processIncomingTiddler(incomingTiddlerFields, existing_attachment_blob, incomingTiddlerFields._canonical_uri); - const result = await this.sqlTiddlerDatabase.saveRecipeTiddler(tiddlerFields, recipe_name, attachment_blob); +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 = await this.sqlTiddlerDatabase.saveRecipeTiddler(tiddlerFields,recipe_name,attachment_blob); this.dispatchEvent("change"); return result; -} +}; -async deleteTiddler(title, bag_name) { - const result = await 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; -} +}; /* returns {tiddler_id:,tiddler:} */ -async getBagTiddler(title, bag_name) { - var tiddlerInfo = await 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, { - tiddler: this.processOutgoingTiddler(tiddlerInfo.tiddler, tiddlerInfo.tiddler_id, bag_name, tiddlerInfo.attachment_blob) - }); + tiddler: this.processOutgoingTiddler(tiddlerInfo.tiddler,tiddlerInfo.tiddler_id,bag_name,tiddlerInfo.attachment_blob) + }); } else { return null; } -} +}; /* Get an attachment ready to stream. Returns null if there is an error or: @@ -359,8 +353,8 @@ stream: stream of file type: type of file Returns {tiddler_id:,bag_name:} */ -async getBagTiddlerStream(title, bag_name) { - const tiddlerInfo = await 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( @@ -372,12 +366,12 @@ async getBagTiddlerStream(title, bag_name) { } ); } else { - const{ Readable } = require("stream"); + const { Readable } = require('stream'); const stream = new Readable(); - stream._read = function () { + stream._read = function() { // Push data const type = tiddlerInfo.tiddler.type || "text/plain"; - stream.push(tiddlerInfo.tiddler.text || "", ($tw.config.contentTypeInfo[type] || { encoding: "utf8" }).encoding); + stream.push(tiddlerInfo.tiddler.text || "",($tw.config.contentTypeInfo[type] ||{encoding: "utf8"}).encoding); // Push null to indicate the end of the stream stream.push(null); }; @@ -386,74 +380,73 @@ async getBagTiddlerStream(title, bag_name) { bag_name: bag_name, stream: stream, type: tiddlerInfo.tiddler.type || "text/plain" - }; + } } } else { return null; } -} +}; /* Returns {bag_name:, tiddler: {fields}, tiddler_id:} */ -async getRecipeTiddler(title, recipe_name) { - var tiddlerInfo = await this.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( {}, tiddlerInfo, { - tiddler: this.processOutgoingTiddler(tiddlerInfo.tiddler, tiddlerInfo.tiddler_id, tiddlerInfo.bag_name, tiddlerInfo.attachment_blob) + tiddler: this.processOutgoingTiddler(tiddlerInfo.tiddler,tiddlerInfo.tiddler_id,tiddlerInfo.bag_name,tiddlerInfo.attachment_blob) }); } else { return null; } -} +}; /* Get the titles of the tiddlers in a bag. Returns an empty array for bags that do not exist */ -async getBagTiddlers(bag_name) { +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 */ -async 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 */ -async getRecipeTiddlers(recipe_name, options) { - return await 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 */ -async getRecipeLastTiddlerId(recipe_name) { +SqlTiddlerStore.prototype.getRecipeLastTiddlerId = async function(recipe_name) { return await this.sqlTiddlerDatabase.getRecipeLastTiddlerId(recipe_name); -} +}; -async deleteAllTiddlersInBag(bag_name) { +SqlTiddlerStore.prototype.deleteAllTiddlersInBag = async function(bag_name) { var self = this; - return await this.sqlTiddlerDatabase.transaction(async function () { + return await this.sqlTiddlerDatabase.transaction(async function() { const result = await self.sqlTiddlerDatabase.deleteAllTiddlersInBag(bag_name); self.dispatchEvent("change"); return result; }); -} +}; /* Get the names of the bags in a recipe. Returns an empty array for recipes that do not exist */ -async 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 8821522c0aa..1eec51bbc8d 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/tests-sql-tiddler-database.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/tests-sql-tiddler-database.js @@ -15,17 +15,14 @@ if($tw.node) { "use strict"; describe("SQL tiddler database with node built-in sqlite", function () { - // eslint-disable-next-line custom-rules/always-await void runSqlDatabaseTests("node").catch(console.error); }); describe("SQL tiddler database with node-sqlite3-wasm", function () { - // eslint-disable-next-line custom-rules/always-await void runSqlDatabaseTests("wasm").catch(console.error); }); describe("SQL tiddler database with better-sqlite3", function () { - // eslint-disable-next-line custom-rules/always-await void runSqlDatabaseTests("better").catch(console.error); }); From 01ec422bcf158954ca7417330e96c86f3dd3451c Mon Sep 17 00:00:00 2001 From: arlen22 Date: Fri, 10 Jan 2025 12:17:34 -0500 Subject: [PATCH 12/14] simple typescript errors --- .../modules/routes/handlers/get-users.js | 2 +- .../modules/routes/handlers/manage-user.js | 2 +- .../modules/routes/handlers/update-role.js | 2 +- .../modules/store/sql-tiddler-database.js | 16 ++++++++++++++-- .../modules/store/sql-tiddler-store.js | 2 +- .../modules/store/tests-sql-tiddler-store.js | 5 +++-- .../modules/tests/test-attachment.js | 4 ++-- 7 files changed, 23 insertions(+), 10 deletions(-) diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-users.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-users.js index 6beeda2fc02..2e0f7d59dfa 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-users.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-users.js @@ -29,7 +29,7 @@ exports.handler = async 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/manage-user.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-user.js index 8ec85aef97b..17e9b512ee5 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-user.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/manage-user.js @@ -46,7 +46,7 @@ exports.handler = async 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"); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-role.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-role.js index bd6bdb594cf..401211051f0 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-role.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/update-role.js @@ -26,7 +26,7 @@ exports.handler = async function(request, response, state) { 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; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js index b2af90f07a1..921da98a438 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js @@ -536,7 +536,19 @@ Checks if a user has permission to access a bag SqlTiddlerDatabase.prototype.hasBagPermission = async function(userId, bagName, permissionName) { return await this.checkACLPermission(userId, "bag", bagName, permissionName) }; - +/** + * @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) { @@ -1078,7 +1090,7 @@ SqlTiddlerDatabase.prototype.findUserBySessionId = async function(sessionId) { $sessionId: sessionId, $timestamp: currentTimestamp }); - + /** @type {any} */ const userResult = await this.engine.runStatementGet(` SELECT * FROM users diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js index 5d94fc3dfa1..4a5d4885aa3 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js @@ -171,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)); 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 a914fca9987..5fe904079ef 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/tests-sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/tests-sql-tiddler-store.js @@ -29,9 +29,10 @@ function runSqlStoreTests(engine) { beforeEach(async function() { store = new SqlTiddlerStore({ databasePath: ":memory:", - engine: engine + engine: engine, + attachmentStore: {} }); - await store.initCheck(); + await store.init(); }); afterEach(async function() { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/tests/test-attachment.js b/plugins/tiddlywiki/multiwikiserver/modules/tests/test-attachment.js index 39d09c12195..1fa84f7eee9 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/tests/test-attachment.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/tests/test-attachment.js @@ -104,7 +104,7 @@ if(typeof window === "undefined" && typeof process !== "undefined" && process.ve 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() { @@ -173,7 +173,7 @@ if(typeof window === "undefined" && typeof process !== "undefined" && process.ve 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"); } }); }); From be5dbb368bb0422f86a540da495f296683daa1c6 Mon Sep 17 00:00:00 2001 From: arlen22 Date: Fri, 10 Jan 2025 12:25:48 -0500 Subject: [PATCH 13/14] significant errors --- .../modules/routes/handlers/get-acl.js | 20 +++++++++++-------- .../modules/store/sql-tiddler-database.js | 2 +- .../modules/store/sql-tiddler-store.js | 2 +- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-acl.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-acl.js index 3494bdcdac6..b441af2b51e 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-acl.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-acl.js @@ -58,29 +58,33 @@ exports.handler = async 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/store/sql-tiddler-database.js b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js index 921da98a438..5c3325a7785 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js @@ -903,7 +903,7 @@ SqlTiddlerDatabase.prototype.listUsersByRoleId = async function(roleId) { }; SqlTiddlerDatabase.prototype.updateUser = async function (userId, username, email, roleId) { - const existingUser = await this.engine.runStatement(` + const existingUser = await this.engine.runStatementGet(` SELECT user_id FROM users WHERE email = $email AND user_id != $userId `, { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js index 4a5d4885aa3..078da381ce2 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-store.js @@ -216,7 +216,7 @@ SqlTiddlerStore.prototype.saveTiddlersFromPath = async function(tiddler_files_pa // Save the tiddlers for(const tiddlersFromFile of tiddlersFromPath) { for(const tiddler of tiddlersFromFile.tiddlers) { - await self.saveBagTiddler(tiddler,bag_name,null); + await self.saveBagTiddler(tiddler,bag_name); } } }); From d96962fc21ff5bf62f159f2e18a0b59985410555 Mon Sep 17 00:00:00 2001 From: arlen22 Date: Fri, 10 Jan 2025 15:42:01 -0500 Subject: [PATCH 14/14] Use sqlite worker thread --- .../multiwikiserver/eslint.config.js | 5 +- .../modules/store/sql-engine-worker.js | 99 +++++++++++++++ .../modules/store/sql-engine.js | 118 ++++++++++++++---- .../modules/store/sql-tiddler-database.js | 2 +- .../store/tests-sql-tiddler-database.js | 16 ++- 5 files changed, 207 insertions(+), 33 deletions(-) create mode 100644 plugins/tiddlywiki/multiwikiserver/modules/store/sql-engine-worker.js diff --git a/plugins/tiddlywiki/multiwikiserver/eslint.config.js b/plugins/tiddlywiki/multiwikiserver/eslint.config.js index 3d7a02d32bc..41490a64eeb 100644 --- a/plugins/tiddlywiki/multiwikiserver/eslint.config.js +++ b/plugins/tiddlywiki/multiwikiserver/eslint.config.js @@ -165,9 +165,10 @@ module.exports = tsLint.config( "id-length": "off", "id-match": "error", "implicit-arrow-linebreak": "error", - "indent": "off", + // "indent": "warn", // "indent": ["warn", "tab", { - // "outerIIFEBody": 0 + // "outerIIFEBody": 0 , + // "SwitchCase": 1, // }], "indent-legacy": "off", "init-declarations": "off", 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 b9b3926cd5a..a8a14812baa 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-engine.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-engine.js @@ -10,6 +10,86 @@ 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); + } + }); + + } + + 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. @@ -24,30 +104,16 @@ function SqlEngine(options) { 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; - default: - throw new Error("Unknown database engine " + this.engine); + // 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} */ @@ -55,6 +121,8 @@ function SqlEngine(options) { $tw.utils.warning(_syncError); }); + this.transactionQueue = []; + } SqlEngine.prototype.init = async function() { @@ -63,7 +131,7 @@ SqlEngine.prototype.init = async function() { // Turn on WAL mode for better-sqlite3 if(this.engine === "better") { // See https://github.com/WiseLibs/better-sqlite3/blob/master/docs/performance.md - await this.db.pragma("journal_mode = WAL"); + await this.db.pragma("journal_mode = WAL"); } } @@ -124,7 +192,7 @@ SqlEngine.prototype.runStatementGetAll = async function(sql,params) { SqlEngine.prototype.runStatements = async function(sqlArray) { /** @type {Awaited>[]} */ const results = new Array(sqlArray.length); - for(let t=0; t 0; this.transactionDepth++; - try { + try { if(alreadyInTransaction) { return await fn(); } else { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js index 5c3325a7785..88693329db7 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js @@ -911,7 +911,7 @@ SqlTiddlerDatabase.prototype.updateUser = async function (userId, username, emai $userId: userId }); - if (existingUser.length > 0) { + if (existingUser) { return { success: false, message: "Email address already in use by another user." 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 1eec51bbc8d..f18f13e3655 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/tests-sql-tiddler-database.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/tests-sql-tiddler-database.js @@ -15,24 +15,30 @@ if($tw.node) { "use strict"; describe("SQL tiddler database with node built-in sqlite", function () { - void runSqlDatabaseTests("node").catch(console.error); + // try {require("node:sqlite");} catch(e) {return;} + void runSqlDatabaseTests("node") }); describe("SQL tiddler database with node-sqlite3-wasm", function () { - void runSqlDatabaseTests("wasm").catch(console.error); + void runSqlDatabaseTests("wasm") }); describe("SQL tiddler database with better-sqlite3", function () { - void runSqlDatabaseTests("better").catch(console.error); + void runSqlDatabaseTests("better") }); -async function runSqlDatabaseTests(engine) { +function runSqlDatabaseTests(engine) { // Create and initialise the tiddler store var SqlTiddlerDatabase = require("$:/plugins/tiddlywiki/multiwikiserver/store/sql-tiddler-database.js").SqlTiddlerDatabase; const sqlTiddlerDatabase = new SqlTiddlerDatabase({ engine: engine }); - await sqlTiddlerDatabase.createTables(); + // eslint-disable-next-line custom-rules/always-await + const beforeStart = sqlTiddlerDatabase.init(); + beforeAll(async () => { + await beforeStart; + await sqlTiddlerDatabase.createTables(); + }); // Tear down afterAll(async function() { // Close the database