diff --git a/.env.example b/.env.example index 32b3cdc9a..4e401ec8c 100644 --- a/.env.example +++ b/.env.example @@ -64,7 +64,6 @@ OIDC_USERQUERY_OPERATOR=<"or"|"and"> OIDC_USERQUERY_FILTER="username:username, email:email" ELASTICSEARCH_ENABLED=<"yes"|"no"> -APP_PORT=3003 STACK_VERSION="8.8.2" CLUSTER_NAME="es-cluster" MEM_LIMIT="4G" @@ -79,3 +78,4 @@ ES_PASSWORD="duo-password" ES_REFRESH=<"wait_for"|"false"> LOGGERS_CONFIG_FILE="loggers.json" +PROPOSAL_TYPES_FILE="proposalTypes.json" diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index f9e04bb63..000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": "tsconfig.json", - "sourceType": "module" - }, - "plugins": ["@typescript-eslint/eslint-plugin"], - "extends": [ - "plugin:@typescript-eslint/recommended", - "plugin:prettier/recommended", - "prettier" - ], - "root": true, - "env": { - "node": true, - "jest": true - }, - "rules": { - "@typescript-eslint/interface-name-prefix": "off", - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/naming-convention": [ - "error", - { - "format": ["camelCase", "PascalCase", "snake_case", "UPPER_CASE"], - "selector": ["variable", "function"] - } - ], - "@typescript-eslint/no-explicit-any": "error", - "@typescript-eslint/no-inferrable-types": "error", - "@typescript-eslint/quotes": [ - "error", - "double", - { - "allowTemplateLiterals": true, - "avoidEscape": true - } - ], - "no-constant-condition": ["error", { "checkLoops": false }], - "@typescript-eslint/no-unused-vars": "warn" - } -} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a757094f9..e564bcaf5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -173,5 +173,6 @@ jobs: run: | cp CI/ESS/docker-compose.api.yaml docker-compose.yaml cp functionalAccounts.json.test functionalAccounts.json + cp proposalTypes.example.json proposalTypes.json docker compose up --build -d npm run test:api diff --git a/.gitignore b/.gitignore index a29dbc032..924c630ca 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /dist /node_modules functionalAccounts.json +proposalTypes.json loggers.json # Configs diff --git a/README.md b/README.md index 1f4c418bb..dd85794f1 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,10 @@ Thank you for your interest in contributing to our project! 3. Add _.env_ file to project root folder. See [Environment variables](#environment-variables). 4. _Optional_ Add [functionalAccounts.json](#local-user-accounts) file to project root folder to create local users. 5. _Optional_ Add [loggers.json](#loggers-configuration) file to the root folder and configure multiple loggers. -6. `npm run start:dev` -7. Go to http://localhost:3000/explorer to get an overview of available endpoints and database schemas. -8. To be able to run the e2e tests with the same setup as in the Github actions you will need to run `npm run prepare:local` and after that run `npm run start:dev`. This will start all needed containers and copy some configuration to the right place. +6. _Optional_ Add [proposalTypes.json](#prpopsal-types-configuration) file to the root folder and configure the proposal types. +7. `npm run start:dev` +8. Go to http://localhost:3000/explorer to get an overview of available endpoints and database schemas. +9. To be able to run the e2e tests with the same setup as in the Github actions you will need to run `npm run prepare:local` and after that run `npm run start:dev`. This will start all needed containers and copy some configuration to the right place. ## Develop in a container using the docker-compose.dev file @@ -50,10 +51,11 @@ Thank you for your interest in contributing to our project! 2. docker-compose -f docker-compose.dev.yaml up -d 3. _Optional_ Mount [functionalAccounts.json](#local-user-accounts) file to a volume in the container to create local users. 4. _Optional_ Mount [loggers.json](#loggers-configuration) file to a volume in the container to configure multiple loggers. -5. _Optional_ change the container env variables -6. Attach to the container -7. `npm run start:dev` -8. Go to http://localhost:3000/explorer to get an overview of available endpoints and database schemas. +5. _Optional_ Mount [proposalTypes.json](#prpopsal-types-configuration) file to a volume in the container to configure the proposal types. +6. _Optional_ change the container env variables +7. Attach to the container +8. `npm run start:dev` +9. Go to http://localhost:3000/explorer to get an overview of available endpoints and database schemas. ## Test the app @@ -96,6 +98,12 @@ Providing a file called _loggers.json_ at the root of the project, locally or in The `loggers.json.example` file in the root directory showcases the example of configuration structure for the one or multiple loggers. `logger.service.ts` file contains the configuration handling process logic, and `src/loggers/loggingProviders/grayLogger.ts` includes actual usecase of grayLogger. +### Prpopsal types configuration + +Providing a file called _proposalTypes.json_ at the root of the project, locally or in the container, will be automatically loaded into the application configuration service under property called `proposalTypes` and used for validation against proposal creation and update. + +The `proposalTypes.json.example` file in the root directory showcases the example of configuration structure for proposal types. + ## Environment variables Valid environment variables for the .env file. See [.env.example](/.env.example) for examples value formats. @@ -176,6 +184,7 @@ Valid environment variables for the .env file. See [.env.example](/.env.example) | `ES_FIELDS_LIMIT` | number | | The total number of fields in an index. | 1000 | | `ES_REFRESH` | string | | If set to `wait_for`, Elasticsearch will wait till data is inserted into the specified index before returning a response. | false | | `LOGGERS_CONFIG_FILE` | string | | The file name for loggers configuration, located in the project root directory. | "loggers.json" | +| `PROPOSAL_TYPES_FILE` | string | | The file name for proposal types configuration, located in the project root directory. | "proposalTypes.json" | | `SWAGGER_PATH` | string | Yes | swaggerPath is the path where the swagger UI will be available| "explorer"| | `MAX_FILE_UPLOAD_SIZE` | string | Yes | Maximum allowed file upload size | "16mb"| diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 000000000..7cf8579ec --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,66 @@ +import typescriptEslintEslintPlugin from "@typescript-eslint/eslint-plugin"; +import stylistic from "@stylistic/eslint-plugin"; +import globals from "globals"; +import tsParser from "@typescript-eslint/parser"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import js from "@eslint/js"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}); + +export default [...compat.extends( + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended", + "prettier", +), { + plugins: { + "@typescript-eslint": typescriptEslintEslintPlugin, + }, + + languageOptions: { + globals: { + ...globals.node, + ...globals.jest, + }, + + parser: tsParser, + ecmaVersion: 5, + sourceType: "module", + + parserOptions: { + project: "tsconfig.json", + }, + }, + + rules: { + "@typescript-eslint/interface-name-prefix": "off", + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + + "@typescript-eslint/naming-convention": ["error", { + format: ["camelCase", "PascalCase", "snake_case", "UPPER_CASE"], + selector: ["variable", "function"], + }], + + "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/no-inferrable-types": "error", + + "quotes": ["error", "double", { + allowTemplateLiterals: true, + avoidEscape: true, + }], + + "no-constant-condition": ["error", { + checkLoops: false, + }], + + "@typescript-eslint/no-unused-vars": "warn", + }, +}]; diff --git a/migrations/20230125143522-replace-object-ids.js b/migrations/20230125143522-replace-object-ids.js index 6763408e5..0cbb21fd3 100644 --- a/migrations/20230125143522-replace-object-ids.js +++ b/migrations/20230125143522-replace-object-ids.js @@ -30,7 +30,7 @@ module.exports = { }) .forEach(function (x) { var oldId = x._id; - // eslint-disable-next-line @typescript-eslint/quotes + // eslint-disable-next-line @/quotes x._id = UUID().toString().split('"')[1]; console.info(" Update id " + oldId + " to id " + x._id); if (names[i] == "Sample") { diff --git a/migrations/20241113130700-proposal-type.js b/migrations/20241113130700-proposal-type.js new file mode 100644 index 000000000..8cc924519 --- /dev/null +++ b/migrations/20241113130700-proposal-type.js @@ -0,0 +1,12 @@ +module.exports = { + async up(db, client) { + db.collection("Proposal").updateMany( + { type: { $exists: false } }, + { $set: { type: "Default Proposal" } }, + ); + }, + + async down(db, client) { + db.collection("Proposal").updateMany({}, { $unset: { type: 1 } }); + }, +}; diff --git a/package-lock.json b/package-lock.json index bdc009447..4bb27f882 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,7 @@ "handlebars": "^4.7.7", "lodash": "^4.17.21", "luxon": "^3.2.1", - "mathjs": "^13.0.0", + "mathjs": "^14.0.0", "migrate-mongo": "^11.0.0", "mongoose": "^8.4.0", "node-fetch": "^3.3.0", @@ -48,16 +48,19 @@ "rimraf": "^6.0.1", "rxjs": "^7.5.7", "swagger-ui-express": "^5.0.0", - "uuid": "^10.0.0" + "uuid": "^11.0.3" }, "devDependencies": { + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.14.0", "@faker-js/faker": "^9.0.0", "@nestjs/cli": "^10.0.5", "@nestjs/schematics": "^10.0.1", "@nestjs/testing": "^10.3.8", + "@stylistic/eslint-plugin": "^2.10.1", "@types/bcrypt": "^5.0.0", "@types/chai": "^5.0.0", - "@types/express": "^4.17.13", + "@types/express": "^5.0.0", "@types/express-session": "^1.17.4", "@types/jest": "^27.0.2", "@types/lodash": "^4.14.180", @@ -70,15 +73,16 @@ "@types/passport-local": "^1.0.34", "@types/supertest": "^6.0.1", "@types/uuid": "^10.0.0", - "@typescript-eslint/eslint-plugin": "^7.3.0", - "@typescript-eslint/parser": "^7.3.0", + "@typescript-eslint/eslint-plugin": "^8.1.0", + "@typescript-eslint/parser": "^8.1.0", "chai": "^5.0.0", - "chai-http": "^4.3.6", + "chai-http": "^5.1.1", "concurrently": "^9.0.0", - "eslint": "^8.46.0", + "eslint": "^9.0.0", "eslint-config-loopback": "^13.1.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", + "globals": "^15.12.0", "jest": "27.0.6", "mocha": "^10.0.0", "prettier": "^3.0.3", @@ -1035,111 +1039,6 @@ "@css-inline/css-inline-win32-x64-msvc": "0.14.1" } }, - "node_modules/@css-inline/css-inline-android-arm-eabi": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline-android-arm-eabi/-/css-inline-android-arm-eabi-0.14.1.tgz", - "integrity": "sha512-LNUR8TY4ldfYi0mi/d4UNuHJ+3o8yLQH9r2Nt6i4qeg1i7xswfL3n/LDLRXvGjBYqeEYNlhlBQzbPwMX1qrU6A==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@css-inline/css-inline-android-arm64": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline-android-arm64/-/css-inline-android-arm64-0.14.1.tgz", - "integrity": "sha512-tH5us0NYGoTNBHOUHVV7j9KfJ4DtFOeTLA3cM0XNoMtArNu2pmaaBMFJPqECzavfXkLc7x5Z22UPZYjoyHfvCA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@css-inline/css-inline-darwin-arm64": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline-darwin-arm64/-/css-inline-darwin-arm64-0.14.1.tgz", - "integrity": "sha512-QE5W1YRIfRayFrtrcK/wqEaxNaqLULPI0gZB4ArbFRd3d56IycvgBasDTHPre5qL2cXCO3VyPx+80XyHOaVkag==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@css-inline/css-inline-darwin-x64": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline-darwin-x64/-/css-inline-darwin-x64-0.14.1.tgz", - "integrity": "sha512-mAvv2sN8awNFsbvBzlFkZPbCNZ6GCWY5/YcIz7V5dPYw+bHHRbjnlkNTEZq5BsDxErVrMIGvz05PGgzuNvZvdQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@css-inline/css-inline-linux-arm-gnueabihf": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline-linux-arm-gnueabihf/-/css-inline-linux-arm-gnueabihf-0.14.1.tgz", - "integrity": "sha512-AWC44xL0X7BgKvrWEqfSqkT2tJA5kwSGrAGT+m0gt11wnTYySvQ6YpX0fTY9i3ppYGu4bEdXFjyK2uY1DTQMHA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@css-inline/css-inline-linux-arm64-gnu": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline-linux-arm64-gnu/-/css-inline-linux-arm64-gnu-0.14.1.tgz", - "integrity": "sha512-drj0ciiJgdP3xKXvNAt4W+FH4KKMs8vB5iKLJ3HcH07sNZj58Sx++2GxFRS1el3p+GFp9OoYA6dgouJsGEqt0Q==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@css-inline/css-inline-linux-arm64-musl": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline-linux-arm64-musl/-/css-inline-linux-arm64-musl-0.14.1.tgz", - "integrity": "sha512-FzknI+st8eA8YQSdEJU9ykcM0LZjjigBuynVF5/p7hiMm9OMP8aNhWbhZ8LKJpKbZrQsxSGS4g9Vnr6n6FiSdQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@css-inline/css-inline-linux-x64-gnu": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/@css-inline/css-inline-linux-x64-gnu/-/css-inline-linux-x64-gnu-0.14.1.tgz", @@ -1170,21 +1069,6 @@ "node": ">= 10" } }, - "node_modules/@css-inline/css-inline-win32-x64-msvc": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline-win32-x64-msvc/-/css-inline-win32-x64-msvc-0.14.1.tgz", - "integrity": "sha512-nzotGiaiuiQW78EzsiwsHZXbxEt6DiMUFcDJ6dhiliomXxnlaPyBfZb6/FMBgRJOf6sknDt/5695OttNmbMYzg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@elastic/elasticsearch": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-8.15.0.tgz", @@ -1235,24 +1119,47 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/config-array": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.0.tgz", + "integrity": "sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.0.tgz", + "integrity": "sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "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", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -1260,7 +1167,7 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -1282,6 +1189,18 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -1289,12 +1208,33 @@ "dev": true }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.15.0.tgz", + "integrity": "sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==", "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "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==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz", + "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==", + "dev": true, + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@faker-js/faker": { @@ -1328,18 +1268,39 @@ "@hapi/hoek": "^9.0.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" }, "engines": { - "node": ">=10.10.0" + "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": { @@ -1355,11 +1316,18 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", - "dev": true + "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==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -2845,6 +2813,49 @@ "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", "dev": true }, + "node_modules/@stylistic/eslint-plugin": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.11.0.tgz", + "integrity": "sha512-PNRHbydNG5EH8NK4c+izdJlxajIR6GxcUhzsYNRsn6Myep4dsZt0qFCz3rCPnkvgO5FYibDcMqgNHUT+zvjYZw==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "^8.13.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/@stylistic/eslint-plugin/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@stylistic/eslint-plugin/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -3009,21 +3020,21 @@ "dev": true }, "node_modules/@types/express": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", - "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", "dev": true, "dependencies": { "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", + "@types/express-serve-static-core": "^5.0.0", "@types/qs": "*", "@types/serve-static": "*" } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.36", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.36.tgz", - "integrity": "sha512-zbivROJ0ZqLAtMzgzIUC4oNqDG9iF0lSsAqpOD9kbs5xcIM3dTiyuHvBc7R8MtWBp3AAWGaovJa+wzWPjLYW7Q==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.1.tgz", + "integrity": "sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==", "dev": true, "dependencies": { "@types/node": "*", @@ -3251,21 +3262,21 @@ "optional": true }, "node_modules/@types/qs": { - "version": "6.9.8", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.8.tgz", - "integrity": "sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==", + "version": "6.9.17", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", + "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==", "dev": true }, "node_modules/@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "dev": true }, "node_modules/@types/send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.1.tgz", - "integrity": "sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==", + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", "dev": true, "dependencies": { "@types/mime": "^1", @@ -3350,31 +3361,31 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", - "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.16.0.tgz", + "integrity": "sha512-5YTHKV8MYlyMI6BaEG7crQ9BhSc8RxzshOReKwZwRWN0+XvvTOm+L/UYLCYxFpfwYuAAqhxiq4yae0CMFwbL7Q==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/type-utils": "7.18.0", - "@typescript-eslint/utils": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/type-utils": "8.16.0", + "@typescript-eslint/utils": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -3383,26 +3394,26 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", - "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.16.0.tgz", + "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/typescript-estree": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", "debug": "^4.3.4" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -3411,16 +3422,16 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", - "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.16.0.tgz", + "integrity": "sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0" + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -3428,25 +3439,25 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", - "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.16.0.tgz", + "integrity": "sha512-IqZHGG+g1XCWX9NyqnI/0CX5LL8/18awQqmkZSl2ynn8F76j579dByc0jhfVSnSnhf7zv76mKBQv9HQFKvDCgg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/typescript-estree": "8.16.0", + "@typescript-eslint/utils": "8.16.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -3455,12 +3466,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", - "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.16.0.tgz", + "integrity": "sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ==", "dev": true, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -3468,22 +3479,22 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", - "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.16.0.tgz", + "integrity": "sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -3520,44 +3531,61 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", - "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.16.0.tgz", + "integrity": "sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0" + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/typescript-estree": "8.16.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", - "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.16.0.tgz", + "integrity": "sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.18.0", - "eslint-visitor-keys": "^3.4.3" + "@typescript-eslint/types": "8.16.0", + "eslint-visitor-keys": "^4.2.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@ucast/core": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.2.tgz", @@ -3589,12 +3617,6 @@ "@ucast/mongo": "^2.4.0" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, "node_modules/@user-office-software/duo-logger": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@user-office-software/duo-logger/-/duo-logger-2.2.1.tgz", @@ -4116,15 +4138,6 @@ "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", "dev": true }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -4742,38 +4755,20 @@ } }, "node_modules/chai-http": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/chai-http/-/chai-http-4.4.0.tgz", - "integrity": "sha512-uswN3rZpawlRaa5NiDUHcDZ3v2dw5QgLyAwnQ2tnVNuP7CwIsOFuYJ0xR1WiR7ymD4roBnJIzOUep7w9jQMFJA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai-http/-/chai-http-5.1.1.tgz", + "integrity": "sha512-h+QZNfYdlcoyIyOb26N71S7700CP4EY+CQ1X15AsX1RD2xMLAWbMniS7yUTOEC6DzC5yydGV37wu81AGNm8esA==", "dev": true, "dependencies": { - "@types/chai": "4", - "@types/superagent": "4.1.13", "charset": "^1.0.1", "cookiejar": "^2.1.4", - "is-ip": "^2.0.0", + "is-ip": "^5.0.1", "methods": "^1.1.2", - "qs": "^6.11.2", - "superagent": "^8.0.9" + "qs": "^6.12.1", + "superagent": "^9" }, "engines": { - "node": ">=10" - } - }, - "node_modules/chai-http/node_modules/@types/chai": { - "version": "4.3.20", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", - "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", - "dev": true - }, - "node_modules/chai-http/node_modules/@types/superagent": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.13.tgz", - "integrity": "sha512-YIGelp3ZyMiH0/A09PMAORO0EBGlF5xIKfDpK74wdYvWUs2o96b5CItJcWPdH409b7SAXIIG6p8NdU/4U2Maww==", - "dev": true, - "dependencies": { - "@types/cookiejar": "*", - "@types/node": "*" + "node": ">=18.20.0" } }, "node_modules/chalk": { @@ -5078,6 +5073,21 @@ "node": ">=0.8" } }, + "node_modules/clone-regexp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-3.0.0.tgz", + "integrity": "sha512-ujdnoq2Kxb8s3ItNBtnYeXdm07FcU0u8ARAT1lQ2YdMwQC+cdiXX8KoqMVuglztILivceTtp4ivqGSmEmhBUJw==", + "dev": true, + "dependencies": { + "is-regexp": "^3.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -5155,15 +5165,15 @@ } }, "node_modules/complex.js": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.1.1.tgz", - "integrity": "sha512-8njCHOTtFFLtegk6zQo0kkVX1rngygb/KQI6z1qZxlFI3scluC+LVTCFbrkWjBv4vvLlbQ9t88IPMC6k95VTTg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.4.2.tgz", + "integrity": "sha512-qtx7HRhPGSCBtGiST4/WGHuW+zeaND/6Ld+db6PbrulIB1i2Ev/2UPiqcmpQNPSyfBKraC0EOvOKCB5dGZKt3g==", "engines": { "node": "*" }, "funding": { - "type": "patreon", - "url": "https://www.patreon.com/infusion" + "type": "github", + "url": "https://github.com/sponsors/rawify" } }, "node_modules/component-emitter": { @@ -5326,6 +5336,18 @@ "node": ">= 0.6" } }, + "node_modules/convert-hrtime": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-5.0.0.tgz", + "integrity": "sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", @@ -5401,9 +5423,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==", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -5686,18 +5708,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/display-notification": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/display-notification/-/display-notification-2.0.0.tgz", @@ -5711,18 +5721,6 @@ "node": ">=4" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/doctypes": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", @@ -6091,58 +6089,62 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.15.0.tgz", + "integrity": "sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@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.15.0", + "@eslint/plugin-kit": "^0.2.3", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.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.5", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-config-loopback": { @@ -6212,16 +6214,16 @@ } }, "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -6255,6 +6257,18 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/eslint/node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -6274,17 +6288,29 @@ "dev": true }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "dev": true, "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -6602,9 +6628,9 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -6635,9 +6661,9 @@ "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" }, "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, "dependencies": { "reusify": "^1.0.4" @@ -6699,15 +6725,15 @@ } }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/filelist": { @@ -6838,58 +6864,22 @@ } }, "node_modules/flat-cache": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", - "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", - "dev": true, - "dependencies": { - "flatted": "^3.2.7", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/flat-cache/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "flatted": "^3.2.9", + "keyv": "^4.5.4" }, "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/flat-cache/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=16" } }, "node_modules/flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", "dev": true }, "node_modules/fn-args": { @@ -6988,20 +6978,28 @@ } }, "node_modules/formidable": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", - "integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.2.tgz", + "integrity": "sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==", "dev": true, "dependencies": { "dezalgo": "^1.0.4", - "hexoid": "^1.0.0", - "once": "^1.4.0", - "qs": "^6.11.0" + "hexoid": "^2.0.0", + "once": "^1.4.0" }, "funding": { "url": "https://ko-fi.com/tunnckoCore/commissions" } }, + "node_modules/formidable/node_modules/hexoid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz", + "integrity": "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -7011,14 +7009,14 @@ } }, "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.2.1.tgz", + "integrity": "sha512-Ah6t/7YCYjrPUFUFsOsRLMXAdnYM+aQwmojD2Ayb/Ezr82SwES0vuyQ8qZ3QO8n9j7W14VJuVZZet8U3bhSdQQ==", "engines": { - "node": "*" + "node": ">= 12" }, "funding": { - "type": "patreon", + "type": "github", "url": "https://github.com/sponsors/rawify" } }, @@ -7081,20 +7079,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -7103,6 +7087,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/function-timeout": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/function-timeout/-/function-timeout-0.1.1.tgz", + "integrity": "sha512-0NVVC0TaP7dSTvn1yMiy6d6Q8gifzbvQafO46RtLG/kHJUBNd+pVRGOBoK44wNBvtSPUJRfdVvkFdD3p0xvyZg==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gauge": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", @@ -7297,35 +7293,12 @@ } }, "node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "version": "15.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.12.0.tgz", + "integrity": "sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==", "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -7483,15 +7456,6 @@ "he": "bin/he" } }, - "node_modules/hexoid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", - "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/hpagent": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz", @@ -7760,20 +7724,37 @@ "node": ">=12.0.0" } }, - "node_modules/ip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", - "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "optional": true, + "peer": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "optional": true, "peer": true }, "node_modules/ip-regex": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", - "integrity": "sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz", + "integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==", "dev": true, "engines": { - "node": ">=4" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/ipaddr.js": { @@ -7899,15 +7880,19 @@ } }, "node_modules/is-ip": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-2.0.0.tgz", - "integrity": "sha512-9MTn0dteHETtyUx8pxqMwg5hMBi3pvlyglJ+b79KOCca0po23337LbVV2Hl4xmMvfw++ljnO0/+5G6G+0Szh6g==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-5.0.1.tgz", + "integrity": "sha512-FCsGHdlrOnZQcp0+XT5a+pYowf33itBalCl+7ovNXC/7o5BhIpG14M3OrpPPdBSIQJCm+0M5+9mO7S9VVTTCFw==", "dev": true, "dependencies": { - "ip-regex": "^2.0.0" + "ip-regex": "^5.0.0", + "super-regex": "^0.2.0" }, "engines": { - "node": ">=4" + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-number": { @@ -7919,15 +7904,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -7965,6 +7941,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-regexp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz", + "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -8970,6 +8958,13 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "optional": true, + "peer": true + }, "node_modules/jsdom": { "version": "16.7.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", @@ -9186,9 +9181,9 @@ } }, "node_modules/keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "dependencies": { "json-buffer": "3.0.1" @@ -9292,15 +9287,24 @@ "optional": true }, "node_modules/libmime": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.2.1.tgz", - "integrity": "sha512-A0z9O4+5q+ZTj7QwNe/Juy1KARNb4WaviO4mYeFC4b8dBT2EEqK2pkM+GC8MVnkOjqhl5nYQxRgnPYRRTNmuSQ==", + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.5.tgz", + "integrity": "sha512-nSlR1yRZ43L3cZCiWEw7ali3jY29Hz9CQQ96Oy+sSspYnIP5N54ucOPHqooBsXzwrX1pwn13VUE05q4WmzfaLg==", "optional": true, "dependencies": { - "encoding-japanese": "2.0.0", + "encoding-japanese": "2.1.0", "iconv-lite": "0.6.3", - "libbase64": "1.2.1", - "libqp": "2.0.1" + "libbase64": "1.3.0", + "libqp": "2.1.0" + } + }, + "node_modules/libmime/node_modules/encoding-japanese": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.1.0.tgz", + "integrity": "sha512-58XySVxUgVlBikBTbQ8WdDxBDHIdXucB16LO5PBHR8t75D54wQrNo4cg+58+R1CtJfKnsVsvt9XlteRaR8xw1w==", + "optional": true, + "engines": { + "node": ">=8.10.0" } }, "node_modules/libmime/node_modules/iconv-lite": { @@ -9315,6 +9319,18 @@ "node": ">=0.10.0" } }, + "node_modules/libmime/node_modules/libbase64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-1.3.0.tgz", + "integrity": "sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==", + "optional": true + }, + "node_modules/libmime/node_modules/libqp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/libqp/-/libqp-2.1.0.tgz", + "integrity": "sha512-O6O6/fsG5jiUVbvdgT7YX3xY3uIadR6wEZ7+vy9u7PKHAlSEB6blvC1o5pHBjgsi95Uo0aiBBdkyFecj6jtb7A==", + "optional": true + }, "node_modules/libphonenumber-js": { "version": "1.10.53", "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.53.tgz", @@ -9333,12 +9349,12 @@ "dev": true }, "node_modules/linkify-it": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", - "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", "optional": true, "dependencies": { - "uc.micro": "^1.0.1" + "uc.micro": "^2.0.0" } }, "node_modules/liquidjs": { @@ -9513,20 +9529,30 @@ } }, "node_modules/mailparser": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.6.5.tgz", - "integrity": "sha512-nteTpF0Khm5JLOnt4sigmzNdUH/6mO7PZ4KEnvxf4mckyXYFFhrtAWZzbq/V5aQMH+049gA7ZjfLdh+QiX2Uqg==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.7.1.tgz", + "integrity": "sha512-RCnBhy5q8XtB3mXzxcAfT1huNqN93HTYYyL6XawlIKycfxM/rXPg9tXoZ7D46+SgCS1zxKzw+BayDQSvncSTTw==", "optional": true, "dependencies": { - "encoding-japanese": "2.0.0", + "encoding-japanese": "2.1.0", "he": "1.2.0", "html-to-text": "9.0.5", "iconv-lite": "0.6.3", - "libmime": "5.2.1", - "linkify-it": "4.0.1", + "libmime": "5.3.5", + "linkify-it": "5.0.0", "mailsplit": "5.4.0", - "nodemailer": "6.9.3", - "tlds": "1.240.0" + "nodemailer": "6.9.13", + "punycode.js": "2.3.1", + "tlds": "1.252.0" + } + }, + "node_modules/mailparser/node_modules/encoding-japanese": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.1.0.tgz", + "integrity": "sha512-58XySVxUgVlBikBTbQ8WdDxBDHIdXucB16LO5PBHR8t75D54wQrNo4cg+58+R1CtJfKnsVsvt9XlteRaR8xw1w==", + "optional": true, + "engines": { + "node": ">=8.10.0" } }, "node_modules/mailparser/node_modules/iconv-lite": { @@ -9542,9 +9568,9 @@ } }, "node_modules/mailparser/node_modules/nodemailer": { - "version": "6.9.3", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.3.tgz", - "integrity": "sha512-fy9v3NgTzBngrMFkDsKEj0r02U7jm6XfC3b52eoNV+GCrGj+s8pt5OqhiJdWKuw51zCTdiNR/IUD1z33LIIGpg==", + "version": "6.9.13", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.13.tgz", + "integrity": "sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==", "optional": true, "engines": { "node": ">=6.0.0" @@ -9623,15 +9649,15 @@ } }, "node_modules/mathjs": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-13.2.0.tgz", - "integrity": "sha512-P5PZoiUX2Tkghkv3tsSqlK0B9My/ErKapv1j6wdxd0MOrYQ30cnGE4LH/kzYB2gA5rN46Njqc4cFgJjaxgijoQ==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-14.0.0.tgz", + "integrity": "sha512-MR3me92c6pKBqzUXosqL5KMIZDrb1x0MGOy+Ss6fQllD1zhAFloG6DJnG6X5b0VYAMA9sgGfAR2tYi5HPNNQBQ==", "dependencies": { - "@babel/runtime": "^7.25.6", - "complex.js": "^2.1.1", + "@babel/runtime": "^7.25.7", + "complex.js": "^2.2.5", "decimal.js": "^10.4.3", "escape-latex": "^1.2.0", - "fraction.js": "^4.3.7", + "fraction.js": "^5.2.1", "javascript-natural-sort": "^0.7.1", "seedrandom": "^3.0.5", "tiny-emitter": "^2.1.0", @@ -9707,12 +9733,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "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.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -10456,9 +10482,10 @@ } }, "node_modules/mongodb": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.8.0.tgz", - "integrity": "sha512-HGQ9NWDle5WvwMnrvUxsFYPd3JEbqD3RgABHBQRuoCEND0qzhsd0iH5ypHsf1eJ+sXmvmyKpP+FLOKY8Il7jMw==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.10.0.tgz", + "integrity": "sha512-gP9vduuYWb9ZkDM546M+MP2qKVk5ZG2wPF63OvSRuUbqCR+11ZCAE1mOfllhlAG0wcoJY5yDL/rV3OmYEwXIzg==", + "license": "Apache-2.0", "dependencies": { "@mongodb-js/saslprep": "^1.1.5", "bson": "^6.7.0", @@ -10541,13 +10568,14 @@ } }, "node_modules/mongoose": { - "version": "8.6.2", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.6.2.tgz", - "integrity": "sha512-ErbDVvuUzUfyQpXvJ6sXznmZDICD8r6wIsa0VKjJtB6/LZncqwUn5Um040G1BaNo6L3Jz+xItLSwT0wZmSmUaQ==", + "version": "8.8.3", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.8.3.tgz", + "integrity": "sha512-/I4n/DcXqXyIiLRfAmUIiTjj3vXfeISke8dt4U4Y8Wfm074Wa6sXnQrXN49NFOFf2mM1kUdOXryoBvkuCnr+Qw==", + "license": "MIT", "dependencies": { "bson": "^6.7.0", "kareem": "2.6.3", - "mongodb": "6.8.0", + "mongodb": "~6.10.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", @@ -11730,6 +11758,15 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -12118,9 +12155,10 @@ } }, "node_modules/run-applescript/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "license": "MIT", "optional": true, "dependencies": { "nice-try": "^1.0.4", @@ -12641,17 +12679,17 @@ } }, "node_modules/socks": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", - "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", "optional": true, "peer": true, "dependencies": { - "ip": "^2.0.0", + "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" }, "engines": { - "node": ">= 10.13.0", + "node": ">= 10.0.0", "npm": ">= 3.0.0" } }, @@ -12841,10 +12879,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/super-regex": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/super-regex/-/super-regex-0.2.0.tgz", + "integrity": "sha512-WZzIx3rC1CvbMDloLsVw0lkZVKJWbrkJ0k1ghKFmcnPrW1+jWbgTkTEWVtD9lMdmI4jZEz40+naBxl1dCUhXXw==", + "dev": true, + "dependencies": { + "clone-regexp": "^3.0.0", + "function-timeout": "^0.1.0", + "time-span": "^5.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/superagent": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", - "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", + "integrity": "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==", "dev": true, "dependencies": { "component-emitter": "^1.3.0", @@ -12852,20 +12907,19 @@ "debug": "^4.3.4", "fast-safe-stringify": "^2.1.1", "form-data": "^4.0.0", - "formidable": "^2.1.2", + "formidable": "^3.5.1", "methods": "^1.1.2", "mime": "2.6.0", - "qs": "^6.11.0", - "semver": "^7.3.8" + "qs": "^6.11.0" }, "engines": { - "node": ">=6.4.0 <13 || >=14" + "node": ">=14.18.0" } }, "node_modules/superagent/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", "dev": true, "dependencies": { "asynckit": "^0.4.0", @@ -12901,67 +12955,6 @@ "node": ">=14.18.0" } }, - "node_modules/supertest/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/supertest/node_modules/formidable": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.1.tgz", - "integrity": "sha512-WJWKelbRHN41m5dumb0/k8TeAx7Id/y3a+Z7QfhxP/htI9Js5zYaEDtG8uMgG0vM0lOlqnmjE99/kfpOYi/0Og==", - "dev": true, - "dependencies": { - "dezalgo": "^1.0.4", - "hexoid": "^1.0.0", - "once": "^1.4.0" - }, - "funding": { - "url": "https://ko-fi.com/tunnckoCore/commissions" - } - }, - "node_modules/supertest/node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/supertest/node_modules/superagent": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.1.tgz", - "integrity": "sha512-CcRSdb/P2oUVaEpQ87w9Obsl+E9FruRd6b2b7LdiBtJoyMr2DQt7a89anAfiX/EL59j9b2CbRFvf2S91DhuCww==", - "dev": true, - "dependencies": { - "component-emitter": "^1.3.0", - "cookiejar": "^2.1.4", - "debug": "^4.3.4", - "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.0", - "formidable": "^3.5.1", - "methods": "^1.1.2", - "mime": "2.6.0", - "qs": "^6.11.0", - "semver": "^7.3.8" - }, - "engines": { - "node": ">=14.18.0" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -13205,12 +13198,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "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/throat": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.2.tgz", @@ -13223,15 +13210,30 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, + "node_modules/time-span": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz", + "integrity": "sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==", + "dev": true, + "dependencies": { + "convert-hrtime": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tiny-emitter": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" }, "node_modules/tlds": { - "version": "1.240.0", - "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.240.0.tgz", - "integrity": "sha512-1OYJQenswGZSOdRw7Bql5Qu7uf75b+F3HFBXbqnG/ifHa0fev1XcG+3pJf3pA/KC6RtHQzfKgIf1vkMlMG7mtQ==", + "version": "1.252.0", + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.252.0.tgz", + "integrity": "sha512-GA16+8HXvqtfEnw/DTcwB0UU354QE1n3+wh08oFjr6Znl7ZLAeUgYzCcK+/CCrOyE0vnHR8/pu3XXG3vDijXpQ==", "optional": true, "bin": { "tlds": "bin.js" @@ -13336,9 +13338,9 @@ } }, "node_modules/ts-api-utils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", - "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.2.tgz", + "integrity": "sha512-ZF5gQIQa/UmzfvxbHZI3JXN0/Jt+vnAfAviNRAMc491laiK6YCLpCW9ft8oaCRFOTxCZtUTE6XB0ZQAe3olntw==", "dev": true, "engines": { "node": ">=16" @@ -13602,9 +13604,9 @@ } }, "node_modules/uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "optional": true }, "node_modules/uglify-js": { @@ -13738,15 +13740,15 @@ } }, "node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", + "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/v8-compile-cache-lib": { diff --git a/package.json b/package.json index ceaf4cf54..737cf9ac0 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "test:api": "npm run test:api:jest --maxWorkers=50% && concurrently -k -s first \"wait-on http://localhost:3000/explorer/ && npm run test:api:mocha\" \"npm run start\"", "test:api:jest": "jest --config ./test/config/jest-e2e.json --maxWorkers=50%", "test:api:mocha": "mocha --config ./test/config/.mocharc.json -r chai/register-should.js", - "prepare:local": "docker-compose -f CI/E2E/docker-compose-local.yaml --env-file CI/E2E/.env.elastic-search up -d && cp functionalAccounts.json.test functionalAccounts.json" + "prepare:local": "docker-compose -f CI/E2E/docker-compose-local.yaml --env-file CI/E2E/.env.elastic-search up -d && cp functionalAccounts.json.test functionalAccounts.json && cp proposalTypes.example.json proposalTypes.json" }, "dependencies": { "@casl/ability": "^6.3.2", @@ -55,7 +55,7 @@ "handlebars": "^4.7.7", "lodash": "^4.17.21", "luxon": "^3.2.1", - "mathjs": "^13.0.0", + "mathjs": "^14.0.0", "migrate-mongo": "^11.0.0", "mongoose": "^8.4.0", "node-fetch": "^3.3.0", @@ -69,21 +69,27 @@ "rimraf": "^6.0.1", "rxjs": "^7.5.7", "swagger-ui-express": "^5.0.0", - "uuid": "^10.0.0" + "uuid": "^11.0.3" }, "overrides": { "pac-resolver": { "degenerator": "~5" + }, + "socks": { + "ip": "^2.0.2" } }, "devDependencies": { + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.14.0", "@faker-js/faker": "^9.0.0", "@nestjs/cli": "^10.0.5", "@nestjs/schematics": "^10.0.1", "@nestjs/testing": "^10.3.8", + "@stylistic/eslint-plugin": "^2.10.1", "@types/bcrypt": "^5.0.0", "@types/chai": "^5.0.0", - "@types/express": "^4.17.13", + "@types/express": "^5.0.0", "@types/express-session": "^1.17.4", "@types/jest": "^27.0.2", "@types/lodash": "^4.14.180", @@ -96,15 +102,16 @@ "@types/passport-local": "^1.0.34", "@types/supertest": "^6.0.1", "@types/uuid": "^10.0.0", - "@typescript-eslint/eslint-plugin": "^7.3.0", - "@typescript-eslint/parser": "^7.3.0", + "@typescript-eslint/eslint-plugin": "^8.1.0", + "@typescript-eslint/parser": "^8.1.0", "chai": "^5.0.0", - "chai-http": "^4.3.6", + "chai-http": "^5.1.1", "concurrently": "^9.0.0", - "eslint": "^8.46.0", + "eslint": "^9.0.0", "eslint-config-loopback": "^13.1.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", + "globals": "^15.12.0", "jest": "27.0.6", "mocha": "^10.0.0", "prettier": "^3.0.3", diff --git a/proposalTypes.example.json b/proposalTypes.example.json new file mode 100644 index 000000000..912c44040 --- /dev/null +++ b/proposalTypes.example.json @@ -0,0 +1,4 @@ +{ + "DOORProposal": "DOOR Proposal", + "Beamtime": "Beamtime" +} diff --git a/src/common/pipes/filter.pipe.ts b/src/common/pipes/filter.pipe.ts index 55091ffba..75fa38b4f 100644 --- a/src/common/pipes/filter.pipe.ts +++ b/src/common/pipes/filter.pipe.ts @@ -20,16 +20,16 @@ export class FilterPipe let filter = inValue.filter; // subsitute the loopback operators to mongo equivalent // nin => $in - // eslint-disable-next-line @typescript-eslint/quotes + // eslint-disable-next-line @/quotes filter = filter.replace(/{"inq":/g, '{"$in":'); // nin => $nin - // eslint-disable-next-line @typescript-eslint/quotes + // eslint-disable-next-line @/quotes filter = filter.replace(/{"nin":/g, '{"$nin":'); // and => $and - // eslint-disable-next-line @typescript-eslint/quotes + // eslint-disable-next-line @/quotes filter = filter.replace(/{"and":\[/g, '{"$and":['); // and => $or - // eslint-disable-next-line @typescript-eslint/quotes + // eslint-disable-next-line @/quotes filter = filter.replace(/{"or":\[/g, '{"$or":['); outValue.filter = filter; } diff --git a/src/common/utils.ts b/src/common/utils.ts index b771e2420..0dfe1ea52 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/quotes */ +/* eslint-disable @/quotes */ import { Logger } from "@nestjs/common"; import { inspect } from "util"; import { DateTime } from "luxon"; @@ -12,6 +12,8 @@ import { IScientificFilter, } from "./interfaces/common.interface"; import { ScientificRelation } from "./scientific-relation.enum"; +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { IFullFacets } from "src/elastic-search/interfaces/es-common.type"; // add Ã… to mathjs accepted units as equivalent to angstrom const isAlphaOriginal = Unit.isValidAlpha; @@ -1012,3 +1014,41 @@ const replaceLikeOperatorRecursive = ( export const sleep = (ms: number) => { return new Promise((resolve) => setTimeout(resolve, ms)); }; + +export class FullFacetFilters { + @ApiPropertyOptional() + facets?: string; + + @ApiPropertyOptional() + fields?: string; +} + +export class FullQueryFilters { + @ApiPropertyOptional() + limits?: string; + + @ApiPropertyOptional() + fields?: string; +} + +class TotalSets { + @ApiProperty({ type: Number }) + totalSets: number; +} + +export class FullFacetResponse implements IFullFacets { + @ApiProperty({ type: TotalSets, isArray: true }) + all: [TotalSets]; + + [key: string]: object; +} + +export class CountApiResponse { + @ApiProperty({ type: Number }) + count: number; +} + +export class IsValidResponse { + @ApiProperty({ type: Boolean }) + isvalid: boolean; +} diff --git a/src/config/configuration.ts b/src/config/configuration.ts index 1fc02fbdc..971a2205a 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -2,6 +2,7 @@ import * as fs from "fs"; import { merge } from "lodash"; import localconfiguration from "./localconfiguration"; import { boolean } from "mathjs"; +import { DEFAULT_PROPOSAL_TYPE } from "src/proposals/schemas/proposal.schema"; const configuration = () => { const accessGroupsStaticValues = @@ -38,9 +39,12 @@ const configuration = () => { modulePath: "./loggingProviders/defaultLogger", config: {}, }; - const jsonConfigMap: { [key: string]: object[] | boolean } = {}; + const jsonConfigMap: { [key: string]: object | object[] | boolean } = { + proposalTypes: {}, + }; const jsonConfigFileList: { [key: string]: string } = { loggers: process.env.LOGGERS_CONFIG_FILE || "loggers.json", + proposalTypes: process.env.PROPOSAL_TYPES_FILE || "proposalTypes.json", }; Object.keys(jsonConfigFileList).forEach((key) => { const filePath = jsonConfigFileList[key]; @@ -57,6 +61,11 @@ const configuration = () => { } }); + // NOTE: Add the default proposal type here + Object.assign(jsonConfigMap.proposalTypes, { + DefaultProposal: DEFAULT_PROPOSAL_TYPE, + }); + const config = { maxFileUploadSizeInMb: process.env.MAX_FILE_UPLOAD_SIZE || "16mb", // 16MB by default versions: { @@ -203,6 +212,7 @@ const configuration = () => { policyPublicationShiftInYears: process.env.POLICY_PUBLICATION_SHIFT ?? 3, policyRetentionShiftInYears: process.env.POLICY_RETENTION_SHIFT ?? -1, }, + proposalTypes: jsonConfigMap.proposalTypes, }; return merge(config, localconfiguration); }; diff --git a/src/datasets/datasets.controller.ts b/src/datasets/datasets.controller.ts index 3c7123ea0..32eb8d34b 100644 --- a/src/datasets/datasets.controller.ts +++ b/src/datasets/datasets.controller.ts @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/quotes */ +/* eslint-disable @/quotes */ import { Body, Controller, @@ -83,12 +83,13 @@ import { } from "./dto/update-derived-dataset-obsolete.dto"; import { CreateDatasetDatablockDto } from "src/datablocks/dto/create-dataset-datablock"; import { + CountApiResponse, filterDescription, filterExample, - datasetsFullQueryDescriptionFields, - fullQueryDescriptionLimits, - datasetsFullQueryExampleFields, - fullQueryExampleLimits, + FullFacetFilters, + FullFacetResponse, + FullQueryFilters, + IsValidResponse, replaceLikeOperator, } from "src/common/utils"; import { HistoryClass } from "./schemas/history.schema"; @@ -104,6 +105,7 @@ import { PartialUpdateDatasetDto, UpdateDatasetDto, } from "./dto/update-dataset.dto"; +import { Logbook } from "src/logbooks/schemas/logbook.schema"; @ApiBearerAuth() @ApiExtraModels( @@ -178,10 +180,11 @@ export class DatasetsController { DatasetClass, ); + if (!mergedFilters.where) { + mergedFilters.where = {}; + } + if (!canViewAny) { - if (!mergedFilters.where) { - mergedFilters.where = {}; - } if (canViewAccess) { mergedFilters.where["$or"] = [ { ownerGroup: { $in: user.currentGroups } }, @@ -196,6 +199,10 @@ export class DatasetsController { } } + mergedFilters.where = this.convertObsoleteWhereFilterToCurrentSchema( + mergedFilters.where, + ); + return mergedFilters; } @@ -207,82 +214,84 @@ export class DatasetsController { const dataset = await this.datasetsService.findOne({ where: { pid: id } }); const user: JWTUser = request.user as JWTUser; - if (dataset) { - const datasetInstance = - await this.generateDatasetInstanceForPermissions(dataset); - - const ability = this.caslAbilityFactory.datasetInstanceAccess(user); - - let canDoAction = false; - - if (group == Action.DatasetRead) { - canDoAction = - ability.can(Action.DatasetReadAny, DatasetClass) || - ability.can(Action.DatasetReadOneOwner, datasetInstance) || - ability.can(Action.DatasetReadOneAccess, datasetInstance) || - ability.can(Action.DatasetReadOnePublic, datasetInstance); - } else if (group == Action.DatasetAttachmentRead) { - canDoAction = - ability.can(Action.DatasetAttachmentReadAny, DatasetClass) || - ability.can(Action.DatasetAttachmentReadOwner, datasetInstance) || - ability.can(Action.DatasetAttachmentReadAccess, datasetInstance) || - ability.can(Action.DatasetAttachmentReadPublic, datasetInstance); - } else if (group == Action.DatasetAttachmentCreate) { - canDoAction = - ability.can(Action.DatasetAttachmentCreateAny, DatasetClass) || - ability.can(Action.DatasetAttachmentCreateOwner, datasetInstance); - } else if (group == Action.DatasetAttachmentUpdate) { - canDoAction = - ability.can(Action.DatasetAttachmentUpdateAny, DatasetClass) || - ability.can(Action.DatasetAttachmentUpdateOwner, datasetInstance); - } else if (group == Action.DatasetAttachmentDelete) { - canDoAction = - ability.can(Action.DatasetAttachmentDeleteAny, DatasetClass) || - ability.can(Action.DatasetAttachmentDeleteOwner, datasetInstance); - } else if (group == Action.DatasetOrigdatablockRead) { - canDoAction = - ability.can(Action.DatasetOrigdatablockReadAny, DatasetClass) || - ability.can(Action.DatasetOrigdatablockReadOwner, datasetInstance) || - ability.can(Action.DatasetOrigdatablockReadAccess, datasetInstance) || - ability.can(Action.DatasetOrigdatablockReadPublic, datasetInstance); - } else if (group == Action.DatasetOrigdatablockCreate) { - canDoAction = - ability.can(Action.DatasetOrigdatablockCreateAny, DatasetClass) || - ability.can(Action.DatasetOrigdatablockCreateOwner, datasetInstance); - } else if (group == Action.DatasetOrigdatablockUpdate) { - canDoAction = - ability.can(Action.DatasetOrigdatablockUpdateAny, DatasetClass) || - ability.can(Action.DatasetOrigdatablockUpdateOwner, datasetInstance); - } else if (group == Action.DatasetOrigdatablockDelete) { - canDoAction = - ability.can(Action.DatasetOrigdatablockDeleteAny, DatasetClass) || - ability.can(Action.DatasetOrigdatablockDeleteOwner, datasetInstance); - } else if (group == Action.DatasetDatablockRead) { - canDoAction = - ability.can(Action.DatasetOrigdatablockReadAny, DatasetClass) || - ability.can(Action.DatasetDatablockReadOwner, datasetInstance) || - ability.can(Action.DatasetDatablockReadAccess, datasetInstance) || - ability.can(Action.DatasetDatablockReadPublic, datasetInstance); - } else if (group == Action.DatasetDatablockCreate) { - canDoAction = - ability.can(Action.DatasetDatablockCreateAny, DatasetClass) || - ability.can(Action.DatasetDatablockCreateOwner, datasetInstance); - } else if (group == Action.DatasetDatablockUpdate) { - canDoAction = - ability.can(Action.DatasetDatablockUpdateAny, DatasetClass) || - ability.can(Action.DatasetDatablockUpdateOwner, datasetInstance); - } else if (group == Action.DatasetDatablockDelete) { - canDoAction = - ability.can(Action.DatasetDatablockDeleteAny, DatasetClass) || - ability.can(Action.DatasetDatablockDeleteOwner, datasetInstance); - } else if (group == Action.DatasetLogbookRead) { - canDoAction = - ability.can(Action.DatasetLogbookReadAny, DatasetClass) || - ability.can(Action.DatasetLogbookReadOwner, datasetInstance); - } - if (!canDoAction) { - throw new ForbiddenException("Unauthorized access"); - } + if (!dataset) { + throw new NotFoundException(`dataset: ${id} not found`); + } + + const datasetInstance = + await this.generateDatasetInstanceForPermissions(dataset); + + const ability = this.caslAbilityFactory.datasetInstanceAccess(user); + + let canDoAction = false; + + if (group == Action.DatasetRead) { + canDoAction = + ability.can(Action.DatasetReadAny, DatasetClass) || + ability.can(Action.DatasetReadOneOwner, datasetInstance) || + ability.can(Action.DatasetReadOneAccess, datasetInstance) || + ability.can(Action.DatasetReadOnePublic, datasetInstance); + } else if (group == Action.DatasetAttachmentRead) { + canDoAction = + ability.can(Action.DatasetAttachmentReadAny, DatasetClass) || + ability.can(Action.DatasetAttachmentReadOwner, datasetInstance) || + ability.can(Action.DatasetAttachmentReadAccess, datasetInstance) || + ability.can(Action.DatasetAttachmentReadPublic, datasetInstance); + } else if (group == Action.DatasetAttachmentCreate) { + canDoAction = + ability.can(Action.DatasetAttachmentCreateAny, DatasetClass) || + ability.can(Action.DatasetAttachmentCreateOwner, datasetInstance); + } else if (group == Action.DatasetAttachmentUpdate) { + canDoAction = + ability.can(Action.DatasetAttachmentUpdateAny, DatasetClass) || + ability.can(Action.DatasetAttachmentUpdateOwner, datasetInstance); + } else if (group == Action.DatasetAttachmentDelete) { + canDoAction = + ability.can(Action.DatasetAttachmentDeleteAny, DatasetClass) || + ability.can(Action.DatasetAttachmentDeleteOwner, datasetInstance); + } else if (group == Action.DatasetOrigdatablockRead) { + canDoAction = + ability.can(Action.DatasetOrigdatablockReadAny, DatasetClass) || + ability.can(Action.DatasetOrigdatablockReadOwner, datasetInstance) || + ability.can(Action.DatasetOrigdatablockReadAccess, datasetInstance) || + ability.can(Action.DatasetOrigdatablockReadPublic, datasetInstance); + } else if (group == Action.DatasetOrigdatablockCreate) { + canDoAction = + ability.can(Action.DatasetOrigdatablockCreateAny, DatasetClass) || + ability.can(Action.DatasetOrigdatablockCreateOwner, datasetInstance); + } else if (group == Action.DatasetOrigdatablockUpdate) { + canDoAction = + ability.can(Action.DatasetOrigdatablockUpdateAny, DatasetClass) || + ability.can(Action.DatasetOrigdatablockUpdateOwner, datasetInstance); + } else if (group == Action.DatasetOrigdatablockDelete) { + canDoAction = + ability.can(Action.DatasetOrigdatablockDeleteAny, DatasetClass) || + ability.can(Action.DatasetOrigdatablockDeleteOwner, datasetInstance); + } else if (group == Action.DatasetDatablockRead) { + canDoAction = + ability.can(Action.DatasetOrigdatablockReadAny, DatasetClass) || + ability.can(Action.DatasetDatablockReadOwner, datasetInstance) || + ability.can(Action.DatasetDatablockReadAccess, datasetInstance) || + ability.can(Action.DatasetDatablockReadPublic, datasetInstance); + } else if (group == Action.DatasetDatablockCreate) { + canDoAction = + ability.can(Action.DatasetDatablockCreateAny, DatasetClass) || + ability.can(Action.DatasetDatablockCreateOwner, datasetInstance); + } else if (group == Action.DatasetDatablockUpdate) { + canDoAction = + ability.can(Action.DatasetDatablockUpdateAny, DatasetClass) || + ability.can(Action.DatasetDatablockUpdateOwner, datasetInstance); + } else if (group == Action.DatasetDatablockDelete) { + canDoAction = + ability.can(Action.DatasetDatablockDeleteAny, DatasetClass) || + ability.can(Action.DatasetDatablockDeleteOwner, datasetInstance); + } else if (group == Action.DatasetLogbookRead) { + canDoAction = + ability.can(Action.DatasetLogbookReadAny, DatasetClass) || + ability.can(Action.DatasetLogbookReadOwner, datasetInstance); + } + if (!canDoAction) { + throw new ForbiddenException("Unauthorized access"); } return dataset; @@ -292,20 +301,22 @@ export class DatasetsController { const dataset = await this.datasetsService.findOne({ where: { pid: id } }); const user: JWTUser = request.user as JWTUser; - if (dataset) { - const datasetInstance = - await this.generateDatasetInstanceForPermissions(dataset); + if (!dataset) { + throw new NotFoundException(`dataset: ${id} not found`); + } - const ability = this.caslAbilityFactory.datasetInstanceAccess(user); - const canView = - ability.can(Action.DatasetReadAny, DatasetClass) || - ability.can(Action.DatasetReadOneOwner, datasetInstance) || - ability.can(Action.DatasetReadOneAccess, datasetInstance) || - ability.can(Action.DatasetReadOnePublic, datasetInstance); + const datasetInstance = + await this.generateDatasetInstanceForPermissions(dataset); - if (!canView) { - throw new ForbiddenException("Unauthorized access"); - } + const ability = this.caslAbilityFactory.datasetInstanceAccess(user); + const canView = + ability.can(Action.DatasetReadAny, DatasetClass) || + ability.can(Action.DatasetReadOneOwner, datasetInstance) || + ability.can(Action.DatasetReadOneAccess, datasetInstance) || + ability.can(Action.DatasetReadOnePublic, datasetInstance); + + if (!canView) { + throw new ForbiddenException("Unauthorized access"); } return dataset; @@ -359,43 +370,60 @@ export class DatasetsController { ) { const user: JWTUser = request.user as JWTUser; - if (dataset) { - // NOTE: We need DatasetClass instance because casl module can not recognize the type from dataset mongo database model. If other fields are needed can be added later. - const datasetInstance = - await this.generateDatasetInstanceForPermissions(dataset); - // instantiate the casl matrix for the user - const ability = this.caslAbilityFactory.datasetInstanceAccess(user); - // check if he/she can create this dataset - const canCreate = - ability.can(Action.DatasetCreateAny, DatasetClass) || - ability.can(Action.DatasetCreateOwnerNoPid, datasetInstance) || - ability.can(Action.DatasetCreateOwnerWithPid, datasetInstance); - - if (!canCreate) { - throw new ForbiddenException("Unauthorized to create this dataset"); - } + // NOTE: We need DatasetClass instance because casl module can not recognize the type from dataset mongo database model. If other fields are needed can be added later. + const datasetInstance = + await this.generateDatasetInstanceForPermissions(dataset); + // instantiate the casl matrix for the user + const ability = this.caslAbilityFactory.datasetInstanceAccess(user); + // check if he/she can create this dataset + const canCreate = + ability.can(Action.DatasetCreateAny, DatasetClass) || + ability.can(Action.DatasetCreateOwnerNoPid, datasetInstance) || + ability.can(Action.DatasetCreateOwnerWithPid, datasetInstance); - // now checks if we need to validate the pid - if ( - configuration().datasetCreationValidationEnabled && - configuration().datasetCreationValidationRegex && - dataset.pid - ) { - const re = new RegExp(configuration().datasetCreationValidationRegex); + if (!canCreate) { + throw new ForbiddenException("Unauthorized to create this dataset"); + } - if (!re.test(dataset.pid)) { - throw new BadRequestException( - "PID is not following required standards", - ); - } + // now checks if we need to validate the pid + if ( + configuration().datasetCreationValidationEnabled && + configuration().datasetCreationValidationRegex && + dataset.pid + ) { + const re = new RegExp(configuration().datasetCreationValidationRegex); + + if (!re.test(dataset.pid)) { + throw new BadRequestException( + "PID is not following required standards", + ); } - } else { - throw new ForbiddenException("Unauthorized to create datasets"); } return dataset; } + convertObsoleteWhereFilterToCurrentSchema( + whereFilter: Record, + ): IFilters { + if ("proposalId" in whereFilter) { + whereFilter.proposalIds = whereFilter.proposalId; + delete whereFilter.proposalId; + } + if ("sampleId" in whereFilter) { + whereFilter.sampleIds = whereFilter.sampleId; + delete whereFilter.sampleId; + } + if ("instrumentId" in whereFilter) { + whereFilter.instrumentIds = whereFilter.instrumentId; + delete whereFilter.instrumentId; + } + if ("principalInvestigator" in whereFilter) { + whereFilter.investigator = whereFilter.principalInvestigator; + delete whereFilter.principalInvestigator; + } + return whereFilter; + } convertObsoleteToCurrentSchema( inputObsoleteDataset: | CreateRawDatasetObsoleteDto @@ -527,7 +555,7 @@ export class DatasetsController { }, }) @ApiResponse({ - status: 201, + status: HttpStatus.CREATED, type: OutputDatasetObsoleteDto, description: "Create a new dataset and return its representation in SciCat", }) @@ -664,8 +692,8 @@ export class DatasetsController { }, }) @ApiResponse({ - status: 200, - type: Boolean, + status: HttpStatus.OK, + type: IsValidResponse, description: "Check if the dataset provided pass validation. It return true if the validation is passed", }) @@ -675,7 +703,7 @@ export class DatasetsController { createDatasetObsoleteDto: | CreateRawDatasetObsoleteDto | CreateDerivedDatasetObsoleteDto, - ): Promise<{ valid: boolean }> { + ) { await this.checkPermissionsForObsoleteDatasetCreate( request, createDatasetObsoleteDto, @@ -721,7 +749,7 @@ export class DatasetsController { example: filterExample, }) @ApiResponse({ - status: 200, + status: HttpStatus.OK, type: OutputDatasetObsoleteDto, isArray: true, description: "Return the datasets requested", @@ -730,7 +758,7 @@ export class DatasetsController { @Req() request: Request, @Headers() headers: Record, @Query(new FilterPipe()) queryFilter: { filter?: string }, - ): Promise { + ) { const mergedFilters = replaceLikeOperator( this.updateMergedFiltersForList( request, @@ -777,6 +805,8 @@ export class DatasetsController { }), ); } else { + /* eslint-disable @typescript-eslint/no-unused-expressions */ + // TODO: check the eslint error "Expected an assignment or function call and instead saw an expression" dataset; } }), @@ -798,33 +828,23 @@ export class DatasetsController { "It returns a list of datasets matching the query provided.
This endpoint still needs some work on the query specification.", }) @ApiQuery({ - name: "fields", - description: - "Database filters to apply when retrieving datasets\n" + - datasetsFullQueryDescriptionFields, + name: "filters", + description: "Defines query limits and fields", required: false, - type: String, - example: datasetsFullQueryExampleFields, - }) - @ApiQuery({ - name: "limits", - description: - "Define further query parameters like skip, limit, order\n" + - fullQueryDescriptionLimits, - required: false, - type: String, - example: fullQueryExampleLimits, + type: FullQueryFilters, + example: + '{"limits": {"limit": 1, "skip": 1, "order": "creationTime:desc"}, fields: {"ownerGroup":["group1"],"scientific":[{"lhs":"sample","relation":"EQUAL_TO_STRING","rhs":"my sample"}]}}', }) @ApiResponse({ - status: 200, + status: HttpStatus.OK, type: OutputDatasetObsoleteDto, isArray: true, - description: "Return datasets requested", + description: "Return fullquery response for datasets requested", }) async fullquery( @Req() request: Request, @Query() filters: { fields?: string; limits?: string }, - ): Promise { + ) { const user: JWTUser = request.user as JWTUser; const fields: IDatasetFields = JSON.parse(filters.fields ?? "{}"); @@ -840,21 +860,13 @@ export class DatasetsController { Action.DatasetReadManyOwner, DatasetClass, ); - // const canViewPublic = ability.can( - // Action.DatasetReadManyPublic, - // DatasetClass, - // ); if (canViewAccess) { fields.userGroups = fields.userGroups ?? []; fields.userGroups.push(...user.currentGroups); - // fields.sharedWith = user.email; } else if (canViewOwner) { fields.ownerGroup = fields.ownerGroup ?? []; fields.ownerGroup.push(...user.currentGroups); } - // else if (canViewPublic) { - // fields.isPublished = true; - // } } const parsedFilters: IFilters = { fields: fields, @@ -873,33 +885,20 @@ export class DatasetsController { ) @UseInterceptors(SubDatasetsPublicInterceptor) @Get("/fullfacet") - @ApiOperation({ - summary: - "It returns a list of dataset facets matching the filter provided.", - description: - "It returns a list of dataset facets matching the filter provided.
This endpoint still needs some work on the filter and facets specification.", - }) - @ApiQuery({ - name: "fields", - description: - "Define the filter conditions by specifying the name of values of fields requested. There is also support for a `text` search to look for strings anywhere in the dataset.", - required: false, - type: String, - example: {}, - }) @ApiQuery({ - name: "facets", + name: "filters", description: "Defines list of field names, for which facet counts should be calculated", required: false, - type: String, - example: '["type","creationLocation","ownerGroup","keywords"]', + type: FullFacetFilters, + example: + '{"facets": ["type","creationLocation","ownerGroup","keywords"], fields: {}}', }) @ApiResponse({ - status: 200, - type: Object, + status: HttpStatus.OK, + type: FullFacetResponse, isArray: true, - description: "Return datasets requested", + description: "Return fullfacet response for datasets requested", }) async fullfacet( @Req() request: Request, @@ -912,8 +911,6 @@ export class DatasetsController { const canViewAny = ability.can(Action.DatasetReadAny, DatasetClass); if (!canViewAny && !fields.isPublished) { - // delete fields.isPublished; - const canViewAccess = ability.can( Action.DatasetReadManyAccess, DatasetClass, @@ -922,23 +919,14 @@ export class DatasetsController { Action.DatasetReadManyOwner, DatasetClass, ); - // const canViewPublic = ability.can( - // Action.DatasetReadManyPublic, - // DatasetClass, - // ); if (canViewAccess) { fields.userGroups = fields.userGroups ?? []; fields.userGroups.push(...user.currentGroups); - // fields.isPublished = true; - // fields.sharedWith = user.email; } else if (canViewOwner) { fields.ownerGroup = fields.ownerGroup ?? []; fields.ownerGroup.push(...user.currentGroups); } - // else if (canViewPublic) { - // fields.isPublished = true; - // } } const parsedFilters: IFacets = { @@ -977,15 +965,15 @@ export class DatasetsController { example: '{ "skip": 0, "limit": 25, "order": "creationTime:desc" }', }) @ApiResponse({ - status: 200, + status: HttpStatus.OK, type: String, isArray: true, - description: "Return metadata keys list of datasets selected", + description: "Return metadata keys for list of datasets selected", }) async metadataKeys( @Req() request: Request, @Query() filters: { fields?: string; limits?: string }, - ): Promise { + ) { const user: JWTUser = request.user as JWTUser; const fields: IDatasetFields = JSON.parse(filters.fields ?? "{}"); @@ -1048,7 +1036,7 @@ export class DatasetsController { example: filterExample, }) @ApiResponse({ - status: 200, + status: HttpStatus.OK, type: OutputDatasetObsoleteDto, description: "Return the datasets requested", }) @@ -1122,7 +1110,8 @@ export class DatasetsController { '{"where": {"pid": "20.500.12269/4f8c991e-a879-4e00-9095-5bb13fb02ac4"}}', }) @ApiResponse({ - status: 200, + status: HttpStatus.OK, + type: CountApiResponse, description: "Return the number of datasets in the following format: { count: integer }", }) @@ -1130,7 +1119,7 @@ export class DatasetsController { @Req() request: Request, @Headers() headers: Record, @Query(new FilterPipe()) queryFilter: { filter?: string }, - ): Promise<{ count: number }> { + ) { const mergedFilters = replaceLikeOperator( this.updateMergedFiltersForList( request, @@ -1148,25 +1137,18 @@ export class DatasetsController { ability.can(Action.DatasetRead, DatasetClass), ) @Get("/:pid") - @ApiOperation({ - summary: "It returns the dataset requested.", - description: "It returns the dataset requested through the pid specified.", - }) @ApiParam({ name: "pid", description: "Id of the dataset to return", type: String, }) @ApiResponse({ - status: 200, + status: HttpStatus.OK, type: OutputDatasetObsoleteDto, isArray: false, description: "Return dataset with pid specified", }) - async findById( - @Req() request: Request, - @Param("pid") id: string, - ): Promise { + async findById(@Req() request: Request, @Param("pid") id: string) { const dataset = this.convertCurrentToObsoleteSchema( await this.checkPermissionsForDatasetObsolete(request, id), ); @@ -1213,7 +1195,7 @@ export class DatasetsController { }, }) @ApiResponse({ - status: 200, + status: HttpStatus.OK, type: OutputDatasetObsoleteDto, description: "Update an existing dataset and return its representation in SciCat", @@ -1226,7 +1208,9 @@ export class DatasetsController { | PartialUpdateRawDatasetObsoleteDto | PartialUpdateDerivedDatasetObsoleteDto, ): Promise { - const foundDataset = await this.datasetsService.findOne({ where: { pid } }); + const foundDataset = await this.datasetsService.findOne({ + where: { pid }, + }); if (!foundDataset) { throw new NotFoundException(); @@ -1306,7 +1290,7 @@ export class DatasetsController { }, }) @ApiResponse({ - status: 200, + status: HttpStatus.OK, type: OutputDatasetObsoleteDto, description: "Update an existing dataset and return its representation in SciCat", @@ -1319,7 +1303,9 @@ export class DatasetsController { | UpdateRawDatasetObsoleteDto | UpdateDerivedDatasetObsoleteDto, ): Promise { - const foundDataset = await this.datasetsService.findOne({ where: { pid } }); + const foundDataset = await this.datasetsService.findOne({ + where: { pid }, + }); if (!foundDataset) { throw new NotFoundException(); @@ -1375,14 +1361,14 @@ export class DatasetsController { type: String, }) @ApiResponse({ - status: 200, - description: "No value is returned", + status: HttpStatus.OK, + type: DatasetClass, + description: "DatasetClass value is returned that is removed", }) - async findByIdAndDelete( - @Req() request: Request, - @Param("pid") pid: string, - ): Promise { - const foundDataset = await this.datasetsService.findOne({ where: { pid } }); + async findByIdAndDelete(@Req() request: Request, @Param("pid") pid: string) { + const foundDataset = await this.datasetsService.findOne({ + where: { pid }, + }); if (!foundDataset) { throw new NotFoundException(); @@ -1433,7 +1419,7 @@ export class DatasetsController { type: Array, }) @ApiResponse({ - status: 200, + status: HttpStatus.OK, type: OutputDatasetObsoleteDto, description: "Return new value of the dataset", }) @@ -1446,7 +1432,7 @@ export class DatasetsController { const user: JWTUser = request.user as JWTUser; const ability = this.caslAbilityFactory.datasetInstanceAccess(user); const datasetToUpdate = await this.datasetsService.findOne({ - where: { pid: pid }, + where: { pid }, }); if (!datasetToUpdate) { @@ -1499,7 +1485,7 @@ export class DatasetsController { type: String, }) @ApiResponse({ - status: 200, + status: HttpStatus.OK, type: Attachment, description: "Return new value of the dataset", }) @@ -1548,7 +1534,7 @@ export class DatasetsController { type: CreateAttachmentDto, }) @ApiResponse({ - status: 201, + status: HttpStatus.CREATED, type: Attachment, description: "Returns the new attachment for the dataset identified by the pid specified", @@ -1593,7 +1579,7 @@ export class DatasetsController { type: String, }) @ApiResponse({ - status: 200, + status: HttpStatus.OK, type: Attachment, isArray: true, description: @@ -1636,7 +1622,7 @@ export class DatasetsController { type: String, }) @ApiResponse({ - status: 200, + status: HttpStatus.OK, type: Attachment, isArray: false, description: "Returns the attachment updated.", @@ -1683,14 +1669,14 @@ export class DatasetsController { type: String, }) @ApiResponse({ - status: 200, + status: HttpStatus.OK, description: "No value is returned.", }) async findOneAttachmentAndRemove( @Req() request: Request, @Param("pid") pid: string, @Param("aid") aid: string, - ): Promise { + ) { await this.checkPermissionsForDatasetExtended( request, pid, @@ -1699,7 +1685,7 @@ export class DatasetsController { return this.attachmentsService.findOneAndDelete({ _id: aid, - pid, + datasetId: pid, }); } @@ -1730,7 +1716,7 @@ export class DatasetsController { type: CreateDatasetOrigDatablockDto, }) @ApiResponse({ - status: 201, + status: HttpStatus.CREATED, type: OrigDatablock, description: "It returns the new original datablock created", }) @@ -1792,7 +1778,7 @@ export class DatasetsController { type: CreateDatasetOrigDatablockDto, }) @ApiResponse({ - status: 201, + status: HttpStatus.CREATED, description: "It returns true if the values passed in are a valid original datablock", }) @@ -1836,7 +1822,7 @@ export class DatasetsController { type: String, }) @ApiResponse({ - status: 200, + status: HttpStatus.OK, type: OrigDatablock, isArray: true, description: @@ -1885,7 +1871,7 @@ export class DatasetsController { type: String, }) @ApiResponse({ - status: 200, + status: HttpStatus.OK, type: OrigDatablock, isArray: false, description: @@ -1950,7 +1936,7 @@ export class DatasetsController { type: String, }) @ApiResponse({ - status: 200, + status: HttpStatus.OK, description: "No value is returned", }) async findOneOrigDatablockAndRemove( @@ -2013,7 +1999,7 @@ export class DatasetsController { type: CreateDatasetDatablockDto, }) @ApiResponse({ - status: 201, + status: HttpStatus.CREATED, type: Datablock, description: "It returns the new datablock created", }) @@ -2067,7 +2053,7 @@ export class DatasetsController { type: String, }) @ApiResponse({ - status: 200, + status: HttpStatus.OK, type: Datablock, isArray: true, description: @@ -2113,7 +2099,7 @@ export class DatasetsController { type: String, }) @ApiResponse({ - status: 200, + status: HttpStatus.OK, type: Datablock, isArray: false, description: @@ -2180,7 +2166,7 @@ export class DatasetsController { type: String, }) @ApiResponse({ - status: 200, + status: HttpStatus.OK, description: "No value is returned", }) async findOneDatablockAndRemove( @@ -2242,8 +2228,8 @@ export class DatasetsController { type: String, }) @ApiResponse({ - status: 200, - // type: Logbook, + status: HttpStatus.OK, + type: Logbook, isArray: false, description: "It returns all messages from specificied Logbook room", }) diff --git a/src/datasets/datasets.service.ts b/src/datasets/datasets.service.ts index b43385a3f..26582443e 100644 --- a/src/datasets/datasets.service.ts +++ b/src/datasets/datasets.service.ts @@ -282,7 +282,7 @@ export class DatasetsService { return await this.datasetModel.findOneAndDelete({ pid: id }); } // GET datasets without _id which is used for elastic search data synchronization - async getDatasetsWithoutId() { + async getDatasetsWithoutId(): Promise { try { const datasets = this.datasetModel.find({}, { _id: 0 }).lean().exec(); return datasets; diff --git a/src/jobs/interfaces/dataset-list.interface.ts b/src/jobs/interfaces/dataset-list.interface.ts index 24ea3d6a5..4f20f355c 100644 --- a/src/jobs/interfaces/dataset-list.interface.ts +++ b/src/jobs/interfaces/dataset-list.interface.ts @@ -1,4 +1,9 @@ -export interface IDatasetList { +import { ApiProperty } from "@nestjs/swagger"; + +export class IDatasetList { + @ApiProperty() pid: string; + + @ApiProperty({ type: String, isArray: true }) files: string[]; } diff --git a/src/jobs/schemas/job.schema.ts b/src/jobs/schemas/job.schema.ts index ce5fd8078..649b2b41b 100644 --- a/src/jobs/schemas/job.schema.ts +++ b/src/jobs/schemas/job.schema.ts @@ -2,6 +2,7 @@ import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; import { ApiProperty } from "@nestjs/swagger"; import { Document } from "mongoose"; import { v4 as uuidv4 } from "uuid"; +import { IDatasetList } from "../interfaces/dataset-list.interface"; import { JobType } from "../job-type.enum"; export type JobDocument = JobClass & Document; @@ -22,6 +23,11 @@ export class JobClass { @Prop({ type: String, default: () => uuidv4() }) _id: string; + @ApiProperty({ + type: String, + description: "Globally unique identifier of a job.", + readOnly: true, + }) id?: string; @ApiProperty({ @@ -58,22 +64,25 @@ export class JobClass { "Object of key-value pairs defining job input parameters, e.g. 'destinationPath' for retrieve jobs or 'tapeCopies' for archive jobs.", }) @Prop({ type: Object, required: false }) - jobParams: Record; + jobParams: object; @ApiProperty({ description: "Defines current status of job lifecycle." }) @Prop({ type: String, required: false }) jobStatusMessage: string; @ApiProperty({ + type: IDatasetList, + isArray: true, + required: false, description: "Array of objects with keys: pid, files. The value for the pid key defines the dataset ID, the value for the files key is an array of file names. This array is either an empty array, implying that all files within the dataset are selected or an explicit list of dataset-relative file paths, which should be selected.", }) @Prop({ type: [Object], required: false }) - datasetList: Record[]; + datasetList: IDatasetList[]; @ApiProperty({ description: "Detailed return value after job is finished." }) @Prop({ type: Object, required: false }) - jobResultObject: Record; + jobResultObject: object; @ApiProperty({ type: String, diff --git a/src/logbooks/schemas/logbook.schema.ts b/src/logbooks/schemas/logbook.schema.ts index a25ba3ab1..7c94b5e4a 100644 --- a/src/logbooks/schemas/logbook.schema.ts +++ b/src/logbooks/schemas/logbook.schema.ts @@ -1,5 +1,5 @@ import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; -import { ApiProperty, getSchemaPath } from "@nestjs/swagger"; +import { ApiProperty } from "@nestjs/swagger"; import { Document } from "mongoose"; import { Message, MessageSchema } from "./message.schema"; @@ -15,7 +15,10 @@ export class Logbook { @Prop() roomId: string; - @ApiProperty({ type: "array", items: { $ref: getSchemaPath(Message) } }) + @ApiProperty({ + isArray: true, + type: Message, + }) @Prop([MessageSchema]) messages: Message[]; } diff --git a/src/loggers/logger.service.ts b/src/loggers/logger.service.ts index 0034b5db0..d016b8786 100644 --- a/src/loggers/logger.service.ts +++ b/src/loggers/logger.service.ts @@ -39,7 +39,7 @@ export class ScicatLogger implements Logger, OnModuleInit { private handleLogForwarding(method: keyof Logger, ...args: unknown[]): void { for (const logger of this.loggers) { try { - // eslint-disable-next-line @typescript-eslint/ban-types -- We need to call the method dynamically + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -- We need to call the method dynamically (logger[method] as Function)(...args); } catch (error) { console.error(error); diff --git a/src/origdatablocks/origdatablocks.controller.ts b/src/origdatablocks/origdatablocks.controller.ts index 97e21d50b..66b0e5777 100644 --- a/src/origdatablocks/origdatablocks.controller.ts +++ b/src/origdatablocks/origdatablocks.controller.ts @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/quotes */ +/* eslint-disable @/quotes */ import { Controller, Get, @@ -11,9 +11,9 @@ import { Query, HttpCode, HttpStatus, - BadRequestException, Req, ForbiddenException, + NotFoundException, } from "@nestjs/common"; import { Request } from "express"; import { OrigDatablocksService } from "./origdatablocks.service"; @@ -65,7 +65,7 @@ export class OrigDatablocksController { ) { const origDatablock = await this.origDatablocksService.findOne({ _id: id }); if (!origDatablock) { - throw new BadRequestException("Invalid origDatablock Id"); + throw new NotFoundException(`OrigDatablock: ${id} not found`); } await this.checkPermissionsForOrigDatablockExtended( @@ -125,7 +125,9 @@ export class OrigDatablocksController { const user: JWTUser = request.user as JWTUser; if (!dataset) { - throw new BadRequestException(`Invalid datasetId: ${id}`); + throw new NotFoundException( + `Dataset: ${id} not found for attaching an origdatablock`, + ); } const origDatablockInstance = diff --git a/src/policies/policies.controller.ts b/src/policies/policies.controller.ts index bbc9353dd..18ed87d7a 100644 --- a/src/policies/policies.controller.ts +++ b/src/policies/policies.controller.ts @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/quotes */ +/* eslint-disable @/quotes */ import { Controller, Get, @@ -20,7 +20,7 @@ import { Request } from "express"; import { PoliciesService } from "./policies.service"; import { CreatePolicyDto } from "./dto/create-policy.dto"; import { PartialUpdatePolicyDto } from "./dto/update-policy.dto"; -import { ApiBearerAuth, ApiQuery, ApiTags } from "@nestjs/swagger"; +import { ApiBearerAuth, ApiQuery, ApiResponse, ApiTags } from "@nestjs/swagger"; import { PoliciesGuard } from "src/casl/guards/policies.guard"; import { CheckPolicies } from "src/casl/decorators/check-policies.decorator"; import { AppAbility, CaslAbilityFactory } from "src/casl/casl-ability.factory"; @@ -32,7 +32,7 @@ import { HistoryInterceptor } from "src/common/interceptors/history.interceptor" import { UpdateWherePolicyDto } from "./dto/update-where-policy.dto"; import { IFilters } from "src/common/interfaces/common.interface"; import { JWTUser } from "src/auth/interfaces/jwt-user.interface"; -import { replaceLikeOperator } from "src/common/utils"; +import { CountApiResponse, replaceLikeOperator } from "src/common/utils"; import { FilterPipe } from "src/common/pipes/filter.pipe"; @ApiBearerAuth() @@ -142,7 +142,19 @@ export class PoliciesController { ability.can(Action.Read, Policy), ) @Get("/count") - async count(@Query("where") where?: string): Promise<{ count: number }> { + @ApiQuery({ + name: "where", + description: "Database filters to apply when retrieving count for polices", + required: false, + type: String, + }) + @ApiResponse({ + status: HttpStatus.OK, + type: CountApiResponse, + description: + "Return the number of datasets in the following format: { count: integer }", + }) + async count(@Query("where") where?: string) { const parsedWhere: FilterQuery = JSON.parse(where ?? "{}"); return this.policiesService.count(parsedWhere); } diff --git a/src/policies/policies.service.ts b/src/policies/policies.service.ts index b6eb82801..4896c9cb4 100644 --- a/src/policies/policies.service.ts +++ b/src/policies/policies.service.ts @@ -157,7 +157,7 @@ export class PoliciesService implements OnModuleInit { const ownerGroups = ownerGroupList .split(",") - // eslint-disable-next-line @typescript-eslint/quotes + // eslint-disable-next-line @/quotes .map((ownerGroup) => ownerGroup.trim().replace(new RegExp('"', "g"), "")); if (!ownerGroups) { throw new InternalServerErrorException( diff --git a/src/proposals/dto/update-proposal.dto.ts b/src/proposals/dto/update-proposal.dto.ts index 1ce6ce0f8..c16ef17cb 100644 --- a/src/proposals/dto/update-proposal.dto.ts +++ b/src/proposals/dto/update-proposal.dto.ts @@ -4,6 +4,7 @@ import { IsArray, IsDateString, IsEmail, + IsEnum, IsObject, IsOptional, IsString, @@ -124,6 +125,25 @@ export class UpdateProposalDto extends OwnableDto { @IsOptional() @IsObject() readonly metadata?: Record; + + @ApiProperty({ + type: String, + required: false, + description: "Parent proposal id.", + }) + @IsOptional() + @IsString() + readonly parentProposalId?: string; + + @ApiProperty({ + type: String, + required: false, + description: + "Characterize type of proposal, use some of the configured values", + }) + @IsOptional() + @IsString() + readonly type?: string; } export class PartialUpdateProposalDto extends PartialType(UpdateProposalDto) {} diff --git a/src/proposals/proposals.controller.ts b/src/proposals/proposals.controller.ts index ff509c0fc..6b28b4d21 100644 --- a/src/proposals/proposals.controller.ts +++ b/src/proposals/proposals.controller.ts @@ -14,9 +14,9 @@ import { Req, ForbiddenException, ConflictException, - BadRequestException, Logger, InternalServerErrorException, + NotFoundException, } from "@nestjs/common"; import { Request } from "express"; import { ProposalsService } from "./proposals.service"; @@ -56,13 +56,15 @@ import { validate, ValidatorOptions } from "class-validator"; import { filterDescription, filterExample, - fullQueryDescriptionLimits, + FullFacetResponse, fullQueryExampleLimits, + FullQueryFilters, proposalsFullQueryDescriptionFields, proposalsFullQueryExampleFields, } from "src/common/utils"; import { JWTUser } from "src/auth/interfaces/jwt-user.interface"; import { IDatasetFields } from "src/datasets/interfaces/dataset-filters.interface"; +import { FindByIdAccessResponse } from "src/samples/samples.controller"; @ApiBearerAuth() @ApiTags("proposals") @@ -89,14 +91,17 @@ export class ProposalsController { return proposalInstance; } - private async permissionChecker( + private permissionChecker( group: Action, - proposal: ProposalClass | CreateProposalDto, + proposal: ProposalClass | CreateProposalDto | null, request: Request, ) { - const proposalInstance = this.generateProposalInstanceForPermissions( - proposal as ProposalClass, - ); + if (!proposal) { + return false; + } + + const proposalInstance = + this.generateProposalInstanceForPermissions(proposal); const user: JWTUser = request.user as JWTUser; const ability = this.caslAbilityFactory.proposalsInstanceAccess(user); @@ -172,34 +177,33 @@ export class ProposalsController { proposalId: id, }); - if (proposal) { - const canDoAction = await this.permissionChecker( - group, - proposal, - request, - ); + if (!proposal) { + throw new NotFoundException(`Proposal: ${id} not found`); + } - if (!canDoAction) { - throw new ForbiddenException("Unauthorized to this proposal"); - } + const canDoAction = this.permissionChecker(group, proposal, request); + + if (!canDoAction) { + throw new ForbiddenException("Unauthorized to this proposal"); } + return proposal; } - private async checkPermissionsForProposalCreate( + private checkPermissionsForProposalCreate( request: Request, proposal: CreateProposalDto, group: Action, ) { - if (!proposal) { - throw new BadRequestException("Not able to create this proposal"); - } - const canDoAction = await this.permissionChecker(group, proposal, request); + const canDoAction = this.permissionChecker(group, proposal, request); + if (!canDoAction) { throw new ForbiddenException("Unauthorized to create this proposal"); } + return proposal; } + updateFiltersForList( request: Request, mergedFilters: IFilters, @@ -272,7 +276,7 @@ export class ProposalsController { @Req() request: Request, @Body() createProposalDto: CreateProposalDto, ): Promise { - const proposalDTO = await this.checkPermissionsForProposalCreate( + const proposalDTO = this.checkPermissionsForProposalCreate( request, createProposalDto, Action.ProposalsCreate, @@ -376,25 +380,14 @@ export class ProposalsController { "It returns a list of proposals matching the query provided.
This endpoint still needs some work on the query specification.", }) @ApiQuery({ - name: "fields", - description: - "Full query filters to apply when retrieving proposals\n" + - proposalsFullQueryDescriptionFields, - required: false, - type: String, - example: proposalsFullQueryExampleFields, - }) - @ApiQuery({ - name: "limits", - description: - "Define further query parameters like skip, limit, order\n" + - fullQueryDescriptionLimits, + name: "filters", + description: "Defines query limits and fields", required: false, - type: String, - example: fullQueryExampleLimits, + type: FullQueryFilters, + example: `{"limits": ${fullQueryExampleLimits}, fields: ${proposalsFullQueryExampleFields}}`, }) @ApiResponse({ - status: 200, + status: HttpStatus.OK, type: ProposalClass, isArray: true, description: "Return proposals requested", @@ -466,7 +459,7 @@ export class ProposalsController { }) @ApiResponse({ status: 200, - type: ProposalClass, + type: FullFacetResponse, isArray: true, description: "Return proposals requested", }) @@ -567,9 +560,9 @@ export class ProposalsController { }) @ApiResponse({ status: HttpStatus.OK, - type: Boolean, + type: FindByIdAccessResponse, description: - "Returns true if the user has access to the specified proposal, otherwise false.", + "Returns canAccess property with boolean true if the user has access to the specified sample, otherwise false.", }) async findByIdAccess( @Req() request: Request, @@ -578,9 +571,8 @@ export class ProposalsController { const proposal = await this.proposalsService.findOne({ proposalId, }); - if (!proposal) return { canAccess: false }; - const canAccess = await this.permissionChecker( + const canAccess = this.permissionChecker( Action.ProposalsRead, proposal, request, diff --git a/src/proposals/proposals.module.ts b/src/proposals/proposals.module.ts index 557081bdb..c684c2e87 100644 --- a/src/proposals/proposals.module.ts +++ b/src/proposals/proposals.module.ts @@ -1,4 +1,4 @@ -import { forwardRef, Module } from "@nestjs/common"; +import { BadRequestException, forwardRef, Module } from "@nestjs/common"; import { ProposalsService } from "./proposals.service"; import { ProposalsController } from "./proposals.controller"; import { MongooseModule } from "@nestjs/mongoose"; @@ -6,6 +6,7 @@ import { ProposalClass, ProposalSchema } from "./schemas/proposal.schema"; import { CaslAbilityFactory } from "src/casl/casl-ability.factory"; import { AttachmentsModule } from "src/attachments/attachments.module"; import { DatasetsModule } from "src/datasets/datasets.module"; +import { ConfigModule, ConfigService } from "@nestjs/config"; @Module({ imports: [ @@ -14,7 +15,11 @@ import { DatasetsModule } from "src/datasets/datasets.module"; MongooseModule.forFeatureAsync([ { name: ProposalClass.name, - useFactory: () => { + imports: [ConfigModule], + inject: [ConfigService], + useFactory: async (configService: ConfigService) => { + const proposalTypes = configService.get("proposalTypes") || "{}"; + const proposalTypesArray: string[] = Object.values(proposalTypes); const schema = ProposalSchema; schema.pre("save", function (next) { @@ -23,8 +28,16 @@ import { DatasetsModule } from "src/datasets/datasets.module"; if (!this._id) { this._id = this.proposalId; } + + if (this.type && !proposalTypesArray.includes(this.type)) { + throw new BadRequestException( + `type must be one of the following values: ${proposalTypesArray.join(", ")}`, + ); + } + next(); }); + return schema; }, }, diff --git a/src/proposals/proposals.service.ts b/src/proposals/proposals.service.ts index 610163b32..09258fdc0 100644 --- a/src/proposals/proposals.service.ts +++ b/src/proposals/proposals.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable, Scope } from "@nestjs/common"; +import { Inject, Injectable, NotFoundException, Scope } from "@nestjs/common"; import { REQUEST } from "@nestjs/core"; import { Request } from "express"; import { InjectModel } from "@nestjs/mongoose"; @@ -97,15 +97,17 @@ export class ProposalsService { ): Promise { const username = (this.request.user as JWTUser).username; - return this.proposalModel - .findOneAndUpdate( - filter, - addUpdatedByField(updateProposalDto, username), - { - new: true, - }, - ) - .exec(); + const proposal = await this.proposalModel.findOne(filter); + + if (!proposal) { + throw new NotFoundException(`Proposal with filter: ${filter} not found`); + } + + Object.assign(proposal, addUpdatedByField(updateProposalDto, username)); + + const updatedProposal = new this.proposalModel(proposal); + + return updatedProposal.save(); } async remove(filter: FilterQuery): Promise { diff --git a/src/proposals/schemas/proposal.schema.ts b/src/proposals/schemas/proposal.schema.ts index 8e26a1692..a51110203 100644 --- a/src/proposals/schemas/proposal.schema.ts +++ b/src/proposals/schemas/proposal.schema.ts @@ -1,6 +1,6 @@ import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; import { ApiProperty } from "@nestjs/swagger"; -import { Document } from "mongoose"; +import { Document, Schema as MongooseSchema } from "mongoose"; import { OwnableClass } from "src/common/schemas/ownable.schema"; import { @@ -8,6 +8,9 @@ import { MeasurementPeriodSchema, } from "./measurement-period.schema"; +// NOTE: This is the proposal default type and it will be used if no proposalTypes.json config file is provided +export const DEFAULT_PROPOSAL_TYPE = "Default Proposal"; + export type ProposalDocument = ProposalClass & Document; @Schema({ collection: "Proposal", @@ -15,6 +18,7 @@ export type ProposalDocument = ProposalClass & Document; getters: true, }, timestamps: true, + minimize: false, }) export class ProposalClass extends OwnableClass { @ApiProperty({ @@ -160,13 +164,42 @@ export class ProposalClass extends OwnableClass { MeasurementPeriodList?: MeasurementPeriodClass[]; @ApiProperty({ - type: Object, + type: MongooseSchema.Types.Mixed, required: false, default: {}, description: "JSON object containing the proposal metadata.", }) - @Prop({ type: Object, required: false, default: {} }) + @Prop({ type: MongooseSchema.Types.Mixed, required: false, default: {} }) metadata?: Record; + + @ApiProperty({ + type: String, + required: false, + description: "Parent proposal id", + default: null, + nullable: true, + }) + @Prop({ + type: String, + required: false, + default: null, + ref: "Proposal", + }) + parentProposalId: string; + + @ApiProperty({ + type: String, + required: true, + default: DEFAULT_PROPOSAL_TYPE, + description: + "Characterize type of proposal, use some of the configured values", + }) + @Prop({ + type: String, + required: true, + default: DEFAULT_PROPOSAL_TYPE, + }) + type: string; } export const ProposalSchema = SchemaFactory.createForClass(ProposalClass); diff --git a/src/published-data/interfaces/published-data.interface.ts b/src/published-data/interfaces/published-data.interface.ts index 96c71e4d9..9c3585767 100644 --- a/src/published-data/interfaces/published-data.interface.ts +++ b/src/published-data/interfaces/published-data.interface.ts @@ -1,3 +1,4 @@ +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { FilterQuery } from "mongoose"; import { PublishedDataDocument } from "../schemas/published-data.schema"; @@ -14,15 +15,25 @@ export interface IPublishedDataFilters { }; } -export interface ICount { +export class ICount { + @ApiProperty() count: number; } -export interface IFormPopulateData { +export class FormPopulateData { + @ApiPropertyOptional() resourceType?: string; + + @ApiPropertyOptional() description?: string; + + @ApiPropertyOptional() title?: string; + + @ApiPropertyOptional() abstract?: string; + + @ApiPropertyOptional() thumbnail?: string; } diff --git a/src/published-data/published-data.controller.ts b/src/published-data/published-data.controller.ts index 12dd71a5c..346b3f560 100644 --- a/src/published-data/published-data.controller.ts +++ b/src/published-data/published-data.controller.ts @@ -39,7 +39,7 @@ import { } from "./schemas/published-data.schema"; import { ICount, - IFormPopulateData, + FormPopulateData, IPublishedDataFilters, IRegister, } from "./interfaces/published-data.interface"; @@ -94,11 +94,17 @@ export class PublishedDataController { description: "Database limits to apply when retrieve all published data", required: false, }) + @ApiResponse({ + status: HttpStatus.OK, + type: PublishedData, + isArray: true, + description: "Results with a published documents array", + }) async findAll( @Query("filter") filter?: string, @Query("limits") limits?: string, @Query("fields") fields?: string, - ): Promise { + ) { const publishedDataFilters: IPublishedDataFilters = JSON.parse( filter ?? "{}", ); @@ -128,9 +134,13 @@ export class PublishedDataController { description: "Database filters to apply when retrieve published data count", required: false, }) - async count( - @Query() filter?: { filter: string; fields: string }, - ): Promise { + @ApiResponse({ + status: HttpStatus.OK, + type: ICount, + isArray: false, + description: "Results with a count of the published documents", + }) + async count(@Query() filter?: { filter: string; fields: string }) { const jsonFilters: IPublishedDataFilters = filter?.filter ? JSON.parse(filter.filter) : {}; @@ -162,8 +172,14 @@ export class PublishedDataController { description: "Dataset pid used to fetch form data.", required: true, }) + @ApiResponse({ + status: HttpStatus.OK, + type: FormPopulateData, + isArray: false, + description: "Return form populate data", + }) async formPopulate(@Query("pid") pid: string) { - const formData: IFormPopulateData = {}; + const formData: FormPopulateData = {}; const dataset = (await this.datasetsService.findOne({ where: { pid }, })) as unknown as DatasetClass; diff --git a/src/samples/samples.controller.ts b/src/samples/samples.controller.ts index e5c5feaf7..048dab555 100644 --- a/src/samples/samples.controller.ts +++ b/src/samples/samples.controller.ts @@ -17,6 +17,7 @@ import { BadRequestException, Req, Header, + NotFoundException, } from "@nestjs/common"; import { SamplesService } from "./samples.service"; import { CreateSampleDto } from "./dto/create-sample.dto"; @@ -27,6 +28,7 @@ import { ApiExtraModels, ApiOperation, ApiParam, + ApiProperty, ApiQuery, ApiResponse, ApiTags, @@ -54,9 +56,8 @@ import { import { filterDescription, filterExample, - fullQueryDescriptionLimits, fullQueryExampleLimits, - samplesFullQueryDescriptionFields, + FullQueryFilters, samplesFullQueryExampleFields, } from "src/common/utils"; import { Request } from "express"; @@ -65,6 +66,11 @@ import { IDatasetFields } from "src/datasets/interfaces/dataset-filters.interfac import { CreateSubAttachmentDto } from "src/attachments/dto/create-sub-attachment.dto"; import { AuthenticatedPoliciesGuard } from "src/casl/guards/auth-check.guard"; +export class FindByIdAccessResponse { + @ApiProperty({ type: Boolean }) + canAccess: boolean; +} + @ApiBearerAuth() @ApiTags("samples") @Controller("samples") @@ -87,14 +93,17 @@ export class SamplesController { return sampleInstance; } - private async permissionChecker( + + private permissionChecker( group: Action, - sample: SampleClass | CreateSampleDto, + sample: SampleClass | CreateSampleDto | null, request: Request, ) { - const sampleInstance = this.generateSampleInstanceForPermissions( - sample as SampleClass, - ); + if (!sample) { + return false; + } + + const sampleInstance = this.generateSampleInstanceForPermissions(sample); const user: JWTUser = request.user as JWTUser; const ability = this.caslAbilityFactory.samplesInstanceAccess(user); @@ -164,28 +173,30 @@ export class SamplesController { sampleId: id, }); - if (sample) { - const canDoAction = await this.permissionChecker(group, sample, request); - if (!canDoAction) { - throw new ForbiddenException("Unauthorized to this sample"); - } + if (!sample) { + throw new NotFoundException(`Sample: ${id} not found`); + } + + const canDoAction = this.permissionChecker(group, sample, request); + + if (!canDoAction) { + throw new ForbiddenException("Unauthorized to this sample"); } return sample; } - private async checkPermissionsForSampleCreate( + private checkPermissionsForSampleCreate( request: Request, sample: CreateSampleDto, group: Action, ) { - if (!sample) { - throw new BadRequestException("Not able to create this sample"); - } - const canDoAction = await this.permissionChecker(group, sample, request); + const canDoAction = this.permissionChecker(group, sample, request); + if (!canDoAction) { throw new ForbiddenException("Unauthorized to create this sample"); } + return sample; } @@ -271,11 +282,12 @@ export class SamplesController { @Req() request: Request, @Body() createSampleDto: CreateSampleDto, ): Promise { - const sampleDTO = await this.checkPermissionsForSampleCreate( + const sampleDTO = this.checkPermissionsForSampleCreate( request, createSampleDto, Action.SampleCreate, ); + return this.samplesService.create(sampleDTO); } @@ -325,22 +337,11 @@ export class SamplesController { "It returns a list of samples matching the query provided.
This endpoint still needs some work on the query specification.", }) @ApiQuery({ - name: "fields", - description: - "Full query filters to apply when retrieve samples\n" + - samplesFullQueryDescriptionFields, + name: "filters", + description: "Defines query limits and fields", required: false, - type: String, - example: samplesFullQueryExampleFields, - }) - @ApiQuery({ - name: "limits", - description: - "Define further query parameters like skip, limit, order\n" + - fullQueryDescriptionLimits, - required: false, - type: String, - example: fullQueryExampleLimits, + type: FullQueryFilters, + example: `{"limits": ${fullQueryExampleLimits}, fields: ${samplesFullQueryExampleFields}}`, }) @ApiResponse({ status: HttpStatus.OK, @@ -409,7 +410,7 @@ export class SamplesController { required: false, type: String, // NOTE: This is custom example because the service function metadataKeys expects input like the following. - // eslint-disable-next-line @typescript-eslint/quotes + // eslint-disable-next-line @/quotes example: '{ "fields": { "metadataKey": "chemical_formula" } }', }) @ApiResponse({ @@ -551,6 +552,7 @@ export class SamplesController { id, Action.SampleRead, ); + return sample; } @@ -560,11 +562,6 @@ export class SamplesController { ability.can(Action.SampleRead, SampleClass), ) @Get("/:id/authorization") - @ApiOperation({ - summary: "Check user access to a specific sample.", - description: - "Returns a boolean indicating whether the user has access to the sample with the specified ID.", - }) @ApiParam({ name: "id", description: "ID of the sample to check access for", @@ -572,19 +569,16 @@ export class SamplesController { }) @ApiResponse({ status: HttpStatus.OK, - type: Boolean, + type: FindByIdAccessResponse, description: - "Returns true if the user has access to the specified sample, otherwise false.", + "Returns canAccess property with boolean true if the user has access to the specified sample, otherwise false.", }) - async findByIdAccess( - @Req() request: Request, - @Param("id") id: string, - ): Promise<{ canAccess: boolean }> { + async findByIdAccess(@Req() request: Request, @Param("id") id: string) { const sample = await this.samplesService.findOne({ sampleId: id, }); - if (!sample) return { canAccess: false }; - const canAccess = await this.permissionChecker( + + const canAccess = this.permissionChecker( Action.SampleRead, sample, request, diff --git a/src/users/schemas/user-identity.schema.ts b/src/users/schemas/user-identity.schema.ts index 31825bb91..aadcb2146 100644 --- a/src/users/schemas/user-identity.schema.ts +++ b/src/users/schemas/user-identity.schema.ts @@ -2,6 +2,7 @@ import * as mongoose from "mongoose"; import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; import { Document } from "mongoose"; import { UserProfile } from "./user-profile.schema"; +import { ApiProperty } from "@nestjs/swagger"; export type UserIdentityDocument = UserIdentity & Document; @@ -13,27 +14,35 @@ export type UserIdentityDocument = UserIdentity & Document; timestamps: { createdAt: "created", updatedAt: "modified" }, }) export class UserIdentity { + @ApiProperty() @Prop() authStrategy: string; + @ApiProperty({ type: Date }) @Prop({ type: Date }) created: Date; + @ApiProperty({ type: Object }) @Prop({ type: Object }) credentials: Record; + @ApiProperty() @Prop() externalId: string; + @ApiProperty({ type: Date }) @Prop({ type: Date }) modified: Date; + @ApiProperty({ type: UserProfile }) @Prop({ type: UserProfile }) profile: UserProfile; + @ApiProperty() @Prop() provider: string; + @ApiProperty() @Prop({ type: mongoose.Schema.Types.ObjectId, ref: "User" }) userId: string; } diff --git a/src/users/schemas/user-profile.schema.ts b/src/users/schemas/user-profile.schema.ts index 4b3f3da5e..b522295e1 100644 --- a/src/users/schemas/user-profile.schema.ts +++ b/src/users/schemas/user-profile.schema.ts @@ -1,31 +1,40 @@ import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { Document } from "mongoose"; export type UserProfileDocument = UserProfile & Document; @Schema() export class UserProfile { + @ApiPropertyOptional() @Prop() displayName?: string; + @ApiProperty() @Prop() email: string; + @ApiProperty() @Prop() username: string; + @ApiPropertyOptional() @Prop() thumbnailPhoto?: string; + @ApiPropertyOptional() @Prop() id?: string; + @ApiPropertyOptional({ type: [Object] }) @Prop({ type: [Object] }) emails?: Record[]; + @ApiProperty({ type: [String] }) @Prop({ type: [String] }) accessGroups: string[]; + @ApiPropertyOptional({ type: [Object] }) @Prop({ type: Object }) oidcClaims?: Record; } diff --git a/src/users/schemas/user-settings.schema.ts b/src/users/schemas/user-settings.schema.ts index e5d524936..6830da5f8 100644 --- a/src/users/schemas/user-settings.schema.ts +++ b/src/users/schemas/user-settings.schema.ts @@ -21,6 +21,11 @@ export interface ScientificCondition { export class UserSettings { _id: string; + @ApiProperty({ + type: String, + required: false, + description: "Globally unique identifier of a user setting", + }) id?: string; @ApiProperty({ diff --git a/src/users/user-identities.controller.ts b/src/users/user-identities.controller.ts index 9a4105710..bf4492457 100644 --- a/src/users/user-identities.controller.ts +++ b/src/users/user-identities.controller.ts @@ -3,6 +3,7 @@ import { ForbiddenException, Get, Headers, + HttpStatus, NotFoundException, Query, Req, @@ -47,12 +48,6 @@ export class UserIdentitiesController { ability.can(Action.UserReadAny, User), ) @Get("/findOne") - @ApiOperation({ - summary: - "It returns the user identity profile of the first user matching the query", - description: - "This endpoint returns the user identity profile of the first user matching teh condition", - }) @ApiQuery({ name: "filter", description: @@ -63,10 +58,11 @@ export class UserIdentitiesController { example: filterUserIdentityExample, }) @ApiResponse({ - status: 201, - type: boolean, + status: HttpStatus.OK, + type: UserIdentity, + isArray: false, description: - "Results is true if a registered user exists that have the emailed provided listed as main email", + "Results with UserIdentity object if a registered user exists that have the email provided listed as main email", }) async findOne( // NOTE: This now supports both headers filter and query filter. @@ -75,7 +71,7 @@ export class UserIdentitiesController { @Headers() headers: Record, @Req() request: Request, @Query("filter") queryFilters?: string, - ): Promise { + ) { const parsedQueryFilters = JSON.parse(queryFilters ?? "{}"); let filter = {}; if (headers.filter) { diff --git a/src/users/users.controller.spec.ts b/src/users/users.controller.spec.ts index 5c111e716..a1b58ea49 100644 --- a/src/users/users.controller.spec.ts +++ b/src/users/users.controller.spec.ts @@ -12,7 +12,7 @@ class UsersServiceMock { return { id }; } - async findByIdUserSettings(userId: string) { + async findByIdUserSettings() { return mockUserSettings; } diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index 0747b450c..0b63b8367 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -106,7 +106,7 @@ export class UsersController { description: "This endpoint is deprecated and will be removed soon. Use /auth/login instead", }) - async login(@Req() req: Record): Promise { + async login(): Promise { return null; } diff --git a/test/DerivedDatasetOrigDatablock.js b/test/DerivedDatasetOrigDatablock.js index 8dcc066e3..27fef4c29 100644 --- a/test/DerivedDatasetOrigDatablock.js +++ b/test/DerivedDatasetOrigDatablock.js @@ -15,7 +15,7 @@ describe("0800: DerivedDatasetOrigDatablock: Test OrigDatablocks and their relat db.collection("Dataset").deleteMany({}); db.collection("OrigDatablock").deleteMany({}); }); - beforeEach(async() => { + beforeEach(async () => { accessTokenAdminIngestor = await utils.getToken(appUrl, { username: "adminIngestor", password: TestData.Accounts["adminIngestor"]["password"], @@ -384,20 +384,20 @@ describe("0800: DerivedDatasetOrigDatablock: Test OrigDatablocks and their relat }); }); - it("0200: add a new origDatablock with invalid pid should fail", async () => { + it("0200: add a new origDatablock to the non-existent dataset should fail", async () => { return request(appUrl) .post(`/api/v3/origdatablocks`) .send({ ...TestData.OrigDataBlockCorrect1, datasetId: "wrong" }) .set("Accept", "application/json") .set({ Authorization: `Bearer ${accessTokenAdminIngestor}` }) - .expect(TestData.BadRequestStatusCode) + .expect(TestData.NotFoundStatusCode) .expect("Content-Type", /json/) .then((res) => { res.body.should.have.property("error"); }); }); - it("0210: add a new origDatablock with valid pid should success", async () => { + it("0210: add a new origDatablock to the existent dataset should success", async () => { return request(appUrl) .post(`/api/v3/origdatablocks`) .send({ diff --git a/test/OrigDatablockForRawDataset.js b/test/OrigDatablockForRawDataset.js index d329d5867..ca59bc854 100644 --- a/test/OrigDatablockForRawDataset.js +++ b/test/OrigDatablockForRawDataset.js @@ -745,13 +745,13 @@ describe("1200: OrigDatablockForRawDataset: Test OrigDatablocks and their relati }); }); - it("0400: add a new origDatablock with invalid pid should fail", async () => { + it("0400: add a new origDatablock to the non-existent dataset should fail", async () => { return request(appUrl) .post(`/api/v3/origdatablocks`) .send({ ...origDatablockData1, datasetId: "wrong" }) .set("Accept", "application/json") .set({ Authorization: `Bearer ${accessTokenAdminIngestor}` }) - .expect(TestData.BadRequestStatusCode) + .expect(TestData.NotFoundStatusCode) .expect("Content-Type", /json/) .then((res) => { res.body.should.have.property("error"); diff --git a/test/Policy.js b/test/Policy.js index 7339542fb..a665245c4 100644 --- a/test/Policy.js +++ b/test/Policy.js @@ -25,7 +25,7 @@ describe("1300: Policy: Simple Policy tests", () => { before(() => { db.collection("Policy").deleteMany({}); }); - beforeEach(async() => { + beforeEach(async () => { accessTokenAdminIngestor = await utils.getToken(appUrl, { username: "adminIngestor", password: TestData.Accounts["adminIngestor"]["password"], @@ -36,7 +36,7 @@ describe("1300: Policy: Simple Policy tests", () => { password: TestData.Accounts["archiveManager"]["password"], }); }); - + it("0010: adds a new policy", async () => { return request(appUrl) .post("/api/v3/Policies") diff --git a/test/Proposal.js b/test/Proposal.js index cb0ae6661..e74155869 100644 --- a/test/Proposal.js +++ b/test/Proposal.js @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-var-requires */ "use strict"; - +const { faker } = require("@faker-js/faker"); const utils = require("./LoginUtils"); const { TestData } = require("./TestData"); @@ -8,14 +8,16 @@ let accessTokenProposalIngestor = null, accessTokenAdminIngestor = null, accessTokenArchiveManager = null, defaultProposalId = null, + minimalProposalId = null, proposalId = null, + proposalWithParentId = null, attachmentId = null; describe("1500: Proposal: Simple Proposal", () => { before(() => { db.collection("Proposal").deleteMany({}); }); - beforeEach(async() => { + beforeEach(async () => { accessTokenProposalIngestor = await utils.getToken(appUrl, { username: "proposalIngestor", password: TestData.Accounts["proposalIngestor"]["password"], @@ -89,7 +91,7 @@ describe("1500: Proposal: Simple Proposal", () => { res.body.should.have.property("ownerGroup").and.be.string; res.body.should.have.property("proposalId").and.be.string; defaultProposalId = res.body["proposalId"]; - proposalId = encodeURIComponent(res.body["proposalId"]); + minimalProposalId = encodeURIComponent(res.body["proposalId"]); }); }); @@ -169,6 +171,8 @@ describe("1500: Proposal: Simple Proposal", () => { .then((res) => { res.body.should.have.property("createdBy").and.be.string; res.body.should.have.property("updatedBy").and.be.string; + res.body.should.have.property("type").and.be.string; + res.body.type.should.be.equal("Default Proposal"); }); }); @@ -216,7 +220,108 @@ describe("1500: Proposal: Simple Proposal", () => { }); }); - it("0120: should delete this proposal attachment", async () => { + it("0115: adds a new proposal with parent proposal", async () => { + const proposalWithParentProposal = { + ...TestData.ProposalCorrectComplete, + proposalId: faker.string.numeric(8), + parentProposalId: proposalId, + }; + + return request(appUrl) + .post("/api/v3/Proposals") + .send(proposalWithParentProposal) + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenProposalIngestor}` }) + .expect(TestData.EntryCreatedStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.have.property("ownerGroup").and.be.string; + res.body.should.have.property("proposalId").and.be.string; + proposalWithParentId = res.body.proposalId; + res.body.should.have.property("parentProposalId").and.be.string; + res.body.parentProposalId.should.be.equal(proposalId); + }); + }); + + it("0116: adds a new proposal with a type different than default", async () => { + const proposalWithType = { + ...TestData.ProposalCorrectComplete, + proposalId: faker.string.numeric(8), + type: "DOOR Proposal", + }; + + return request(appUrl) + .post("/api/v3/Proposals") + .send(proposalWithType) + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenProposalIngestor}` }) + .expect(TestData.EntryCreatedStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.have.property("ownerGroup").and.be.string; + res.body.should.have.property("proposalId").and.be.string; + res.body.type.should.be.equal(proposalWithType.type); + }); + }); + + it("0117: adds a new proposal with metadata", async () => { + const proposalWithMetadata = { + ...TestData.ProposalCorrectComplete, + proposalId: faker.string.numeric(8), + metadata: TestData.RawCorrectRandom.scientificMetadata, + }; + + return request(appUrl) + .post("/api/v3/Proposals") + .send(proposalWithMetadata) + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenProposalIngestor}` }) + .expect(TestData.EntryCreatedStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.have.property("ownerGroup").and.be.string; + res.body.should.have.property("proposalId").and.be.string; + res.body.should.have.property("metadata").and.be.an("object"); + JSON.stringify(res.body.metadata).should.be.equal( + JSON.stringify(proposalWithMetadata.metadata), + ); + }); + }); + + it("0117: cannot add a new proposal with a type different than predefined proposal types", async () => { + const proposalType = "Incorrect type"; + const proposalWithIncorrectType = { + ...TestData.ProposalCorrectComplete, + proposalId: faker.string.numeric(8), + type: proposalType, + }; + + return request(appUrl) + .post("/api/v3/Proposals") + .send(proposalWithIncorrectType) + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenProposalIngestor}` }) + .expect(TestData.BadRequestStatusCode) + .expect("Content-Type", /json/); + }); + + it("0120: updates a proposal with a new parent proposal", async () => { + return request(appUrl) + .patch("/api/v3/Proposals/" + proposalWithParentId) + .send({ parentProposalId: minimalProposalId }) + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenAdminIngestor}` }) + .expect(TestData.SuccessfulPatchStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.have.property("ownerGroup").and.be.string; + res.body.should.have.property("proposalId").and.be.string; + res.body.should.have.property("parentProposalId").and.be.string; + res.body.parentProposalId.should.be.equal(minimalProposalId); + }); + }); + + it("0130: should delete this proposal attachment", async () => { return request(appUrl) .delete( "/api/v3/Proposals/" + proposalId + "/attachments/" + attachmentId, @@ -226,7 +331,7 @@ describe("1500: Proposal: Simple Proposal", () => { .expect(TestData.SuccessfulDeleteStatusCode); }); - it("0130: admin can remove all existing proposals", async () => { + it("0140: admin can remove all existing proposals", async () => { return await request(appUrl) .get("/api/v3/Proposals") .set("Accept", "application/json") diff --git a/test/ProposalAuthorization.js b/test/ProposalAuthorization.js index 8294895f3..b0bdd91d8 100644 --- a/test/ProposalAuthorization.js +++ b/test/ProposalAuthorization.js @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-var-requires */ "use strict"; +const { faker } = require("@faker-js/faker"); const utils = require("./LoginUtils"); const { TestData } = require("./TestData"); const sandbox = require("sinon").createSandbox(); @@ -22,21 +23,21 @@ let proposalPid1 = null, const proposal1 = { ...TestData.ProposalCorrectMin, - proposalId: "20170268", + proposalId: faker.string.numeric(8), ownerGroup: "group4", accessGroups: ["group5"], }; const proposal2 = { ...TestData.ProposalCorrectComplete, - proposalId: "20170269", + proposalId: faker.string.numeric(8), ownerGroup: "group1", accessGroups: ["group3"], }; const proposal3 = { ...TestData.ProposalCorrectMin, - proposalId: "20170270", + proposalId: faker.string.numeric(8), ownerGroup: "group2", accessGroups: ["group3"], }; @@ -53,7 +54,7 @@ describe("1400: ProposalAuthorization: Test access to proposal", () => { before(() => { db.collection("Proposal").deleteMany({}); }); - beforeEach(async() => { + beforeEach(async () => { accessTokenAdminIngestor = await utils.getToken(appUrl, { username: "adminIngestor", password: TestData.Accounts["adminIngestor"]["password"], diff --git a/test/RawDataset.js b/test/RawDataset.js index 60563c898..b736c28b7 100644 --- a/test/RawDataset.js +++ b/test/RawDataset.js @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-var-requires */ "use strict"; +const { faker } = require("@faker-js/faker"); var utils = require("./LoginUtils"); const { TestData } = require("./TestData"); @@ -18,7 +19,7 @@ describe("1900: RawDataset: Raw Datasets", () => { db.collection("Dataset").deleteMany({}); db.collection("Proposals").deleteMany({}); }); - beforeEach(async() => { + beforeEach(async () => { accessProposalToken = await utils.getToken(appUrl, { username: "proposalIngestor", password: TestData.Accounts["proposalIngestor"]["password"], @@ -46,6 +47,8 @@ describe("1900: RawDataset: Raw Datasets", () => { .then((res) => { res.body.should.have.property("ownerGroup").and.be.string; res.body.should.have.property("proposalId").and.be.string; + // NOTE: Add real proposal in the testdata instead of fixed (non-existing) one + TestData.RawCorrect.proposalId = res.body["proposalId"]; proposalId = encodeURIComponent(res.body["proposalId"]); }); }); @@ -280,6 +283,26 @@ describe("1900: RawDataset: Raw Datasets", () => { ); }); + it("0124: adds a new proposal for pattching in the existing dataset", async () => { + const newProposal = { + ...TestData.ProposalCorrectComplete, + proposalId: faker.string.numeric(8), + }; + return request(appUrl) + .post("/api/v3/Proposals") + .send(newProposal) + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessProposalToken}` }) + .expect(TestData.EntryCreatedStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.have.property("ownerGroup").and.be.string; + res.body.should.have.property("proposalId").and.be.string; + // NOTE: Add real proposal in the testdata instead of fixed (non-existing) one + TestData.PatchProposal1.proposalId = res.body["proposalId"]; + }); + }); + it("0125: should update proposal of the dataset", async () => { return request(appUrl) .patch("/api/v3/datasets/" + pid) diff --git a/test/RawDatasetOrigDatablock.js b/test/RawDatasetOrigDatablock.js index 553d5bbb0..f86bd5e03 100644 --- a/test/RawDatasetOrigDatablock.js +++ b/test/RawDatasetOrigDatablock.js @@ -18,7 +18,7 @@ describe("2000: RawDatasetOrigDatablock: Test OrigDatablocks and their relation db.collection("Dataset").deleteMany({}); db.collection("OrigDatablock").deleteMany({}); }); - beforeEach(async() => { + beforeEach(async () => { accessTokenAdminIngestor = await utils.getToken(appUrl, { username: "adminIngestor", password: TestData.Accounts["adminIngestor"]["password"], @@ -448,13 +448,13 @@ describe("2000: RawDatasetOrigDatablock: Test OrigDatablocks and their relation }); }); - it("0240: add a new origDatablock with invalid pid should fail", async () => { + it("0240: add a new origDatablock to the non-existent dataset should fail", async () => { return request(appUrl) .post(`/api/v3/origdatablocks`) .send({ ...origDatablockData1, datasetId: "wrong" }) .set("Accept", "application/json") .set({ Authorization: `Bearer ${accessTokenAdminIngestor}` }) - .expect(TestData.BadRequestStatusCode) + .expect(TestData.NotFoundStatusCode) .expect("Content-Type", /json/) .then((res) => { res.body.should.have.property("error"); diff --git a/test/TestData.js b/test/TestData.js index a10e2feeb..09f1541d1 100644 --- a/test/TestData.js +++ b/test/TestData.js @@ -24,6 +24,7 @@ const TestData = { BadRequestStatusCode: 400, AccessForbiddenStatusCode: 403, UnauthorizedStatusCode: 401, + NotFoundStatusCode: 404, CreationUnauthorizedStatusCode: 401, ConflictStatusCode: 409, ApplicationErrorStatusCode: 500, @@ -75,7 +76,7 @@ const TestData = { }, ProposalCorrectComplete: { - proposalId: "20170267", + proposalId: faker.string.numeric(8), pi_email: "pi@uni.edu", pi_firstname: "principal", pi_lastname: "investigator", @@ -102,7 +103,7 @@ const TestData = { }, ProposalWrong_1: { - proposalId: "20170267", + proposalId: faker.string.numeric(8), pi_email: "pi@uni.edu", pi_firstname: "principal", pi_lastname: "investigator", @@ -144,11 +145,11 @@ const TestData = { RawCorrectMin: { ownerGroup: faker.company.name(), creationLocation: faker.location.city(), - principalInvestigator: faker.internet.userName(), + principalInvestigator: faker.internet.username(), type: "raw", creationTime: faker.date.past(), sourceFolder: faker.system.directoryPath(), - owner: faker.internet.userName(), + owner: faker.internet.username(), contactEmail: faker.internet.email(), datasetName: faker.string.sample(), }, @@ -231,7 +232,7 @@ const TestData = { isPublished: false, ownerGroup: "p13388", accessGroups: [], - proposalId: "10.540.16635/20110123", + proposalId: "", runNumber: "123456", instrumentId: "1f016ec4-7a73-11ef-ae3e-439013069377", sampleId: "20c32b4e-7a73-11ef-9aec-5b9688aa3791i", @@ -422,7 +423,7 @@ const TestData = { investigator: faker.internet.email(), inputDatasets: [faker.string.uuid()], usedSoftware: [faker.internet.url()], - owner: faker.internet.userName(), + owner: faker.internet.username(), contactEmail: faker.internet.email(), sourceFolder: faker.system.directoryPath(), creationTime: faker.date.past(), @@ -838,7 +839,7 @@ const TestData = { }, PatchProposal1: { - proposalId: "10.540.16635/20240124", + proposalId: "", }, PatchInstrument1: { @@ -867,12 +868,12 @@ const TestData = { ScientificMetadataForElasticSearch: { ownerGroup: faker.company.name(), creationLocation: faker.location.city(), - principalInvestigator: faker.internet.userName(), + principalInvestigator: faker.internet.username(), type: "raw", datasetName: faker.string.sample(), creationTime: faker.date.past(), sourceFolder: faker.system.directoryPath(), - owner: faker.internet.userName(), + owner: faker.internet.username(), size: faker.number.int({ min: 0, max: 100000000 }), proposalId: faker.string.numeric(6), contactEmail: faker.internet.email(), diff --git a/test/config/pretest.js b/test/config/pretest.js index f935d2239..1af2d0ef3 100644 --- a/test/config/pretest.js +++ b/test/config/pretest.js @@ -1,15 +1,18 @@ /* eslint-disable @typescript-eslint/no-var-requires */ //NOTE: Here we load and initialize some global variables that are used throughout the tests require("dotenv").config(); -var chaiHttp = require("chai-http"); +var chaiHttp; +var chai; const { MongoClient } = require("mongodb"); const client = new MongoClient(process.env.MONGODB_URI); async function loadChai() { - const { chai } = import("chai"); - chai.use(chaiHttp); + chaiHttp = await import("chai-http"); + await import("chai").then((result) => { + chai = result.use(chaiHttp); + }); await client.connect(); }