From 5b68ad92e7e7468f5897b24d6a40f620cbc15a44 Mon Sep 17 00:00:00 2001 From: Samuel Lim Date: Thu, 26 Sep 2024 15:14:28 +0800 Subject: [PATCH 01/28] Initialize Match Service --- .env.sample | 6 + compose.dev.yml | 12 +- compose.yml | 32 + services/match/.env.sample | 7 + services/match/.gitignore | 24 + services/match/.prettierignore | 39 + services/match/.prettierrc.json | 11 + services/match/Dockerfile | 9 + services/match/README.md | 0 services/match/eslint.config.mjs | 27 + services/match/package-lock.json | 4485 +++++++++++++++++++ services/match/package.json | 43 + services/match/src/app.ts | 25 + services/match/src/controllers/index.ts | 7 + services/match/src/index.ts | 12 + services/match/src/models/index.ts | 19 + services/match/src/routes/index.ts | 7 + services/match/src/types/custom.ts | 8 + services/match/src/types/express/index.d.ts | 13 + services/match/src/utils/helpers.ts | 64 + services/match/tsconfig.json | 14 + services/user/src/types/custom.ts | 2 +- 22 files changed, 4864 insertions(+), 2 deletions(-) create mode 100644 services/match/.env.sample create mode 100644 services/match/.gitignore create mode 100644 services/match/.prettierignore create mode 100644 services/match/.prettierrc.json create mode 100644 services/match/Dockerfile create mode 100644 services/match/README.md create mode 100644 services/match/eslint.config.mjs create mode 100644 services/match/package-lock.json create mode 100644 services/match/package.json create mode 100644 services/match/src/app.ts create mode 100644 services/match/src/controllers/index.ts create mode 100644 services/match/src/index.ts create mode 100644 services/match/src/models/index.ts create mode 100644 services/match/src/routes/index.ts create mode 100644 services/match/src/types/custom.ts create mode 100644 services/match/src/types/express/index.d.ts create mode 100644 services/match/src/utils/helpers.ts create mode 100644 services/match/tsconfig.json diff --git a/.env.sample b/.env.sample index fbd0149134..86160f9a44 100644 --- a/.env.sample +++ b/.env.sample @@ -13,6 +13,12 @@ USER_DB_LOCAL_URI=mongodb://user-db:27017/user USER_DB_USERNAME=user USER_DB_PASSWORD=password +# Match Service +MATCH_DB_CLOUD_URI= +MATCH_DB_LOCAL_URI=mongodb://match-db:27017/match +MATCH_DB_USERNAME=user +MATCH_DB_PASSWORD=password + # Secret for creating JWT signature JWT_SECRET=you-can-replace-this-with-your-own-secret diff --git a/compose.dev.yml b/compose.dev.yml index e443e28dad..d877f67429 100644 --- a/compose.dev.yml +++ b/compose.dev.yml @@ -26,4 +26,14 @@ services: user-db: ports: - - 27018:27017 \ No newline at end of file + - 27018:27017 + + match: + command: npm run dev + volumes: + - /app/node_modules + - ./services/match:/app + + match-db: + ports: + - 27019:27017 diff --git a/compose.yml b/compose.yml index d164d901bc..ddc7c1f4a1 100644 --- a/compose.yml +++ b/compose.yml @@ -78,10 +78,40 @@ services: - user-db-network command: --quiet restart: always + + match: + container_name: match + image: match + build: + context: services/match + dockerfile: Dockerfile + environment: + DB_CLOUD_URI: ${MATCH_DB_CLOUD_URI} + DB_LOCAL_URI: ${MATCH_DB_LOCAL_URI} + DB_USERNAME: ${MATCH_DB_USERNAME} + DB_PASSWORD: ${MATCH_DB_PASSWORD} + ports: + - 8083:8083 + networks: + - match-db-network + restart: always + + match-db: + container_name: match-db + image: mongo:7.0.14 + environment: + MONGO_INITDB_ROOT_USERNAME: ${MATCH_DB_USERNAME} + MONGO_INITDB_ROOT_PASSWORD: ${MATCH_DB_PASSWORD} + volumes: + - match-db:/data/db + networks: + - match-db-network + restart: always volumes: question-db: user-db: + match-db: networks: gateway-network: @@ -90,3 +120,5 @@ networks: driver: bridge user-db-network: driver: bridge + match-db-network: + driver: bridge diff --git a/services/match/.env.sample b/services/match/.env.sample new file mode 100644 index 0000000000..86bc9196be --- /dev/null +++ b/services/match/.env.sample @@ -0,0 +1,7 @@ +# This is a sample environment configuration file. +# Copy this file to .env and replace the placeholder values with your own. +DB_CLOUD_URI= +DB_LOCAL_URI=mongodb://match-db:27017/match +DB_USERNAME=user +DB_PASSWORD=password +PORT=8083 \ No newline at end of file diff --git a/services/match/.gitignore b/services/match/.gitignore new file mode 100644 index 0000000000..931232e706 --- /dev/null +++ b/services/match/.gitignore @@ -0,0 +1,24 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build +/dist + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* \ No newline at end of file diff --git a/services/match/.prettierignore b/services/match/.prettierignore new file mode 100644 index 0000000000..420f9273b6 --- /dev/null +++ b/services/match/.prettierignore @@ -0,0 +1,39 @@ +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db + +package.json +package-lock.json diff --git a/services/match/.prettierrc.json b/services/match/.prettierrc.json new file mode 100644 index 0000000000..58416554c2 --- /dev/null +++ b/services/match/.prettierrc.json @@ -0,0 +1,11 @@ +{ + "tabWidth": 4, + "useTabs": false, + "singleQuote": true, + "semi": true, + "bracketSpacing": true, + "arrowParens": "avoid", + "trailingComma": "all", + "bracketSameLine": true, + "printWidth": 120 +} diff --git a/services/match/Dockerfile b/services/match/Dockerfile new file mode 100644 index 0000000000..88c320a448 --- /dev/null +++ b/services/match/Dockerfile @@ -0,0 +1,9 @@ +FROM node:20-alpine + +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm install +COPY . . +EXPOSE 8083 + +CMD ["npm", "start"] \ No newline at end of file diff --git a/services/match/README.md b/services/match/README.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/services/match/eslint.config.mjs b/services/match/eslint.config.mjs new file mode 100644 index 0000000000..8d28bc7edd --- /dev/null +++ b/services/match/eslint.config.mjs @@ -0,0 +1,27 @@ +// @ts-check + +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; + +export default tseslint.config({ + files: ['**/*.ts'], + extends: [ + eslint.configs.recommended, + ...tseslint.configs.strict, + ...tseslint.configs.stylistic, + eslintPluginPrettierRecommended, + ], + rules: { + '@typescript-eslint/no-explicit-any': 'off', + "@typescript-eslint/no-extraneous-class": "off", + + // https://stackoverflow.com/questions/68816664/get-rid-of-error-delete-eslint-prettier-prettier-and-allow-use-double + 'prettier/prettier': [ + 'error', + { + 'endOfLine': 'auto', + } + ] + }, +}); diff --git a/services/match/package-lock.json b/services/match/package-lock.json new file mode 100644 index 0000000000..c9686f85f7 --- /dev/null +++ b/services/match/package-lock.json @@ -0,0 +1,4485 @@ +{ + "name": "match", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "match", + "version": "0.0.0", + "license": "ISC", + "dependencies": { + "axios": "^1.7.7", + "body-parser": "^1.20.3", + "cors": "^2.8.5", + "express": "^4.21.0", + "mongoose": "^8.7.0", + "morgan": "^1.10.0" + }, + "devDependencies": { + "@eslint/js": "^9.10.0", + "@types/axios": "^0.9.36", + "@types/cors": "^2.8.17", + "@types/eslint__js": "^8.42.3", + "@types/express": "^4.17.21", + "@types/mongoose": "^5.11.96", + "@types/morgan": "^1.9.9", + "@types/node": "^22.5.4", + "@typescript-eslint/eslint-plugin": "^8.5.0", + "eslint": "^9.10.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "nodemon": "^3.1.4", + "prettier": "3.3.3", + "prettier-eslint": "^16.3.0", + "ts-node": "^10.9.2", + "typescript": "^5.5.4", + "typescript-eslint": "^8.5.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "dev": true, + "license": "Apache-2.0", + "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/config-array/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/js": { + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.10.0.tgz", + "integrity": "sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.1.0.tgz", + "integrity": "sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.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==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", + "integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/axios": { + "version": "0.9.36", + "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.9.36.tgz", + "integrity": "sha512-NLOpedx9o+rxo/X5ChbdiX6mS1atE4WHmEEIcR9NLenRVa5HoVjAvjafwU3FPTqnZEstpoqCaW7fagqSoTDNeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint__js": { + "version": "8.42.3", + "resolved": "https://registry.npmjs.org/@types/eslint__js/-/eslint__js-8.42.3.tgz", + "integrity": "sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", + "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mongoose": { + "version": "5.11.96", + "resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-5.11.96.tgz", + "integrity": "sha512-keiY22ljJtXyM7osgScmZOHV6eL5VFUD5tQumlu+hjS++HND5nM8jNEdj5CSWfKIJpVwQfPuwQ2SfBqUnCAVRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mongoose": "*" + } + }, + "node_modules/@types/morgan": { + "version": "1.9.9", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.9.tgz", + "integrity": "sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.5.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz", + "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/qs": { + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "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, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.5.0.tgz", + "integrity": "sha512-lHS5hvz33iUFQKuPFGheAB84LwcJ60G8vKnEhnfcK1l8kGVLro2SFYW6K0/tj8FUhRJ0VHyg1oAfg50QGbPPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.5.0", + "@typescript-eslint/type-utils": "8.5.0", + "@typescript-eslint/utils": "8.5.0", + "@typescript-eslint/visitor-keys": "8.5.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.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.5.0.tgz", + "integrity": "sha512-gF77eNv0Xz2UJg/NbpWJ0kqAm35UMsvZf1GHj8D9MRFTj/V3tAciIWXfmPLsAAF/vUlpWPvUDyH1jjsr0cMVWw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "8.5.0", + "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/typescript-estree": "8.5.0", + "@typescript-eslint/visitor-keys": "8.5.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.5.0.tgz", + "integrity": "sha512-06JOQ9Qgj33yvBEx6tpC8ecP9o860rsR22hWMEd12WcTRrfaFgHr2RB/CA/B+7BMhHkXT4chg2MyboGdFGawYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/visitor-keys": "8.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.5.0.tgz", + "integrity": "sha512-N1K8Ix+lUM+cIDhL2uekVn/ZD7TZW+9/rwz8DclQpcQ9rk4sIL5CAlBC0CugWKREmDjBzI/kQqU4wkg46jWLYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.5.0", + "@typescript-eslint/utils": "8.5.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/types": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.5.0.tgz", + "integrity": "sha512-qjkormnQS5wF9pjSi6q60bKUHH44j2APxfh9TQRXK8wbYVeDYYdYJGIROL87LGZZ2gz3Rbmjc736qyL8deVtdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.5.0.tgz", + "integrity": "sha512-vEG2Sf9P8BPQ+d0pxdfndw3xIXaoSjliG0/Ejk7UggByZPKXmJmw3GW5jV2gHNQNawBUyfahoSiCFVov0Ruf7Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/visitor-keys": "8.5.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.5.0.tgz", + "integrity": "sha512-6yyGYVL0e+VzGYp60wvkBHiqDWOpT63pdMV2CVG4LVDd5uR6q1qQN/7LafBZtAtNIn/mqXjsSeS5ggv/P0iECw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.5.0", + "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/typescript-estree": "8.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.5.0.tgz", + "integrity": "sha512-yTPqMnbAZJNy2Xq2XU8AdtOW9tJIr+UQb64aXB9f3B1498Zx9JorVgFJcZpEc9UBuCCrdzKID2RGAMkYcDtZOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.5.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "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": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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, + "license": "ISC" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bson": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.8.0.tgz", + "integrity": "sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "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, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.10.0.tgz", + "integrity": "sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.10.0", + "@eslint/plugin-kit": "^0.1.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.0.2", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "json-stable-stringify-without-jsonify": "^1.0.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" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.9.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", + "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.10", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "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, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "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==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "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, + "license": "MIT", + "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", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "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" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "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==", + "dev": true, + "license": "MIT", + "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" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/loglevel-colored-level-prefix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/loglevel-colored-level-prefix/-/loglevel-colored-level-prefix-1.0.0.tgz", + "integrity": "sha512-u45Wcxxc+SdAlh4yeF/uKlC1SPUPCy0gullSNKXod5I4bmifzk+Q4lSLExNEVn19tGaJipbZ4V4jbFn79/6mVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^1.1.3", + "loglevel": "^1.4.1" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mongodb": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.9.0.tgz", + "integrity": "sha512-UMopBVx1LmEUbW/QE0Hw18u583PEDVQmUmVzzBRH0o/xtE9DBRA5ZYLOjpLIa03i8FXjzvQECJcqoMvCXftTUA==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.5", + "bson": "^6.7.0", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", + "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.7.0.tgz", + "integrity": "sha512-rUCSF1mMYQXjXYdqEQLLlMD3xbcj2j1/hRn+9VnVj7ipzru/UoUZxlj/hWmteKMAh4EFnDZ+BIrmma9l/0Hi1g==", + "license": "MIT", + "dependencies": { + "bson": "^6.7.0", + "kareem": "2.6.3", + "mongodb": "6.9.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/mquery/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mquery/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.4.tgz", + "integrity": "sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-eslint": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/prettier-eslint/-/prettier-eslint-16.3.0.tgz", + "integrity": "sha512-Lh102TIFCr11PJKUMQ2kwNmxGhTsv/KzUg9QYF2Gkw259g/kPgndZDWavk7/ycbRvj2oz4BPZ1gCU8bhfZH/Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/parser": "^6.7.5", + "common-tags": "^1.4.0", + "dlv": "^1.1.0", + "eslint": "^8.7.0", + "indent-string": "^4.0.0", + "lodash.merge": "^4.6.0", + "loglevel-colored-level-prefix": "^1.0.0", + "prettier": "^3.0.1", + "pretty-format": "^29.7.0", + "require-relative": "^0.8.7", + "typescript": "^5.2.2", + "vue-eslint-parser": "^9.1.0" + }, + "engines": { + "node": ">=16.10.0" + }, + "peerDependencies": { + "prettier-plugin-svelte": "^3.0.0", + "svelte-eslint-parser": "*" + }, + "peerDependenciesMeta": { + "prettier-plugin-svelte": { + "optional": true + }, + "svelte-eslint-parser": { + "optional": true + } + } + }, + "node_modules/prettier-eslint/node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/prettier-eslint/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/prettier-eslint/node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/prettier-eslint/node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/prettier-eslint/node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/prettier-eslint/node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/prettier-eslint/node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/prettier-eslint/node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/prettier-eslint/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/prettier-eslint/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/prettier-eslint/node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "license": "MIT", + "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", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "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", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "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" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/prettier-eslint/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==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/prettier-eslint/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/prettier-eslint/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==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/prettier-eslint/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/prettier-eslint/node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/prettier-eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/prettier-eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prettier-eslint/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-relative": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", + "integrity": "sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/synckit": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", + "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "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, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.5.0.tgz", + "integrity": "sha512-uD+XxEoSIvqtm4KE97etm32Tn5MfaZWgWfMMREStLxR6JzvHkc2Tkj7zhTEK5XmtpTmKHNnG8Sot6qDfhHtR1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.5.0", + "@typescript-eslint/parser": "8.5.0", + "@typescript-eslint/utils": "8.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vue-eslint-parser": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/vue-eslint-parser/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==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-eslint-parser/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==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-eslint-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "license": "MIT", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/services/match/package.json b/services/match/package.json new file mode 100644 index 0000000000..965e72e540 --- /dev/null +++ b/services/match/package.json @@ -0,0 +1,43 @@ +{ + "name": "match", + "version": "0.0.0", + "main": "index.js", + "scripts": { + "build": "npx tsc", + "start": "npm run build && node ./dist/index.js", + "dev": "nodemon --files ./src/index.ts", + "lint": "npx eslint .", + "lint:fix": "npx eslint . --fix" + }, + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "axios": "^1.7.7", + "body-parser": "^1.20.3", + "cors": "^2.8.5", + "express": "^4.21.0", + "mongoose": "^8.7.0", + "morgan": "^1.10.0" + }, + "devDependencies": { + "@eslint/js": "^9.10.0", + "@types/axios": "^0.9.36", + "@types/cors": "^2.8.17", + "@types/eslint__js": "^8.42.3", + "@types/express": "^4.17.21", + "@types/mongoose": "^5.11.96", + "@types/morgan": "^1.9.9", + "@types/node": "^22.5.4", + "@typescript-eslint/eslint-plugin": "^8.5.0", + "eslint": "^9.10.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "nodemon": "^3.1.4", + "prettier": "3.3.3", + "prettier-eslint": "^16.3.0", + "ts-node": "^10.9.2", + "typescript": "^5.5.4", + "typescript-eslint": "^8.5.0" + } +} diff --git a/services/match/src/app.ts b/services/match/src/app.ts new file mode 100644 index 0000000000..0af1e48732 --- /dev/null +++ b/services/match/src/app.ts @@ -0,0 +1,25 @@ +import express, { Express } from 'express'; +import morgan from 'morgan'; +import cors from 'cors'; +import router from './routes'; +import bodyParser from 'body-parser'; + +const app: Express = express(); + +// Middleware +app.use(morgan('dev')); +app.use(bodyParser.urlencoded({ extended: false })); +app.use(bodyParser.json()); + +app.use( + cors({ + origin: process.env.CORS_ORIGIN ?? true, + methods: ['GET', 'POST', 'DELETE', 'PUT', 'PATCH'], + allowedHeaders: ['Origin', 'X-Request-With', 'Content-Type', 'Accept', 'Authorization'], + }), +); + +// Routes +app.use('/', router); + +export default app; diff --git a/services/match/src/controllers/index.ts b/services/match/src/controllers/index.ts new file mode 100644 index 0000000000..98bd3586eb --- /dev/null +++ b/services/match/src/controllers/index.ts @@ -0,0 +1,7 @@ +import { Request, Response } from 'express'; + +export const getHealth = async (req: Request, res: Response) => { + res.status(200).json({ + message: 'Server is up and running!', + }); +}; diff --git a/services/match/src/index.ts b/services/match/src/index.ts new file mode 100644 index 0000000000..6d863f78c8 --- /dev/null +++ b/services/match/src/index.ts @@ -0,0 +1,12 @@ +import app from './app'; +import { connectToDB } from './models'; + +const port = process.env.PORT || 8082; + +connectToDB() + .then(() => console.log('MongoDB connected successfully')) + .then(() => app.listen(port, () => console.log(`Question service is listening on port ${port}.`))) + .catch(error => { + console.error('Failed to start server'); + console.error(error); + }); diff --git a/services/match/src/models/index.ts b/services/match/src/models/index.ts new file mode 100644 index 0000000000..ebe06de1e9 --- /dev/null +++ b/services/match/src/models/index.ts @@ -0,0 +1,19 @@ +import mongoose from 'mongoose'; + +export async function connectToDB() { + const mongoURI = process.env.NODE_ENV === 'production' ? process.env.DB_CLOUD_URI : process.env.DB_LOCAL_URI; + + console.log('MongoDB URI:', mongoURI); + + if (!mongoURI) { + throw new Error('MongoDB URI not specified'); + } else if (!process.env.DB_USERNAME || !process.env.DB_PASSWORD) { + throw Error('MongoDB credentials not specified'); + } + + await mongoose.connect(mongoURI, { + authSource: 'admin', + user: process.env.DB_USERNAME, + pass: process.env.DB_PASSWORD, + }); +} diff --git a/services/match/src/routes/index.ts b/services/match/src/routes/index.ts new file mode 100644 index 0000000000..7e7faddcfd --- /dev/null +++ b/services/match/src/routes/index.ts @@ -0,0 +1,7 @@ +import express from 'express'; +import { getHealth } from '../controllers'; +const router = express.Router(); + +router.get('/ht', getHealth); + +export default router; diff --git a/services/match/src/types/custom.ts b/services/match/src/types/custom.ts new file mode 100644 index 0000000000..6644aae8b7 --- /dev/null +++ b/services/match/src/types/custom.ts @@ -0,0 +1,8 @@ +import { Types } from 'mongoose'; + +export interface RequestUser { + id: Types.ObjectId | string; + username: string; + email: string; + isAdmin: boolean; +} diff --git a/services/match/src/types/express/index.d.ts b/services/match/src/types/express/index.d.ts new file mode 100644 index 0000000000..ca510382a8 --- /dev/null +++ b/services/match/src/types/express/index.d.ts @@ -0,0 +1,13 @@ +// https://blog.logrocket.com/extend-express-request-object-typescript/ + +import { RequestUser } from '../custom'; + +export {}; + +declare global { + namespace Express { + export interface Request { + user: RequestUser; + } + } +} diff --git a/services/match/src/utils/helpers.ts b/services/match/src/utils/helpers.ts new file mode 100644 index 0000000000..a2f5de2657 --- /dev/null +++ b/services/match/src/utils/helpers.ts @@ -0,0 +1,64 @@ +import { Response } from 'express'; + +/** + * Handles an internal response and sends a 500 response with the error message. + * @param res + * @param message + */ +export const handleInternalError = (res: Response, message = 'An unexpected error occurred') => { + res.status(500).json({ + status: 'Error', + message, + }); +}; + +/** + * Handles bad requests and sends a 400 response with a custom message. + * @param res + * @param message + */ +export const handleBadRequest = (res: Response, message = 'Bad Request') => { + res.status(400).json({ + status: 'Error', + message, + }); +}; + +/** + * Handles unauthorized requests and sends a 401 response with a custom message. + * @param res + * @param message + */ +export const handleUnauthorized = (res: Response, message = 'Unauthorized Request') => { + res.status(401).json({ + status: 'Error', + message, + }); +}; + +/** + * Handles not found errors and sends a 404 response with a custom message. + * @param res + * @param message + */ +export const handleNotFound = (res: Response, message = 'Not Found') => { + res.status(404).json({ + status: 'Error', + message, + }); +}; + +/** + * Handles successful responses and sends a 200 response message with the provided data. + * @param res + * @param data + * @param message + * @param statusCode - HTTP status code (default is 200) + */ +export const handleSuccess = (res: Response, statusCode = 200, message: string, data: unknown) => { + res.status(statusCode).json({ + status: 'Success', + message, + data, + }); +}; diff --git a/services/match/tsconfig.json b/services/match/tsconfig.json new file mode 100644 index 0000000000..f5b5901804 --- /dev/null +++ b/services/match/tsconfig.json @@ -0,0 +1,14 @@ +/* Visit https://aka.ms/tsconfig to read more about this file */ +{ + "compilerOptions": { + "target": "es2016", + "module": "CommonJS", + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/services/user/src/types/custom.ts b/services/user/src/types/custom.ts index d3d9c90d08..7b556dde12 100644 --- a/services/user/src/types/custom.ts +++ b/services/user/src/types/custom.ts @@ -3,8 +3,8 @@ import { Types } from 'mongoose'; export interface RequestUser { id: Types.ObjectId | string; username: string; - email: string; password?: string; createdAt?: Date; + email: string; isAdmin: boolean; } From 5da51fc8e329c978cf1964b5133dd63936c7e0c6 Mon Sep 17 00:00:00 2001 From: Samuel Lim Date: Tue, 8 Oct 2024 17:45:43 +0800 Subject: [PATCH 02/28] User: Remove extra fields --- services/user/src/types/custom.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/services/user/src/types/custom.ts b/services/user/src/types/custom.ts index 7b556dde12..6644aae8b7 100644 --- a/services/user/src/types/custom.ts +++ b/services/user/src/types/custom.ts @@ -3,8 +3,6 @@ import { Types } from 'mongoose'; export interface RequestUser { id: Types.ObjectId | string; username: string; - password?: string; - createdAt?: Date; email: string; isAdmin: boolean; } From bff05042c7949472a46256c543b8b50b7320b3a5 Mon Sep 17 00:00:00 2001 From: Samuel Lim Date: Tue, 8 Oct 2024 17:39:45 +0800 Subject: [PATCH 03/28] Match: Implement /request routes --- compose.yml | 4 + services/match/package-lock.json | 61 +++++++++++++++ services/match/package.json | 2 + services/match/src/app.ts | 3 + .../src/controllers/matchRequestController.ts | 75 +++++++++++++++++++ services/match/src/index.ts | 4 +- services/match/src/middleware/jwt.ts | 29 +++++++ .../match/src/models/matchRequestModel.ts | 37 +++++++++ .../match/src/routes/matchRequestRoutes.ts | 9 +++ .../{express/index.d.ts => express.d.ts} | 0 .../match/src/types/{custom.ts => request.ts} | 0 services/match/src/types/response.ts | 10 +++ .../src/validation/matchRequestValidation.ts | 14 ++++ 13 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 services/match/src/controllers/matchRequestController.ts create mode 100644 services/match/src/middleware/jwt.ts create mode 100644 services/match/src/models/matchRequestModel.ts create mode 100644 services/match/src/routes/matchRequestRoutes.ts rename services/match/src/types/{express/index.d.ts => express.d.ts} (100%) rename services/match/src/types/{custom.ts => request.ts} (100%) create mode 100644 services/match/src/types/response.ts create mode 100644 services/match/src/validation/matchRequestValidation.ts diff --git a/compose.yml b/compose.yml index ddc7c1f4a1..7e80711b54 100644 --- a/compose.yml +++ b/compose.yml @@ -64,6 +64,7 @@ services: networks: - gateway-network - user-db-network + - match-network restart: always user-db: @@ -93,6 +94,7 @@ services: ports: - 8083:8083 networks: + - match-network - match-db-network restart: always @@ -120,5 +122,7 @@ networks: driver: bridge user-db-network: driver: bridge + match-network: + driver: bridge match-db-network: driver: bridge diff --git a/services/match/package-lock.json b/services/match/package-lock.json index c9686f85f7..4aa14a2dc3 100644 --- a/services/match/package-lock.json +++ b/services/match/package-lock.json @@ -13,6 +13,7 @@ "body-parser": "^1.20.3", "cors": "^2.8.5", "express": "^4.21.0", + "joi": "^17.13.3", "mongoose": "^8.7.0", "morgan": "^1.10.0" }, @@ -22,6 +23,7 @@ "@types/cors": "^2.8.17", "@types/eslint__js": "^8.42.3", "@types/express": "^4.17.21", + "@types/joi": "^17.2.2", "@types/mongoose": "^5.11.96", "@types/morgan": "^1.9.9", "@types/node": "^22.5.4", @@ -211,6 +213,21 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", + "dependencies": { + "@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", @@ -389,6 +406,27 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "license": "BSD-3-Clause" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -523,6 +561,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/joi": { + "version": "17.2.2", + "resolved": "https://registry.npmjs.org/@types/joi/-/joi-17.2.2.tgz", + "integrity": "sha512-vPvPwxn0Y4pQyqkEcMCJYxXCMYcrHqdfFX4SpF4zcqYioYexmDyxtM3OK+m/ZwGBS8/dooJ0il9qCwAdd6KFtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "joi": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2427,6 +2475,19 @@ "dev": true, "license": "ISC" }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", diff --git a/services/match/package.json b/services/match/package.json index 965e72e540..7a74432110 100644 --- a/services/match/package.json +++ b/services/match/package.json @@ -17,6 +17,7 @@ "body-parser": "^1.20.3", "cors": "^2.8.5", "express": "^4.21.0", + "joi": "^17.13.3", "mongoose": "^8.7.0", "morgan": "^1.10.0" }, @@ -26,6 +27,7 @@ "@types/cors": "^2.8.17", "@types/eslint__js": "^8.42.3", "@types/express": "^4.17.21", + "@types/joi": "^17.2.2", "@types/mongoose": "^5.11.96", "@types/morgan": "^1.9.9", "@types/node": "^22.5.4", diff --git a/services/match/src/app.ts b/services/match/src/app.ts index 0af1e48732..fb15a8c8b7 100644 --- a/services/match/src/app.ts +++ b/services/match/src/app.ts @@ -2,7 +2,9 @@ import express, { Express } from 'express'; import morgan from 'morgan'; import cors from 'cors'; import router from './routes'; +import matchRequestRouter from './routes/matchRequestRoutes'; import bodyParser from 'body-parser'; +import { verifyAccessToken } from './middleware/jwt'; const app: Express = express(); @@ -21,5 +23,6 @@ app.use( // Routes app.use('/', router); +app.use('/request', verifyAccessToken, matchRequestRouter); export default app; diff --git a/services/match/src/controllers/matchRequestController.ts b/services/match/src/controllers/matchRequestController.ts new file mode 100644 index 0000000000..15c6e5f154 --- /dev/null +++ b/services/match/src/controllers/matchRequestController.ts @@ -0,0 +1,75 @@ +import { Request, Response } from 'express'; +import { handleBadRequest, handleInternalError, handleNotFound, handleSuccess } from '../utils/helpers'; +import { MatchRequest } from '../models/matchRequestModel'; +import { isValidObjectId } from 'mongoose'; +import { createMatchRequestSchema, updateMatchRequestSchema } from '../validation/matchRequestValidation'; + +/** + * Creates a match request. + * @param req + * @param res + */ +export const createMatchRequest = async (req: Request, res: Response) => { + const user = req.user; + const { error, value } = createMatchRequestSchema.validate(req.query); + if (error) { + return handleBadRequest(res, error.message); + } + + try { + const matchRequest = new MatchRequest({ userId: user.id, ...value }); + await matchRequest.save(); + handleSuccess(res, 201, 'Match request created successfully', matchRequest); + } catch (error) { + console.error('Error in createMatchRequest:', error); + handleInternalError(res, 'Failed to create match request'); + } +}; + +/** + * Updates a match request. + * @param req + * @param res + */ +export const updateMatchRequest = async (req: Request, res: Response) => { + const id = req.params.id; + const userId = req.user.id; + const { error, value } = updateMatchRequestSchema.validate(req.query); + if (error) { + return handleBadRequest(res, error.message); + } + + try { + const matchRequest = await MatchRequest.findOneAndUpdate({ _id: id, userId }, value); + handleSuccess(res, 201, 'Match request update successfully', matchRequest); + } catch (error) { + console.error('Error in updateMatchRequest:', error); + handleInternalError(res, 'Failed to update match request'); + } +}; + +/** + * Deletes a match request. + * @param req + * @param res + */ +export const deleteMatchRequest = async (req: Request, res: Response) => { + const id = req.params.id; + const userId = req.user.id; + + if (!isValidObjectId(id)) { + return handleNotFound(res, `Request ${id} not found`); + } + + try { + const matchRequest = await MatchRequest.findOneAndDelete({ _id: id, userId }); + if (!matchRequest) { + return handleNotFound(res, `Request ${id} not found`); + } + + handleSuccess(res, 200, 'Question deleted successfully', matchRequest); + } catch (error) { + console.log('Error in deleteMatchRequest:', error); + handleInternalError(res, 'Failed to delete match request'); + } +}; diff --git a/services/match/src/index.ts b/services/match/src/index.ts index 6d863f78c8..aa4efa8aca 100644 --- a/services/match/src/index.ts +++ b/services/match/src/index.ts @@ -1,11 +1,11 @@ import app from './app'; import { connectToDB } from './models'; -const port = process.env.PORT || 8082; +const port = process.env.PORT || 8083; connectToDB() .then(() => console.log('MongoDB connected successfully')) - .then(() => app.listen(port, () => console.log(`Question service is listening on port ${port}.`))) + .then(() => app.listen(port, () => console.log(`Match service is listening on port ${port}.`))) .catch(error => { console.error('Failed to start server'); console.error(error); diff --git a/services/match/src/middleware/jwt.ts b/services/match/src/middleware/jwt.ts new file mode 100644 index 0000000000..352ab3bb82 --- /dev/null +++ b/services/match/src/middleware/jwt.ts @@ -0,0 +1,29 @@ +import { NextFunction, Request, Response } from 'express'; +import { handleInternalError, handleUnauthorized } from '../utils/helpers'; +import { VerifyTokenResponse } from '../types/response'; +import axios from 'axios'; + +export async function verifyAccessToken(req: Request, res: Response, next: NextFunction) { + const authHeader = req.headers['authorization']; + if (!authHeader) { + handleUnauthorized(res, 'Authenticated failed'); + return; + } + try { + console.log(authHeader); + const response = await axios.get('http://user:8082/auth/verify-token', { + headers: { authorization: authHeader }, + }); + req.user = response.data.data; + next(); + } catch (error: any) { + if (error?.response?.status == 401) { + handleUnauthorized(res); + return; + } + + console.error(error); + handleInternalError(res); + return; + } +} diff --git a/services/match/src/models/matchRequestModel.ts b/services/match/src/models/matchRequestModel.ts new file mode 100644 index 0000000000..767ab68d32 --- /dev/null +++ b/services/match/src/models/matchRequestModel.ts @@ -0,0 +1,37 @@ +import { model, Schema, Types } from 'mongoose'; + +export enum Difficulty { + Easy = 'Easy', + Medium = 'Medium', + Hard = 'Hard', +} + +export interface IMatchRequest { + id: Types.ObjectId; + userId: Types.ObjectId; + topics: [string]; + difficulty: Difficulty; + createdAt: Date; + updatedAt: Date; +} + +const matchRequestSchema = new Schema( + { + userId: { + type: Schema.Types.ObjectId, + required: true, + }, + topics: { + type: [String], + required: true, + }, + difficulty: { + type: String, + required: true, + enum: ['Easy', 'Medium', 'Hard'], + }, + }, + { versionKey: false, timestamps: true }, +); + +export const MatchRequest = model('MatchRequest', matchRequestSchema); diff --git a/services/match/src/routes/matchRequestRoutes.ts b/services/match/src/routes/matchRequestRoutes.ts new file mode 100644 index 0000000000..cab6d03912 --- /dev/null +++ b/services/match/src/routes/matchRequestRoutes.ts @@ -0,0 +1,9 @@ +import express from 'express'; +import { createMatchRequest, deleteMatchRequest, updateMatchRequest } from '../controllers/matchRequestController'; +const router = express.Router(); + +router.post('', createMatchRequest); +router.put('/:id', updateMatchRequest); +router.delete('/:id', deleteMatchRequest); + +export default router; diff --git a/services/match/src/types/express/index.d.ts b/services/match/src/types/express.d.ts similarity index 100% rename from services/match/src/types/express/index.d.ts rename to services/match/src/types/express.d.ts diff --git a/services/match/src/types/custom.ts b/services/match/src/types/request.ts similarity index 100% rename from services/match/src/types/custom.ts rename to services/match/src/types/request.ts diff --git a/services/match/src/types/response.ts b/services/match/src/types/response.ts new file mode 100644 index 0000000000..7a26dc74aa --- /dev/null +++ b/services/match/src/types/response.ts @@ -0,0 +1,10 @@ +import { RequestUser } from '../models/request'; + +export interface BaseResponse { + status: string; + message: string; +} + +export interface VerifyTokenResponse extends BaseResponse { + data: RequestUser; +} diff --git a/services/match/src/validation/matchRequestValidation.ts b/services/match/src/validation/matchRequestValidation.ts new file mode 100644 index 0000000000..6bcd970771 --- /dev/null +++ b/services/match/src/validation/matchRequestValidation.ts @@ -0,0 +1,14 @@ +import Joi from 'joi'; +import { Difficulty } from '../models/matchRequestModel'; + +export const createMatchRequestSchema = Joi.object({ + topics: Joi.array().items(Joi.string()).min(1).required(), + difficulty: Joi.string() + .valid(...Object.values(Difficulty)) + .required(), +}); + +export const updateMatchRequestSchema = Joi.object({ + topics: Joi.array().items(Joi.string()).min(1), + difficulty: Joi.string().valid(...Object.values(Difficulty)), +}).min(1); From aaf1ce05da7eb62b37d30515f69484cc330f84e0 Mon Sep 17 00:00:00 2001 From: Samuel Lim Date: Tue, 8 Oct 2024 23:20:09 +0800 Subject: [PATCH 04/28] Match: Move DB logic into repository.ts --- .../src/controllers/matchRequestController.ts | 21 +++++++----- services/match/src/index.ts | 2 +- services/match/src/models/index.ts | 19 ----------- services/match/src/models/repository.ts | 32 +++++++++++++++++++ 4 files changed, 46 insertions(+), 28 deletions(-) delete mode 100644 services/match/src/models/index.ts create mode 100644 services/match/src/models/repository.ts diff --git a/services/match/src/controllers/matchRequestController.ts b/services/match/src/controllers/matchRequestController.ts index 15c6e5f154..053fe54e7d 100644 --- a/services/match/src/controllers/matchRequestController.ts +++ b/services/match/src/controllers/matchRequestController.ts @@ -1,8 +1,12 @@ import { Request, Response } from 'express'; import { handleBadRequest, handleInternalError, handleNotFound, handleSuccess } from '../utils/helpers'; -import { MatchRequest } from '../models/matchRequestModel'; import { isValidObjectId } from 'mongoose'; import { createMatchRequestSchema, updateMatchRequestSchema } from '../validation/matchRequestValidation'; +import { + createMatchRequest as _createMatchRequest, + findMatchRequestAndUpdate, + findMatchRequestAndDelete, +} from '../models/repository'; /** * Creates a match request. @@ -10,15 +14,15 @@ import { createMatchRequestSchema, updateMatchRequestSchema } from '../validatio * @param res */ export const createMatchRequest = async (req: Request, res: Response) => { - const user = req.user; const { error, value } = createMatchRequestSchema.validate(req.query); if (error) { return handleBadRequest(res, error.message); } + const userId = req.user.id; + const { topics, difficulty } = value; try { - const matchRequest = new MatchRequest({ userId: user.id, ...value }); - await matchRequest.save(); + const matchRequest = await _createMatchRequest(userId, topics, difficulty); handleSuccess(res, 201, 'Match request created successfully', matchRequest); } catch (error) { console.error('Error in createMatchRequest:', error); @@ -32,15 +36,16 @@ export const createMatchRequest = async (req: Request, res: Response) => { * @param res */ export const updateMatchRequest = async (req: Request, res: Response) => { - const id = req.params.id; - const userId = req.user.id; const { error, value } = updateMatchRequestSchema.validate(req.query); if (error) { return handleBadRequest(res, error.message); } + const id = req.params.id; + const userId = req.user.id; + const { topics, difficulty } = value; try { - const matchRequest = await MatchRequest.findOneAndUpdate({ _id: id, userId }, value); + const matchRequest = await findMatchRequestAndUpdate(id, userId, topics, difficulty); handleSuccess(res, 201, 'Match request update successfully', matchRequest); } catch (error) { console.error('Error in updateMatchRequest:', error); @@ -62,7 +67,7 @@ export const deleteMatchRequest = async (req: Request, res: Response) => { } try { - const matchRequest = await MatchRequest.findOneAndDelete({ _id: id, userId }); + const matchRequest = await findMatchRequestAndDelete(id, userId); if (!matchRequest) { return handleNotFound(res, `Request ${id} not found`); } diff --git a/services/match/src/index.ts b/services/match/src/index.ts index aa4efa8aca..dd570f4e67 100644 --- a/services/match/src/index.ts +++ b/services/match/src/index.ts @@ -1,5 +1,5 @@ import app from './app'; -import { connectToDB } from './models'; +import { connectToDB } from './models/repository'; const port = process.env.PORT || 8083; diff --git a/services/match/src/models/index.ts b/services/match/src/models/index.ts deleted file mode 100644 index ebe06de1e9..0000000000 --- a/services/match/src/models/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import mongoose from 'mongoose'; - -export async function connectToDB() { - const mongoURI = process.env.NODE_ENV === 'production' ? process.env.DB_CLOUD_URI : process.env.DB_LOCAL_URI; - - console.log('MongoDB URI:', mongoURI); - - if (!mongoURI) { - throw new Error('MongoDB URI not specified'); - } else if (!process.env.DB_USERNAME || !process.env.DB_PASSWORD) { - throw Error('MongoDB credentials not specified'); - } - - await mongoose.connect(mongoURI, { - authSource: 'admin', - user: process.env.DB_USERNAME, - pass: process.env.DB_PASSWORD, - }); -} diff --git a/services/match/src/models/repository.ts b/services/match/src/models/repository.ts new file mode 100644 index 0000000000..3b75eb8716 --- /dev/null +++ b/services/match/src/models/repository.ts @@ -0,0 +1,32 @@ +import mongoose from 'mongoose'; +import { Difficulty, MatchRequest } from './matchRequestModel'; + +export async function connectToDB() { + const mongoURI = process.env.NODE_ENV === 'production' ? process.env.DB_CLOUD_URI : process.env.DB_LOCAL_URI; + + console.log('MongoDB URI:', mongoURI); + + if (!mongoURI) { + throw new Error('MongoDB URI not specified'); + } else if (!process.env.DB_USERNAME || !process.env.DB_PASSWORD) { + throw Error('MongoDB credentials not specified'); + } + + await mongoose.connect(mongoURI, { + authSource: 'admin', + user: process.env.DB_USERNAME, + pass: process.env.DB_PASSWORD, + }); +} + +export async function createMatchRequest(userId: string, topics: string[], difficulty: Difficulty) { + return await new MatchRequest({ userId, topics, difficulty }).save(); +} + +export async function findMatchRequestAndUpdate(id: string, userId: string, topics: string[], difficulty: Difficulty) { + return await MatchRequest.findOneAndUpdate({ _id: id, userId }, { $set: { topics, difficulty } }); +} + +export async function findMatchRequestAndDelete(id: string, userId: string) { + return await MatchRequest.findOneAndDelete({ _id: id, userId }); +} From c3bd798aaa4a98d53a57bca27c4e6b318c0b8389 Mon Sep 17 00:00:00 2001 From: Samuel Lim Date: Tue, 8 Oct 2024 23:31:42 +0800 Subject: [PATCH 05/28] Match: Init message broker --- compose.dev.yml | 4 + compose.yml | 7 ++ services/match/package-lock.json | 128 ++++++++++++++++++++++++++ services/match/package.json | 2 + services/match/src/events/broker.ts | 69 ++++++++++++++ services/match/src/events/consumer.ts | 5 + services/match/src/events/producer.ts | 0 services/match/src/index.ts | 4 + 8 files changed, 219 insertions(+) create mode 100644 services/match/src/events/broker.ts create mode 100644 services/match/src/events/consumer.ts create mode 100644 services/match/src/events/producer.ts diff --git a/compose.dev.yml b/compose.dev.yml index d877f67429..63086233f7 100644 --- a/compose.dev.yml +++ b/compose.dev.yml @@ -37,3 +37,7 @@ services: match-db: ports: - 27019:27017 + + match-broker: + ports: + - 5672:5672 diff --git a/compose.yml b/compose.yml index 7e80711b54..60c51f034d 100644 --- a/compose.yml +++ b/compose.yml @@ -109,6 +109,13 @@ services: networks: - match-db-network restart: always + + match-broker: + container_name: match-broker + hostname: match-broker + image: rabbitmq:4.0.2 + networks: + - match-db-network volumes: question-db: diff --git a/services/match/package-lock.json b/services/match/package-lock.json index 4aa14a2dc3..5eb089a86e 100644 --- a/services/match/package-lock.json +++ b/services/match/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "license": "ISC", "dependencies": { + "amqplib": "^0.10.4", "axios": "^1.7.7", "body-parser": "^1.20.3", "cors": "^2.8.5", @@ -19,6 +20,7 @@ }, "devDependencies": { "@eslint/js": "^9.10.0", + "@types/amqplib": "^0.10.5", "@types/axios": "^0.9.36", "@types/cors": "^2.8.17", "@types/eslint__js": "^8.42.3", @@ -39,6 +41,49 @@ "typescript-eslint": "^8.5.0" } }, + "node_modules/@acuminous/bitsyntax": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@acuminous/bitsyntax/-/bitsyntax-0.1.2.tgz", + "integrity": "sha512-29lUK80d1muEQqiUsSo+3A0yP6CdspgC95EnKBMi22Xlwt79i/En4Vr67+cXhU+cZjbti3TgGGC5wy1stIywVQ==", + "license": "MIT", + "dependencies": { + "buffer-more-ints": "~1.0.0", + "debug": "^4.3.4", + "safe-buffer": "~5.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/@acuminous/bitsyntax/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@acuminous/bitsyntax/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@acuminous/bitsyntax/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -462,6 +507,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/amqplib": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.10.5.tgz", + "integrity": "sha512-/cSykxROY7BWwDoi4Y4/jLAuZTshZxd8Ey1QYa/VaXriMotBDoou7V/twJiOSHzU6t1Kp1AHAUXGCgqq+6DNeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/axios": { "version": "0.9.36", "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.9.36.tgz", @@ -1044,6 +1099,21 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/amqplib": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.4.tgz", + "integrity": "sha512-DMZ4eCEjAVdX1II2TfIUpJhfKAuoCeDIo/YyETbfAqehHTXxxs7WOOd+N1Xxr4cKhx12y23zk8/os98FxlZHrw==", + "license": "MIT", + "dependencies": { + "@acuminous/bitsyntax": "^0.1.2", + "buffer-more-ints": "~1.0.0", + "readable-stream": "1.x >=1.1.9", + "url-parse": "~1.5.10" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -1226,6 +1296,12 @@ "node": ">=16.20.1" } }, + "node_modules/buffer-more-ints": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz", + "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==", + "license": "MIT" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1414,6 +1490,12 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -2468,6 +2550,12 @@ "node": ">=8" } }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3722,6 +3810,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -3774,6 +3868,18 @@ "dev": true, "license": "MIT" }, + "node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -3794,6 +3900,12 @@ "dev": true, "license": "MIT" }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -4060,6 +4172,12 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "license": "MIT" + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -4341,6 +4459,16 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", diff --git a/services/match/package.json b/services/match/package.json index 7a74432110..9cc2dbc358 100644 --- a/services/match/package.json +++ b/services/match/package.json @@ -13,6 +13,7 @@ "license": "ISC", "description": "", "dependencies": { + "amqplib": "^0.10.4", "axios": "^1.7.7", "body-parser": "^1.20.3", "cors": "^2.8.5", @@ -23,6 +24,7 @@ }, "devDependencies": { "@eslint/js": "^9.10.0", + "@types/amqplib": "^0.10.5", "@types/axios": "^0.9.36", "@types/cors": "^2.8.17", "@types/eslint__js": "^8.42.3", diff --git a/services/match/src/events/broker.ts b/services/match/src/events/broker.ts new file mode 100644 index 0000000000..ea0697b39d --- /dev/null +++ b/services/match/src/events/broker.ts @@ -0,0 +1,69 @@ +import client, { Channel, Connection } from 'amqplib'; + +// TODO: Add authentication + +/** + * Adapated from + * https://hassanfouad.medium.com/using-rabbitmq-with-nodejs-and-typescript-8b33d56a62cc + */ +class MessageBroker { + connection!: Connection; + channel!: Channel; + private connected = false; + + async connect(): Promise { + if (this.connection && this.channel) { + return; + } + + try { + this.connection = await client.connect('amqp://match-broker'); + console.log('Connected to RabbitMQ'); + this.channel = await this.connection.createChannel(); + this.connected = true; + } catch (error) { + console.error('Failed to connect to RabbitMQ: ', error); + throw error; + } + } + + async produce(queue: string, message: any): Promise { + try { + if (!this.connected) { + await this.connect(); + } + + this.channel.sendToQueue(queue, Buffer.from(JSON.stringify(message))); + } catch (error) { + console.error('Failed to produce message: ', error); + throw error; + } + } + + async consume(queue: string, onMessage: (message: string) => void): Promise { + try { + if (!this.connected) { + await this.connect(); + } + + await this.channel.assertQueue(queue, { durable: true }); + + this.channel.consume( + queue, + msg => { + if (!msg) return console.error('Invalid message from queue ', queue); + + onMessage(msg.content.toString()); + this.channel.ack(msg); + }, + { noAck: false }, + ); + } catch (error) { + console.error('Failed to consume message: ', error); + throw error; + } + } +} + +const messageBroker = new MessageBroker(); +export default messageBroker; diff --git a/services/match/src/events/consumer.ts b/services/match/src/events/consumer.ts new file mode 100644 index 0000000000..ecdb4a680b --- /dev/null +++ b/services/match/src/events/consumer.ts @@ -0,0 +1,5 @@ +import messageBroker from './broker'; + +export async function initializeConsumers() { + // TODO: Add consumers +} diff --git a/services/match/src/events/producer.ts b/services/match/src/events/producer.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/services/match/src/index.ts b/services/match/src/index.ts index dd570f4e67..60fed0b15a 100644 --- a/services/match/src/index.ts +++ b/services/match/src/index.ts @@ -1,10 +1,14 @@ import app from './app'; +import messageBroker from './events/broker'; +import { initializeConsumers } from './events/consumer'; import { connectToDB } from './models/repository'; const port = process.env.PORT || 8083; connectToDB() .then(() => console.log('MongoDB connected successfully')) + .then(async () => await messageBroker.connect()) + .then(async () => await initializeConsumers()) .then(() => app.listen(port, () => console.log(`Match service is listening on port ${port}.`))) .catch(error => { console.error('Failed to start server'); From 164b5a48f9a8e9af032ba0d22a2f048aad01c9c3 Mon Sep 17 00:00:00 2001 From: Samuel Lim Date: Thu, 10 Oct 2024 00:45:12 +0800 Subject: [PATCH 06/28] Match: Add endpoints and consumers --- .../src/controllers/matchRequestController.ts | 45 +++++++++++++++++-- services/match/src/events/broker.ts | 4 +- services/match/src/events/consumer.ts | 41 ++++++++++++++++- services/match/src/events/producer.ts | 24 ++++++++++ services/match/src/events/queues.ts | 5 +++ services/match/src/middleware/jwt.ts | 2 +- .../match/src/models/matchRequestModel.ts | 39 ++++++++++++++-- services/match/src/models/repository.ts | 14 +++--- .../match/src/routes/matchRequestRoutes.ts | 8 +++- services/match/src/types/event.ts | 34 ++++++++++++++ services/match/src/types/response.ts | 7 ++- services/match/src/utils/date.ts | 3 ++ .../src/utils/{helpers.ts => responses.ts} | 0 13 files changed, 208 insertions(+), 18 deletions(-) create mode 100644 services/match/src/events/queues.ts create mode 100644 services/match/src/types/event.ts create mode 100644 services/match/src/utils/date.ts rename services/match/src/utils/{helpers.ts => responses.ts} (100%) diff --git a/services/match/src/controllers/matchRequestController.ts b/services/match/src/controllers/matchRequestController.ts index 053fe54e7d..999759f9de 100644 --- a/services/match/src/controllers/matchRequestController.ts +++ b/services/match/src/controllers/matchRequestController.ts @@ -1,12 +1,15 @@ import { Request, Response } from 'express'; -import { handleBadRequest, handleInternalError, handleNotFound, handleSuccess } from '../utils/helpers'; +import { handleBadRequest, handleInternalError, handleNotFound, handleSuccess } from '../utils/responses'; import { isValidObjectId } from 'mongoose'; import { createMatchRequestSchema, updateMatchRequestSchema } from '../validation/matchRequestValidation'; import { createMatchRequest as _createMatchRequest, findMatchRequestAndUpdate, findMatchRequestAndDelete, + findMatchRequest, } from '../models/repository'; +import { produceMatchUpdatedRequest } from '../events/producer'; +import { getStatus } from '../models/matchRequestModel'; /** * Creates a match request. @@ -19,10 +22,11 @@ export const createMatchRequest = async (req: Request, res: Response) => { return handleBadRequest(res, error.message); } - const userId = req.user.id; + const { id: userId, username } = req.user; const { topics, difficulty } = value; try { - const matchRequest = await _createMatchRequest(userId, topics, difficulty); + const matchRequest = await _createMatchRequest(userId, username, topics, difficulty); + await produceMatchUpdatedRequest(matchRequest.id, userId, username, topics, difficulty); handleSuccess(res, 201, 'Match request created successfully', matchRequest); } catch (error) { console.error('Error in createMatchRequest:', error); @@ -42,10 +46,14 @@ export const updateMatchRequest = async (req: Request, res: Response) => { } const id = req.params.id; - const userId = req.user.id; + const { id: userId, username } = req.user; const { topics, difficulty } = value; try { const matchRequest = await findMatchRequestAndUpdate(id, userId, topics, difficulty); + if (!matchRequest) { + return handleNotFound(res, `Request ${id} not found`); + } + await produceMatchUpdatedRequest(matchRequest.id, userId, username, topics, difficulty); handleSuccess(res, 201, 'Match request update successfully', matchRequest); } catch (error) { console.error('Error in updateMatchRequest:', error); @@ -78,3 +86,32 @@ export const deleteMatchRequest = async (req: Request, res: Response) => { handleInternalError(res, 'Failed to delete match request'); } }; + +/** + * Retrieves a match request status. + * @param req + * @param res + */ +export const retrieveMatchRequest = async (req: Request, res: Response) => { + const id = req.params.id; + const userId = req.user.id; + + if (!isValidObjectId(id)) { + return handleNotFound(res, `Request ${id} not found`); + } + + try { + const matchRequest = await findMatchRequest(id, userId); + if (!matchRequest) { + return handleNotFound(res, `Request ${id} not found`); + } + + const status = getStatus(matchRequest); + const response = { ...matchRequest.toObject(), status }; + + handleSuccess(res, 200, 'Match request retrieved successfully', response); + } catch (error) { + console.log('Error in retrieveMatchRequest:', error); + handleInternalError(res, 'Failed to retrieve match request'); + } +}; diff --git a/services/match/src/events/broker.ts b/services/match/src/events/broker.ts index ea0697b39d..21e1bd2802 100644 --- a/services/match/src/events/broker.ts +++ b/services/match/src/events/broker.ts @@ -40,7 +40,7 @@ class MessageBroker { } } - async consume(queue: string, onMessage: (message: string) => void): Promise { + async consume(queue: string, onMessage: (message: T) => void): Promise { try { if (!this.connected) { await this.connect(); @@ -53,7 +53,7 @@ class MessageBroker { msg => { if (!msg) return console.error('Invalid message from queue ', queue); - onMessage(msg.content.toString()); + onMessage(JSON.parse(msg.content.toString()) as T); this.channel.ack(msg); }, { noAck: false }, diff --git a/services/match/src/events/consumer.ts b/services/match/src/events/consumer.ts index ecdb4a680b..7e59442c79 100644 --- a/services/match/src/events/consumer.ts +++ b/services/match/src/events/consumer.ts @@ -1,5 +1,44 @@ +import { MatchRequestModel } from '../models/matchRequestModel'; +import { CollabCreatedEvent, MatchUpdatedEvent } from '../types/event'; +import { oneMinuteAgo } from '../utils/date'; import messageBroker from './broker'; +import { produceMatchFound } from './producer'; +import { Queues } from './queues'; + +async function consumeMatchUpdated(msg: MatchUpdatedEvent) { + const { + user: { id: userId, username, requestId }, + topics, + difficulty, + } = msg; + + const match = await MatchRequestModel.findOneAndUpdate( + { + _id: { $ne: requestId }, + userId: { $ne: userId }, + pairId: null, + topics: { $in: topics }, + difficulty, + updatedAt: { $gte: oneMinuteAgo() }, + }, + { $set: { pairId: requestId } }, + ); + + if (!match) return; + await MatchRequestModel.findByIdAndUpdate(requestId, { $set: { pairId: match.id } }); + + const user1 = { id: userId, username, requestId }; + const user2 = { id: match.userId, username: match.username, requestId: match.id }; + const commonTopics = topics.filter(topic => match.topics.includes(topic)); + await produceMatchFound(user1, user2, commonTopics, difficulty); +} + +async function consumeCollabCreated(msg: CollabCreatedEvent) { + const { requestId1, requestId2, collabId } = msg; + await MatchRequestModel.updateMany({ _id: { $in: [requestId1, requestId2] } }, { $set: { collabId } }); +} export async function initializeConsumers() { - // TODO: Add consumers + messageBroker.consume(Queues.MATCH_REQUEST_UPDATED, consumeMatchUpdated); + messageBroker.consume(Queues.COLLAB_CREATED, consumeCollabCreated); } diff --git a/services/match/src/events/producer.ts b/services/match/src/events/producer.ts index e69de29bb2..080ea8cc2f 100644 --- a/services/match/src/events/producer.ts +++ b/services/match/src/events/producer.ts @@ -0,0 +1,24 @@ +import { Difficulty } from '../models/matchRequestModel'; +import { MatchUpdatedEvent, MatchFoundEvent } from '../types/event'; +import messageBroker from './broker'; +import { Queues } from './queues'; + +export async function produceMatchUpdatedRequest( + requestId: string, + userId: string, + username: string, + topics: string[], + difficulty: Difficulty, +) { + const message: MatchUpdatedEvent = { + user: { id: userId, username, requestId }, + topics, + difficulty, + }; + await messageBroker.produce(Queues.MATCH_REQUEST_UPDATED, message); +} + +export async function produceMatchFound(user1: any, user2: any, topics: string[], difficulty: Difficulty) { + const message: MatchFoundEvent = { user1, user2, topics, difficulty }; + await messageBroker.produce(Queues.MATCH_FOUND, message); +} diff --git a/services/match/src/events/queues.ts b/services/match/src/events/queues.ts new file mode 100644 index 0000000000..fdbb36d6b7 --- /dev/null +++ b/services/match/src/events/queues.ts @@ -0,0 +1,5 @@ +export enum Queues { + MATCH_REQUEST_UPDATED = 'MATCH_REQUEST_UPDATED', + MATCH_FOUND = 'MATCH_FOUND', + COLLAB_CREATED = 'COLLAB_CREATED', +} diff --git a/services/match/src/middleware/jwt.ts b/services/match/src/middleware/jwt.ts index 352ab3bb82..3fb04abf34 100644 --- a/services/match/src/middleware/jwt.ts +++ b/services/match/src/middleware/jwt.ts @@ -1,5 +1,5 @@ import { NextFunction, Request, Response } from 'express'; -import { handleInternalError, handleUnauthorized } from '../utils/helpers'; +import { handleInternalError, handleUnauthorized } from '../utils/responses'; import { VerifyTokenResponse } from '../types/response'; import axios from 'axios'; diff --git a/services/match/src/models/matchRequestModel.ts b/services/match/src/models/matchRequestModel.ts index 767ab68d32..54c8ed1de4 100644 --- a/services/match/src/models/matchRequestModel.ts +++ b/services/match/src/models/matchRequestModel.ts @@ -1,4 +1,5 @@ import { model, Schema, Types } from 'mongoose'; +import { oneMinuteAgo } from '../utils/date'; export enum Difficulty { Easy = 'Easy', @@ -6,21 +7,35 @@ export enum Difficulty { Hard = 'Hard', } -export interface IMatchRequest { +export enum MatchRequestStatus { + PENDING = 'PENDING', + TIME_OUT = 'TIME_OUT', + MATCH_FOUND = 'MATCH_FOUND', + COLLAB_CREATED = 'COLLAB_CREATED', +} + +export interface MatchRequest { id: Types.ObjectId; userId: Types.ObjectId; + username: string; topics: [string]; difficulty: Difficulty; createdAt: Date; updatedAt: Date; + pairId: Types.ObjectId; + collabId: Types.ObjectId; } -const matchRequestSchema = new Schema( +const matchRequestSchema = new Schema( { userId: { type: Schema.Types.ObjectId, required: true, }, + username: { + type: String, + required: true, + }, topics: { type: [String], required: true, @@ -30,8 +45,26 @@ const matchRequestSchema = new Schema( required: true, enum: ['Easy', 'Medium', 'Hard'], }, + pairId: { + type: Schema.Types.ObjectId, + required: false, + }, + collabId: { + type: Schema.Types.ObjectId, + required: false, + }, }, { versionKey: false, timestamps: true }, ); -export const MatchRequest = model('MatchRequest', matchRequestSchema); +export const MatchRequestModel = model('MatchRequest', matchRequestSchema); + +export function getStatus(matchRequest: MatchRequest): MatchRequestStatus { + return matchRequest.collabId + ? MatchRequestStatus.COLLAB_CREATED + : matchRequest.pairId + ? MatchRequestStatus.MATCH_FOUND + : matchRequest.updatedAt >= oneMinuteAgo() + ? MatchRequestStatus.PENDING + : MatchRequestStatus.TIME_OUT; +} diff --git a/services/match/src/models/repository.ts b/services/match/src/models/repository.ts index 3b75eb8716..e0f9d30fcd 100644 --- a/services/match/src/models/repository.ts +++ b/services/match/src/models/repository.ts @@ -1,5 +1,5 @@ import mongoose from 'mongoose'; -import { Difficulty, MatchRequest } from './matchRequestModel'; +import { Difficulty, MatchRequestModel } from './matchRequestModel'; export async function connectToDB() { const mongoURI = process.env.NODE_ENV === 'production' ? process.env.DB_CLOUD_URI : process.env.DB_LOCAL_URI; @@ -19,14 +19,18 @@ export async function connectToDB() { }); } -export async function createMatchRequest(userId: string, topics: string[], difficulty: Difficulty) { - return await new MatchRequest({ userId, topics, difficulty }).save(); +export async function createMatchRequest(userId: string, username: string, topics: string[], difficulty: Difficulty) { + return await new MatchRequestModel({ userId, username, topics, difficulty }).save(); } export async function findMatchRequestAndUpdate(id: string, userId: string, topics: string[], difficulty: Difficulty) { - return await MatchRequest.findOneAndUpdate({ _id: id, userId }, { $set: { topics, difficulty } }); + return await MatchRequestModel.findOneAndUpdate({ _id: id, userId }, { $set: { topics, difficulty } }); } export async function findMatchRequestAndDelete(id: string, userId: string) { - return await MatchRequest.findOneAndDelete({ _id: id, userId }); + return await MatchRequestModel.findOneAndDelete({ _id: id, userId }); +} + +export async function findMatchRequest(id: string, userId: string) { + return await MatchRequestModel.findOne({ _id: id, userId }); } diff --git a/services/match/src/routes/matchRequestRoutes.ts b/services/match/src/routes/matchRequestRoutes.ts index cab6d03912..543b1a003e 100644 --- a/services/match/src/routes/matchRequestRoutes.ts +++ b/services/match/src/routes/matchRequestRoutes.ts @@ -1,9 +1,15 @@ import express from 'express'; -import { createMatchRequest, deleteMatchRequest, updateMatchRequest } from '../controllers/matchRequestController'; +import { + createMatchRequest, + deleteMatchRequest, + retrieveMatchRequest, + updateMatchRequest, +} from '../controllers/matchRequestController'; const router = express.Router(); router.post('', createMatchRequest); router.put('/:id', updateMatchRequest); router.delete('/:id', deleteMatchRequest); +router.get('/:id', retrieveMatchRequest); export default router; diff --git a/services/match/src/types/event.ts b/services/match/src/types/event.ts new file mode 100644 index 0000000000..5c230103f6 --- /dev/null +++ b/services/match/src/types/event.ts @@ -0,0 +1,34 @@ +import { Types } from 'mongoose'; +import { Difficulty } from '../models/matchRequestModel'; + +export interface UserWithRequest { + id: Types.ObjectId | string; + username: string; + email: string; + requestId: Types.ObjectId | string; +} + +export interface MatchRequestUser { + id: Types.ObjectId | string; + username: string; + requestId: Types.ObjectId | string; +} + +export interface MatchUpdatedEvent { + user: MatchRequestUser; + topics: string[]; + difficulty: Difficulty; +} + +export interface MatchFoundEvent { + user1: UserWithRequest; + user2: UserWithRequest; + topics: string[]; + difficulty: Difficulty; +} + +export interface CollabCreatedEvent { + requestId1: Types.ObjectId | string; + requestId2: Types.ObjectId | string; + collabId: Types.ObjectId | string; +} diff --git a/services/match/src/types/response.ts b/services/match/src/types/response.ts index 7a26dc74aa..d5a63921ce 100644 --- a/services/match/src/types/response.ts +++ b/services/match/src/types/response.ts @@ -1,4 +1,5 @@ -import { RequestUser } from '../models/request'; +import { MatchRequest, MatchRequestStatus } from '../models/matchRequestModel'; +import { RequestUser } from './request'; export interface BaseResponse { status: string; @@ -8,3 +9,7 @@ export interface BaseResponse { export interface VerifyTokenResponse extends BaseResponse { data: RequestUser; } + +export interface MatchRequestWithStatus extends MatchRequest { + status: MatchRequestStatus; +} diff --git a/services/match/src/utils/date.ts b/services/match/src/utils/date.ts new file mode 100644 index 0000000000..9cb1ea938a --- /dev/null +++ b/services/match/src/utils/date.ts @@ -0,0 +1,3 @@ +export const oneMinuteAgo = (): Date => { + return new Date(Date.now() - 60 * 1000); +}; diff --git a/services/match/src/utils/helpers.ts b/services/match/src/utils/responses.ts similarity index 100% rename from services/match/src/utils/helpers.ts rename to services/match/src/utils/responses.ts From 0d3e85ecd2e05efc0bc35fdfcfdde788fa4b0672 Mon Sep 17 00:00:00 2001 From: Samuel Lim Date: Thu, 17 Oct 2024 14:43:49 +0800 Subject: [PATCH 07/28] Small fixes * Fix minor typos * Modify requests to use POST body rather than query params * Ensure match request only finds requests within valid time --- services/match/src/controllers/matchRequestController.ts | 6 +++--- services/match/src/models/repository.ts | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/services/match/src/controllers/matchRequestController.ts b/services/match/src/controllers/matchRequestController.ts index 999759f9de..4f78bde39e 100644 --- a/services/match/src/controllers/matchRequestController.ts +++ b/services/match/src/controllers/matchRequestController.ts @@ -17,7 +17,7 @@ import { getStatus } from '../models/matchRequestModel'; * @param res */ export const createMatchRequest = async (req: Request, res: Response) => { - const { error, value } = createMatchRequestSchema.validate(req.query); + const { error, value } = createMatchRequestSchema.validate(req.body); if (error) { return handleBadRequest(res, error.message); } @@ -40,7 +40,7 @@ export const createMatchRequest = async (req: Request, res: Response) => { * @param res */ export const updateMatchRequest = async (req: Request, res: Response) => { - const { error, value } = updateMatchRequestSchema.validate(req.query); + const { error, value } = updateMatchRequestSchema.validate(req.body); if (error) { return handleBadRequest(res, error.message); } @@ -80,7 +80,7 @@ export const deleteMatchRequest = async (req: Request, res: Response) => { return handleNotFound(res, `Request ${id} not found`); } - handleSuccess(res, 200, 'Question deleted successfully', matchRequest); + handleSuccess(res, 200, 'Match request deleted successfully', matchRequest); } catch (error) { console.log('Error in deleteMatchRequest:', error); handleInternalError(res, 'Failed to delete match request'); diff --git a/services/match/src/models/repository.ts b/services/match/src/models/repository.ts index e0f9d30fcd..85fcc4b144 100644 --- a/services/match/src/models/repository.ts +++ b/services/match/src/models/repository.ts @@ -1,5 +1,6 @@ import mongoose from 'mongoose'; import { Difficulty, MatchRequestModel } from './matchRequestModel'; +import { oneMinuteAgo } from '../utils/date'; export async function connectToDB() { const mongoURI = process.env.NODE_ENV === 'production' ? process.env.DB_CLOUD_URI : process.env.DB_LOCAL_URI; @@ -28,7 +29,7 @@ export async function findMatchRequestAndUpdate(id: string, userId: string, topi } export async function findMatchRequestAndDelete(id: string, userId: string) { - return await MatchRequestModel.findOneAndDelete({ _id: id, userId }); + return await MatchRequestModel.findOneAndDelete({ _id: id, userId, updatedAt: { $gte: oneMinuteAgo() } }); } export async function findMatchRequest(id: string, userId: string) { From b7b84ea3fe43ab6062b3843c12fc746cffa05dc9 Mon Sep 17 00:00:00 2001 From: Samuel Lim Date: Thu, 17 Oct 2024 22:53:29 +0800 Subject: [PATCH 08/28] Match: Use gateway --- compose.dev.yml | 2 ++ compose.yml | 7 +------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/compose.dev.yml b/compose.dev.yml index 63086233f7..b0ab310edc 100644 --- a/compose.dev.yml +++ b/compose.dev.yml @@ -30,6 +30,8 @@ services: match: command: npm run dev + ports: + - 8083:8083 volumes: - /app/node_modules - ./services/match:/app diff --git a/compose.yml b/compose.yml index 60c51f034d..33b7490d25 100644 --- a/compose.yml +++ b/compose.yml @@ -64,7 +64,6 @@ services: networks: - gateway-network - user-db-network - - match-network restart: always user-db: @@ -91,10 +90,8 @@ services: DB_LOCAL_URI: ${MATCH_DB_LOCAL_URI} DB_USERNAME: ${MATCH_DB_USERNAME} DB_PASSWORD: ${MATCH_DB_PASSWORD} - ports: - - 8083:8083 networks: - - match-network + - gateway-network - match-db-network restart: always @@ -129,7 +126,5 @@ networks: driver: bridge user-db-network: driver: bridge - match-network: - driver: bridge match-db-network: driver: bridge From 2cd617c77a4fab7b44238a5d7017c843dea0dee5 Mon Sep 17 00:00:00 2001 From: Samuel Lim Date: Thu, 17 Oct 2024 23:18:15 +0800 Subject: [PATCH 09/28] Add Match Service --- frontend/src/_services/match.service.ts | 37 ++++++++++++++++++++++++ frontend/src/app/matching/match.model.ts | 35 ++++++++++++++++++++++ nginx/default.conf | 9 ++++++ 3 files changed, 81 insertions(+) create mode 100644 frontend/src/_services/match.service.ts create mode 100644 frontend/src/app/matching/match.model.ts diff --git a/frontend/src/_services/match.service.ts b/frontend/src/_services/match.service.ts new file mode 100644 index 0000000000..6c6581e1df --- /dev/null +++ b/frontend/src/_services/match.service.ts @@ -0,0 +1,37 @@ +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { ApiService } from './api.service'; +import { MatchRequest, MatchResponse } from '../app/matching/match.model'; + +@Injectable({ + providedIn: 'root', +}) +export class MatchService extends ApiService { + protected apiPath = 'match'; + + private httpOptions = { + headers: new HttpHeaders({ + 'Content-Type': 'application/json', + }), + }; + + constructor(private http: HttpClient) { + super(); + } + + createMatchRequest(matchRequest: MatchRequest) { + return this.http.post(this.apiUrl, matchRequest, this.httpOptions); + } + + retrieveMatchRequest(id: string) { + return this.http.get(this.apiUrl + '/' + id); + } + + updateMatchRequest(id: string, matchRequest: MatchRequest) { + return this.http.put(this.apiUrl + '/' + id, matchRequest, this.httpOptions); + } + + deleteMatchRequest(id: string) { + return this.http.delete(this.apiUrl + '/' + id); + } +} diff --git a/frontend/src/app/matching/match.model.ts b/frontend/src/app/matching/match.model.ts new file mode 100644 index 0000000000..f18182fafc --- /dev/null +++ b/frontend/src/app/matching/match.model.ts @@ -0,0 +1,35 @@ +import { Difficulty } from './user-criteria.model'; + +export interface MatchRequest { + topics: string[]; + difficulty: Difficulty; +} + +export enum MatchStatus { + PENDING = 'PENDING', + TIME_OUT = 'TIME_OUT', + MATCH_FOUND = 'MATCH_FOUND', + COLLAB_CREATED = 'COLLAB_CREATED', +} + +export interface MatchRequestStatus { + _id: string; + userId: string; + username: string; + createdAt: Date; + updatedAt: Date; + topics: string[]; + difficulty: Difficulty; + status?: MatchStatus; + pairId?: string; + collabId?: string; +} + +export interface BaseResponse { + status: string; + message: string; +} + +export interface MatchResponse extends BaseResponse { + data: MatchRequestStatus; +} diff --git a/nginx/default.conf b/nginx/default.conf index 89f3fc5525..3e3730a84b 100644 --- a/nginx/default.conf +++ b/nginx/default.conf @@ -6,6 +6,10 @@ upstream user-api { server user:8082; } +upstream match-api { + server match:8083; +} + server { listen 8080; server_name localhost; @@ -19,4 +23,9 @@ server { proxy_pass http://user-api/; proxy_set_header Host $host; } + + location /api/match/ { + proxy_pass http://match-api/; + proxy_set_header Host $host; + } } From 248ea90200b477f0650deaaa0bc7463750e24252 Mon Sep 17 00:00:00 2001 From: McNaBry Date: Thu, 17 Oct 2024 23:59:34 +0800 Subject: [PATCH 10/28] Integrate matching into frontend --- frontend/src/_services/match.service.ts | 2 +- .../finding-match/finding-match.component.ts | 67 ++++++++++++++++--- .../src/app/matching/matching.component.html | 3 + .../src/app/matching/matching.component.ts | 23 +++++-- 4 files changed, 81 insertions(+), 14 deletions(-) diff --git a/frontend/src/_services/match.service.ts b/frontend/src/_services/match.service.ts index 6c6581e1df..ba0c21efad 100644 --- a/frontend/src/_services/match.service.ts +++ b/frontend/src/_services/match.service.ts @@ -7,7 +7,7 @@ import { MatchRequest, MatchResponse } from '../app/matching/match.model'; providedIn: 'root', }) export class MatchService extends ApiService { - protected apiPath = 'match'; + protected apiPath = 'match/request'; private httpOptions = { headers: new HttpHeaders({ diff --git a/frontend/src/app/matching/finding-match/finding-match.component.ts b/frontend/src/app/matching/finding-match/finding-match.component.ts index 0e8fa120d8..d5484fb603 100644 --- a/frontend/src/app/matching/finding-match/finding-match.component.ts +++ b/frontend/src/app/matching/finding-match/finding-match.component.ts @@ -5,6 +5,12 @@ import { ButtonModule } from 'primeng/button'; import { ProgressSpinnerModule } from 'primeng/progressspinner'; import { ChipModule } from 'primeng/chip'; import { CommonModule } from '@angular/common'; +import { catchError, delay, filter, map, Observable, of, repeat, Subject, Subscription, switchMap, take, takeUntil, takeWhile, tap, timer, UnsubscriptionError } from 'rxjs'; +import { QuestionService } from '../../../_services/question.service'; +import { TopicResponse } from '../../questions/topic.model'; +import { MessageService } from 'primeng/api'; +import { MatchService } from '../../../_services/match.service'; +import { MatchResponse, MatchStatus } from '../match.model'; @Component({ selector: 'app-finding-match', @@ -15,6 +21,7 @@ import { CommonModule } from '@angular/common'; }) export class FindingMatchComponent { @Input() userCriteria!: UserCriteria; + @Input() matchId!: string; @Input() isVisible = false; @Output() dialogClose = new EventEmitter(); @@ -23,7 +30,15 @@ export class FindingMatchComponent { isFindingMatch = true; + matchPoll!: Subscription; + + constructor( + private matchService: MatchService, + private messageService: MessageService + ) {} + closeDialog() { + this.matchPoll.unsubscribe(); this.dialogClose.emit(); } @@ -37,14 +52,50 @@ export class FindingMatchComponent { // Possible to handle routing to workspace here. } + stopPolling$ = new EventEmitter(); + + requestData() { + return this.matchService.retrieveMatchRequest(this.matchId).pipe( + tap((response: MatchResponse) => { + console.log(response); + const status: MatchStatus = response.data.status || MatchStatus.PENDING + switch(status) { + case MatchStatus.MATCH_FOUND: + this.onMatchSuccess(); + break; + case MatchStatus.TIME_OUT: + this.stopPolling$.next(false); + this.onMatchFailed(); + break; + // TODO: Add case for MatchStatus.COLLAB_CREATED + } + }), + catchError( + _ => { + this.messageService.add({ + severity: 'error', + summary: 'Error', + detail: `Something went wrong while matching.`, + life: 3000, + }); + this.closeDialog(); + return of(null); + } + ) + ) + } + + startPolling(interval: number): Observable { + return timer(0, interval) + .pipe( + switchMap(_ => this.requestData()) + ); + } + onDialogShow() { - // Simulate request to API and subsequent success/failure. - setTimeout(() => { - if (this.isVisible) { - // Toggle to simulate different situations. - // this.onMatchFailed(); - this.onMatchSuccess(); - } - }, 3000); + this.matchPoll = this.startPolling(5000).pipe( + tap(), + takeUntil(this.stopPolling$), + ).subscribe(); } } diff --git a/frontend/src/app/matching/matching.component.html b/frontend/src/app/matching/matching.component.html index d23ca108c2..22082f4978 100644 --- a/frontend/src/app/matching/matching.component.html +++ b/frontend/src/app/matching/matching.component.html @@ -59,6 +59,7 @@

Matching Criteria

@@ -66,4 +67,6 @@

Matching Criteria

[isVisible]="isMatchFailed" (retryMatch)="onRetryMatchRequest()" (dialogClose)="onRetryMatchDialogClose()" /> + + diff --git a/frontend/src/app/matching/matching.component.ts b/frontend/src/app/matching/matching.component.ts index 0502f53c75..2adddb9c42 100644 --- a/frontend/src/app/matching/matching.component.ts +++ b/frontend/src/app/matching/matching.component.ts @@ -14,6 +14,9 @@ import { QuestionService } from '../../_services/question.service'; import { MessageService } from 'primeng/api'; import { HAS_NO_QUESTIONS, hasQuestionsValidator } from './_validators/has-questions.validator'; import { Difficulty } from './user-criteria.model'; +import { ToastModule } from 'primeng/toast'; +import { MatchService } from '../../_services/match.service'; +import { MatchRequest } from './match.model'; @Component({ selector: 'app-matching', @@ -22,6 +25,7 @@ import { Difficulty } from './user-criteria.model'; FindingMatchComponent, RetryMatchingComponent, ChipModule, + ToastModule, MultiSelectModule, PanelModule, DropdownModule, @@ -43,10 +47,12 @@ export class MatchingComponent implements OnInit { isLoadingTopics = true; isProcessingMatch = false; isMatchFailed = false; + matchId!: string; constructor( private messageService: MessageService, private questionService: QuestionService, + private matchService: MatchService, ) {} matchForm!: FormGroup; @@ -104,12 +110,19 @@ export class MatchingComponent implements OnInit { } onMatch() { - console.log({ - topic: this.topics, - difficulty: this.difficulty, + const matchRequest: MatchRequest = {topics: this.topics, difficulty: this.difficulty} + console.log(matchRequest); + this.matchService.createMatchRequest(matchRequest).subscribe({ + next: response => { + this.matchId = response.data._id; + }, + error: () => { + this.onErrorReceive('Failed to create a match. Please try again later.'); + }, + complete: () => { + this.isProcessingMatch = true; + }, }); - this.isProcessingMatch = true; - // TODO: Add API request to start matching. } onMatchFailed() { From 8004d5e536faf6d2e500ec928a464df8f2a5ad46 Mon Sep 17 00:00:00 2001 From: McNaBry Date: Fri, 18 Oct 2024 00:34:56 +0800 Subject: [PATCH 11/28] Add handling for timeout and cancellation of match --- .../finding-match/finding-match.component.ts | 62 ++++++++++--------- .../src/app/matching/matching.component.html | 2 + .../src/app/matching/matching.component.ts | 2 +- .../retry-matching.component.ts | 28 ++++++++- services/match/src/utils/date.ts | 2 +- 5 files changed, 65 insertions(+), 31 deletions(-) diff --git a/frontend/src/app/matching/finding-match/finding-match.component.ts b/frontend/src/app/matching/finding-match/finding-match.component.ts index d5484fb603..0973bebd7b 100644 --- a/frontend/src/app/matching/finding-match/finding-match.component.ts +++ b/frontend/src/app/matching/finding-match/finding-match.component.ts @@ -5,9 +5,7 @@ import { ButtonModule } from 'primeng/button'; import { ProgressSpinnerModule } from 'primeng/progressspinner'; import { ChipModule } from 'primeng/chip'; import { CommonModule } from '@angular/common'; -import { catchError, delay, filter, map, Observable, of, repeat, Subject, Subscription, switchMap, take, takeUntil, takeWhile, tap, timer, UnsubscriptionError } from 'rxjs'; -import { QuestionService } from '../../../_services/question.service'; -import { TopicResponse } from '../../questions/topic.model'; +import { catchError, Observable, of, Subscription, switchMap, takeUntil, tap, timer } from 'rxjs'; import { MessageService } from 'primeng/api'; import { MatchService } from '../../../_services/match.service'; import { MatchResponse, MatchStatus } from '../match.model'; @@ -34,12 +32,28 @@ export class FindingMatchComponent { constructor( private matchService: MatchService, - private messageService: MessageService + private messageService: MessageService, ) {} closeDialog() { this.matchPoll.unsubscribe(); - this.dialogClose.emit(); + this.matchService.deleteMatchRequest(this.matchId).subscribe({ + next: response => { + console.log(response); + }, + error: () => { + this.messageService.add({ + severity: 'error', + summary: 'Error', + detail: `Something went wrong while cancelling your match.`, + life: 3000, + }); + this.closeDialog(); + }, + complete: () => { + this.dialogClose.emit(); + }, + }); } onMatchFailed() { @@ -58,8 +72,8 @@ export class FindingMatchComponent { return this.matchService.retrieveMatchRequest(this.matchId).pipe( tap((response: MatchResponse) => { console.log(response); - const status: MatchStatus = response.data.status || MatchStatus.PENDING - switch(status) { + const status: MatchStatus = response.data.status || MatchStatus.PENDING; + switch (status) { case MatchStatus.MATCH_FOUND: this.onMatchSuccess(); break; @@ -70,32 +84,24 @@ export class FindingMatchComponent { // TODO: Add case for MatchStatus.COLLAB_CREATED } }), - catchError( - _ => { - this.messageService.add({ - severity: 'error', - summary: 'Error', - detail: `Something went wrong while matching.`, - life: 3000, - }); - this.closeDialog(); - return of(null); - } - ) - ) + catchError(() => { + this.messageService.add({ + severity: 'error', + summary: 'Error', + detail: `Something went wrong while matching.`, + life: 3000, + }); + this.closeDialog(); + return of(null); + }), + ); } startPolling(interval: number): Observable { - return timer(0, interval) - .pipe( - switchMap(_ => this.requestData()) - ); + return timer(0, interval).pipe(switchMap(() => this.requestData())); } onDialogShow() { - this.matchPoll = this.startPolling(5000).pipe( - tap(), - takeUntil(this.stopPolling$), - ).subscribe(); + this.matchPoll = this.startPolling(5000).pipe(tap(), takeUntil(this.stopPolling$)).subscribe(); } } diff --git a/frontend/src/app/matching/matching.component.html b/frontend/src/app/matching/matching.component.html index 22082f4978..0e1bd3b545 100644 --- a/frontend/src/app/matching/matching.component.html +++ b/frontend/src/app/matching/matching.component.html @@ -64,6 +64,8 @@

Matching Criteria

(matchFailed)="onMatchFailed()" (dialogClose)="onMatchDialogClose()" /> diff --git a/frontend/src/app/matching/matching.component.ts b/frontend/src/app/matching/matching.component.ts index 2adddb9c42..35808e46ad 100644 --- a/frontend/src/app/matching/matching.component.ts +++ b/frontend/src/app/matching/matching.component.ts @@ -110,7 +110,7 @@ export class MatchingComponent implements OnInit { } onMatch() { - const matchRequest: MatchRequest = {topics: this.topics, difficulty: this.difficulty} + const matchRequest: MatchRequest = { topics: this.topics, difficulty: this.difficulty }; console.log(matchRequest); this.matchService.createMatchRequest(matchRequest).subscribe({ next: response => { diff --git a/frontend/src/app/matching/retry-matching/retry-matching.component.ts b/frontend/src/app/matching/retry-matching/retry-matching.component.ts index 5e5b26030e..4a7ccfd9e6 100644 --- a/frontend/src/app/matching/retry-matching/retry-matching.component.ts +++ b/frontend/src/app/matching/retry-matching/retry-matching.component.ts @@ -1,6 +1,9 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { ButtonModule } from 'primeng/button'; import { DialogModule } from 'primeng/dialog'; +import { UserCriteria } from '../user-criteria.model'; +import { MatchService } from '../../../_services/match.service'; +import { MessageService } from 'primeng/api'; @Component({ selector: 'app-retry-matching', @@ -11,15 +14,38 @@ import { DialogModule } from 'primeng/dialog'; }) export class RetryMatchingComponent { @Input() isVisible = false; + @Input() userCriteria!: UserCriteria; + @Input() matchId!: string; @Output() dialogClose = new EventEmitter(); @Output() retryMatch = new EventEmitter(); + constructor( + private matchService: MatchService, + private messageService: MessageService, + ) {} + closeDialog() { this.dialogClose.emit(); } onRetryMatch() { - this.retryMatch.emit(); + this.matchService.updateMatchRequest(this.matchId, this.userCriteria).subscribe({ + next: response => { + console.log(response); + }, + error: () => { + this.messageService.add({ + severity: 'error', + summary: 'Error', + detail: `Something went wrong while matching.`, + life: 3000, + }); + this.closeDialog(); + }, + complete: () => { + this.retryMatch.emit(); + }, + }); } } diff --git a/services/match/src/utils/date.ts b/services/match/src/utils/date.ts index 9cb1ea938a..7206a3844f 100644 --- a/services/match/src/utils/date.ts +++ b/services/match/src/utils/date.ts @@ -1,3 +1,3 @@ export const oneMinuteAgo = (): Date => { - return new Date(Date.now() - 60 * 1000); + return new Date(Date.now() - 20 * 1000); }; From 10a5c4475ea98c4333b1d5e355442d4d58077393 Mon Sep 17 00:00:00 2001 From: Samuel Lim Date: Sat, 19 Oct 2024 22:27:45 +0800 Subject: [PATCH 12/28] Fix dependency issues --- compose.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/compose.yml b/compose.yml index 33b7490d25..e165caa306 100644 --- a/compose.yml +++ b/compose.yml @@ -16,6 +16,10 @@ services: - 8080:8080 volumes: - ./nginx/default.conf:/etc/nginx/conf.d/default.conf + depends_on: + - question + - user + - match networks: - gateway-network @@ -90,6 +94,10 @@ services: DB_LOCAL_URI: ${MATCH_DB_LOCAL_URI} DB_USERNAME: ${MATCH_DB_USERNAME} DB_PASSWORD: ${MATCH_DB_PASSWORD} + depends_on: + match-broker: + condition: service_healthy + networks: - gateway-network - match-db-network @@ -113,6 +121,11 @@ services: image: rabbitmq:4.0.2 networks: - match-db-network + healthcheck: + test: rabbitmq-diagnostics check_port_connectivity + interval: 5s + timeout: 15s + retries: 20 volumes: question-db: From 331fc33d9891c66aab70c1920e7e02a2fd2c2d08 Mon Sep 17 00:00:00 2001 From: Samuel Lim Date: Sat, 19 Oct 2024 22:28:47 +0800 Subject: [PATCH 13/28] Ensure logging for each match request --- services/match/src/events/consumer.ts | 13 ++++++++++++- services/match/src/middleware/jwt.ts | 1 - services/match/src/models/repository.ts | 4 ++++ services/match/src/utils/logger.ts | 17 +++++++++++++++++ 4 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 services/match/src/utils/logger.ts diff --git a/services/match/src/events/consumer.ts b/services/match/src/events/consumer.ts index 7e59442c79..ddf4fa9e1c 100644 --- a/services/match/src/events/consumer.ts +++ b/services/match/src/events/consumer.ts @@ -1,6 +1,7 @@ import { MatchRequestModel } from '../models/matchRequestModel'; import { CollabCreatedEvent, MatchUpdatedEvent } from '../types/event'; import { oneMinuteAgo } from '../utils/date'; +import { logQueueStatus } from '../utils/logger'; import messageBroker from './broker'; import { produceMatchFound } from './producer'; import { Queues } from './queues'; @@ -12,6 +13,9 @@ async function consumeMatchUpdated(msg: MatchUpdatedEvent) { difficulty, } = msg; + console.log("Attempting to find match for user", username); + await logQueueStatus(); + const match = await MatchRequestModel.findOneAndUpdate( { _id: { $ne: requestId }, @@ -24,9 +28,16 @@ async function consumeMatchUpdated(msg: MatchUpdatedEvent) { { $set: { pairId: requestId } }, ); - if (!match) return; + if (!match) { + console.log("Unable to find match for user", username); + await logQueueStatus(); + return; + }; await MatchRequestModel.findByIdAndUpdate(requestId, { $set: { pairId: match.id } }); + console.log("Succesfully found match for user", username); + await logQueueStatus(); + const user1 = { id: userId, username, requestId }; const user2 = { id: match.userId, username: match.username, requestId: match.id }; const commonTopics = topics.filter(topic => match.topics.includes(topic)); diff --git a/services/match/src/middleware/jwt.ts b/services/match/src/middleware/jwt.ts index 3fb04abf34..371c9c31e2 100644 --- a/services/match/src/middleware/jwt.ts +++ b/services/match/src/middleware/jwt.ts @@ -10,7 +10,6 @@ export async function verifyAccessToken(req: Request, res: Response, next: NextF return; } try { - console.log(authHeader); const response = await axios.get('http://user:8082/auth/verify-token', { headers: { authorization: authHeader }, }); diff --git a/services/match/src/models/repository.ts b/services/match/src/models/repository.ts index 85fcc4b144..aec6c0b2f3 100644 --- a/services/match/src/models/repository.ts +++ b/services/match/src/models/repository.ts @@ -35,3 +35,7 @@ export async function findMatchRequestAndDelete(id: string, userId: string) { export async function findMatchRequest(id: string, userId: string) { return await MatchRequestModel.findOne({ _id: id, userId }); } + +export async function retrieveAllMatchRequests() { + return await MatchRequestModel.find({updatedAt: { $gte: oneMinuteAgo() }}); +} diff --git a/services/match/src/utils/logger.ts b/services/match/src/utils/logger.ts new file mode 100644 index 0000000000..e05baebe13 --- /dev/null +++ b/services/match/src/utils/logger.ts @@ -0,0 +1,17 @@ +import { MatchRequest } from "../models/matchRequestModel"; +import { retrieveAllMatchRequests } from "../models/repository"; + + +export async function logQueueStatus(): Promise { + const currentRequests = await retrieveAllMatchRequests() as MatchRequest[]; + currentRequests.sort((r1, r2) => r1.updatedAt > r2.updatedAt ? 1 : -1); + + const queueStatus = currentRequests.map(r => ({ + username: r.username, + topics: r.topics, + difficulty: r.difficulty, + updatedAt: r.updatedAt, + })); + + console.log('Current Queue Status: ', queueStatus); +} \ No newline at end of file From 9f9dee0c589ee2898cc4ccef9a10c2fade19b64b Mon Sep 17 00:00:00 2001 From: Samuel Lim Date: Sat, 19 Oct 2024 22:37:02 +0800 Subject: [PATCH 14/28] Fix recursive call to closeDialog() Co-authored-by: Bryan Lee --- .../src/app/matching/finding-match/finding-match.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/app/matching/finding-match/finding-match.component.ts b/frontend/src/app/matching/finding-match/finding-match.component.ts index 0973bebd7b..19c25f6ade 100644 --- a/frontend/src/app/matching/finding-match/finding-match.component.ts +++ b/frontend/src/app/matching/finding-match/finding-match.component.ts @@ -48,7 +48,6 @@ export class FindingMatchComponent { detail: `Something went wrong while cancelling your match.`, life: 3000, }); - this.closeDialog(); }, complete: () => { this.dialogClose.emit(); From db84f12e00b9dbb4585d75eb987a86f91d2c3b1f Mon Sep 17 00:00:00 2001 From: Samuel Lim Date: Sat, 19 Oct 2024 22:53:10 +0800 Subject: [PATCH 15/28] Implement match frontend timer * Fix one minute being defined as 20 seconds on match service Co-authored-by: Bryan Lee --- .../finding-match.component.html | 4 ++++ .../finding-match/finding-match.component.ts | 23 +++++++++++++++++++ frontend/tsconfig.app.json | 2 +- services/match/src/utils/date.ts | 2 +- 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/matching/finding-match/finding-match.component.html b/frontend/src/app/matching/finding-match/finding-match.component.html index fbd8656a19..d0f954a7ae 100644 --- a/frontend/src/app/matching/finding-match/finding-match.component.html +++ b/frontend/src/app/matching/finding-match/finding-match.component.html @@ -11,6 +11,10 @@ @if (isFindingMatch) {

Finding a Match...

+
+ +

Time Left: {{ timeLeft }}

+
} @else {

Match Found!

diff --git a/frontend/src/app/matching/finding-match/finding-match.component.ts b/frontend/src/app/matching/finding-match/finding-match.component.ts index 19c25f6ade..e40912fe5c 100644 --- a/frontend/src/app/matching/finding-match/finding-match.component.ts +++ b/frontend/src/app/matching/finding-match/finding-match.component.ts @@ -27,8 +27,10 @@ export class FindingMatchComponent { @Output() matchSuccess = new EventEmitter(); isFindingMatch = true; + timeLeft!: number; matchPoll!: Subscription; + interval!: NodeJS.Timeout; constructor( private matchService: MatchService, @@ -36,6 +38,7 @@ export class FindingMatchComponent { ) {} closeDialog() { + this.stopTimer(); this.matchPoll.unsubscribe(); this.matchService.deleteMatchRequest(this.matchId).subscribe({ next: response => { @@ -56,10 +59,12 @@ export class FindingMatchComponent { } onMatchFailed() { + this.stopTimer(); this.matchFailed.emit(); } onMatchSuccess() { + this.stopTimer(); this.isFindingMatch = false; this.matchSuccess.emit(); // Possible to handle routing to workspace here. @@ -100,7 +105,25 @@ export class FindingMatchComponent { return timer(0, interval).pipe(switchMap(() => this.requestData())); } + startTimer(time: number) { + this.timeLeft = time; + this.interval = setInterval(() => { + if (this.timeLeft > 0) { + this.timeLeft--; + } else { + this.stopTimer(); + } + }, 1000); + } + + stopTimer() { + if (this.interval) { + clearInterval(this.interval); + } + } + onDialogShow() { + this.startTimer(60); this.matchPoll = this.startPolling(5000).pipe(tap(), takeUntil(this.stopPolling$)).subscribe(); } } diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index 3775b37e3b..e2722e2f6d 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -4,7 +4,7 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", - "types": [] + "types": ["node"] }, "files": [ "src/main.ts" diff --git a/services/match/src/utils/date.ts b/services/match/src/utils/date.ts index 7206a3844f..9cb1ea938a 100644 --- a/services/match/src/utils/date.ts +++ b/services/match/src/utils/date.ts @@ -1,3 +1,3 @@ export const oneMinuteAgo = (): Date => { - return new Date(Date.now() - 20 * 1000); + return new Date(Date.now() - 60 * 1000); }; From 7422aa915ca154cff374b6565477a6b36f9ed232 Mon Sep 17 00:00:00 2001 From: Samuel Lim Date: Sat, 19 Oct 2024 23:12:30 +0800 Subject: [PATCH 16/28] Integrate navbar --- frontend/src/app/matching/matching.component.css | 3 +-- frontend/src/app/navigation-bar/navigation-bar.component.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/matching/matching.component.css b/frontend/src/app/matching/matching.component.css index 4d572d4c2f..de9a26844b 100644 --- a/frontend/src/app/matching/matching.component.css +++ b/frontend/src/app/matching/matching.component.css @@ -3,8 +3,7 @@ justify-content: center; align-items: center; width: 100%; - height: 100%; - padding: 1rem; + padding: calc(88px + 1rem); } .form-wrapper { diff --git a/frontend/src/app/navigation-bar/navigation-bar.component.ts b/frontend/src/app/navigation-bar/navigation-bar.component.ts index 8857a2bbcb..3059666d37 100644 --- a/frontend/src/app/navigation-bar/navigation-bar.component.ts +++ b/frontend/src/app/navigation-bar/navigation-bar.component.ts @@ -39,7 +39,7 @@ export class NavigationBarComponent implements OnInit { { label: 'Find Match', icon: 'pi pi-users', - // route: '', + route: '/matching', class: 'p-submenu-list', }, { From ede8065cbe88c2a391129ceee4d5bc120676f1bb Mon Sep 17 00:00:00 2001 From: Samuel Lim Date: Sun, 20 Oct 2024 12:52:34 +0800 Subject: [PATCH 17/28] Minor code clean up --- services/match/src/events/consumer.ts | 8 ++++---- services/match/src/models/repository.ts | 2 +- services/match/src/utils/logger.ts | 11 ++++------- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/services/match/src/events/consumer.ts b/services/match/src/events/consumer.ts index ddf4fa9e1c..f4eed6a296 100644 --- a/services/match/src/events/consumer.ts +++ b/services/match/src/events/consumer.ts @@ -13,7 +13,7 @@ async function consumeMatchUpdated(msg: MatchUpdatedEvent) { difficulty, } = msg; - console.log("Attempting to find match for user", username); + console.log('Attempting to find match for user', username); await logQueueStatus(); const match = await MatchRequestModel.findOneAndUpdate( @@ -29,13 +29,13 @@ async function consumeMatchUpdated(msg: MatchUpdatedEvent) { ); if (!match) { - console.log("Unable to find match for user", username); + console.log('Unable to find match for user', username); await logQueueStatus(); return; - }; + } await MatchRequestModel.findByIdAndUpdate(requestId, { $set: { pairId: match.id } }); - console.log("Succesfully found match for user", username); + console.log('Succesfully found match for user', username); await logQueueStatus(); const user1 = { id: userId, username, requestId }; diff --git a/services/match/src/models/repository.ts b/services/match/src/models/repository.ts index aec6c0b2f3..fa8e84c3a9 100644 --- a/services/match/src/models/repository.ts +++ b/services/match/src/models/repository.ts @@ -37,5 +37,5 @@ export async function findMatchRequest(id: string, userId: string) { } export async function retrieveAllMatchRequests() { - return await MatchRequestModel.find({updatedAt: { $gte: oneMinuteAgo() }}); + return await MatchRequestModel.find({ pairId: null, updatedAt: { $gte: oneMinuteAgo() } }).sort({ updatedAt: 1 }); } diff --git a/services/match/src/utils/logger.ts b/services/match/src/utils/logger.ts index e05baebe13..aadd5bb638 100644 --- a/services/match/src/utils/logger.ts +++ b/services/match/src/utils/logger.ts @@ -1,11 +1,8 @@ -import { MatchRequest } from "../models/matchRequestModel"; -import { retrieveAllMatchRequests } from "../models/repository"; - +import { MatchRequest } from '../models/matchRequestModel'; +import { retrieveAllMatchRequests } from '../models/repository'; export async function logQueueStatus(): Promise { - const currentRequests = await retrieveAllMatchRequests() as MatchRequest[]; - currentRequests.sort((r1, r2) => r1.updatedAt > r2.updatedAt ? 1 : -1); - + const currentRequests = (await retrieveAllMatchRequests()) as MatchRequest[]; const queueStatus = currentRequests.map(r => ({ username: r.username, topics: r.topics, @@ -14,4 +11,4 @@ export async function logQueueStatus(): Promise { })); console.log('Current Queue Status: ', queueStatus); -} \ No newline at end of file +} From ea0e2401f7a8ee851197b512f7b3acd5278b1c4c Mon Sep 17 00:00:00 2001 From: Samuel Lim Date: Sun, 20 Oct 2024 12:55:43 +0800 Subject: [PATCH 18/28] Reroute login to /matching --- frontend/src/app/account/login.component.ts | 6 ++---- frontend/src/app/account/register.component.ts | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/account/login.component.ts b/frontend/src/app/account/login.component.ts index 8bc426b81c..8733b14b6c 100644 --- a/frontend/src/app/account/login.component.ts +++ b/frontend/src/app/account/login.component.ts @@ -26,7 +26,7 @@ export class LoginComponent { ) { //redirect to home if already logged in if (this.authenticationService.userValue) { - this.router.navigate(['/']); + this.router.navigate(['/matching']); } } @@ -44,9 +44,7 @@ export class LoginComponent { // authenticationService returns an observable that we can subscribe to this.authenticationService.login(this.userForm.username, this.userForm.password).subscribe({ next: () => { - // get return url from route parameters or default to '/' - const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/'; - this.router.navigate([returnUrl]); + this.router.navigate(['/matching']); }, error: error => { this.isProcessingLogin = false; diff --git a/frontend/src/app/account/register.component.ts b/frontend/src/app/account/register.component.ts index ee67c228e4..908716bbe0 100644 --- a/frontend/src/app/account/register.component.ts +++ b/frontend/src/app/account/register.component.ts @@ -41,7 +41,7 @@ export class RegisterComponent { ) { // redirect to home if already logged in if (this.authenticationService.userValue) { - this.router.navigate(['/']); + this.router.navigate(['/matching']); } } @@ -99,9 +99,7 @@ export class RegisterComponent { .pipe() .subscribe({ next: () => { - // get return url from route parameters or default to '/' - const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/'; - this.router.navigate([returnUrl]); + this.router.navigate(['/matching']); }, // error handling for registration because we assume there will be no errors with auto login error: error => { From b59f678f4e1962d6cf750972281f754a88489c3d Mon Sep 17 00:00:00 2001 From: McNaBry Date: Sun, 20 Oct 2024 16:09:56 +0800 Subject: [PATCH 19/28] Clean up match frontend code * Reorder function declarations for finding match dialog * Remove unecessary comments --- .../finding-match/finding-match.component.ts | 60 +++++++++---------- .../src/app/matching/matching.component.ts | 1 - 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/frontend/src/app/matching/finding-match/finding-match.component.ts b/frontend/src/app/matching/finding-match/finding-match.component.ts index e40912fe5c..0abc35f175 100644 --- a/frontend/src/app/matching/finding-match/finding-match.component.ts +++ b/frontend/src/app/matching/finding-match/finding-match.component.ts @@ -27,37 +27,18 @@ export class FindingMatchComponent { @Output() matchSuccess = new EventEmitter(); isFindingMatch = true; + timeLeft!: number; + interval!: NodeJS.Timeout; matchPoll!: Subscription; - interval!: NodeJS.Timeout; + stopPolling$ = new EventEmitter(); constructor( private matchService: MatchService, private messageService: MessageService, ) {} - closeDialog() { - this.stopTimer(); - this.matchPoll.unsubscribe(); - this.matchService.deleteMatchRequest(this.matchId).subscribe({ - next: response => { - console.log(response); - }, - error: () => { - this.messageService.add({ - severity: 'error', - summary: 'Error', - detail: `Something went wrong while cancelling your match.`, - life: 3000, - }); - }, - complete: () => { - this.dialogClose.emit(); - }, - }); - } - onMatchFailed() { this.stopTimer(); this.matchFailed.emit(); @@ -70,7 +51,14 @@ export class FindingMatchComponent { // Possible to handle routing to workspace here. } - stopPolling$ = new EventEmitter(); + onDialogShow() { + this.startTimer(60); + this.matchPoll = this.startPolling(5000).pipe(tap(), takeUntil(this.stopPolling$)).subscribe(); + } + + startPolling(interval: number): Observable { + return timer(0, interval).pipe(switchMap(() => this.requestData())); + } requestData() { return this.matchService.retrieveMatchRequest(this.matchId).pipe( @@ -101,8 +89,25 @@ export class FindingMatchComponent { ); } - startPolling(interval: number): Observable { - return timer(0, interval).pipe(switchMap(() => this.requestData())); + closeDialog() { + this.stopTimer(); + this.matchPoll.unsubscribe(); + this.matchService.deleteMatchRequest(this.matchId).subscribe({ + next: response => { + console.log(response); + }, + error: () => { + this.messageService.add({ + severity: 'error', + summary: 'Error', + detail: `Something went wrong while cancelling your match.`, + life: 3000, + }); + }, + complete: () => { + this.dialogClose.emit(); + }, + }); } startTimer(time: number) { @@ -121,9 +126,4 @@ export class FindingMatchComponent { clearInterval(this.interval); } } - - onDialogShow() { - this.startTimer(60); - this.matchPoll = this.startPolling(5000).pipe(tap(), takeUntil(this.stopPolling$)).subscribe(); - } } diff --git a/frontend/src/app/matching/matching.component.ts b/frontend/src/app/matching/matching.component.ts index 35808e46ad..5c25edc30f 100644 --- a/frontend/src/app/matching/matching.component.ts +++ b/frontend/src/app/matching/matching.component.ts @@ -133,7 +133,6 @@ export class MatchingComponent implements OnInit { onRetryMatchRequest() { this.isMatchFailed = false; this.isProcessingMatch = true; - // TODO: Add API request to retry matching. } onMatchDialogClose() { From bf06529874776c9c787a3764597cf5ed303aa919 Mon Sep 17 00:00:00 2001 From: McNaBry Date: Sun, 20 Oct 2024 16:14:27 +0800 Subject: [PATCH 20/28] Add loading state for start matching button when initiating a match --- frontend/src/app/matching/matching.component.html | 4 ++-- frontend/src/app/matching/matching.component.ts | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/matching/matching.component.html b/frontend/src/app/matching/matching.component.html index 0e1bd3b545..7154906f88 100644 --- a/frontend/src/app/matching/matching.component.html +++ b/frontend/src/app/matching/matching.component.html @@ -49,8 +49,8 @@

Matching Criteria

diff --git a/frontend/src/app/matching/matching.component.ts b/frontend/src/app/matching/matching.component.ts index 5c25edc30f..4df3e85a62 100644 --- a/frontend/src/app/matching/matching.component.ts +++ b/frontend/src/app/matching/matching.component.ts @@ -17,6 +17,7 @@ import { Difficulty } from './user-criteria.model'; import { ToastModule } from 'primeng/toast'; import { MatchService } from '../../_services/match.service'; import { MatchRequest } from './match.model'; +import { delay } from 'rxjs'; @Component({ selector: 'app-matching', @@ -45,6 +46,7 @@ export class MatchingComponent implements OnInit { availableDifficulties = ['Easy', 'Medium', 'Hard']; isLoadingTopics = true; + isInitiatingMatch = false; isProcessingMatch = false; isMatchFailed = false; matchId!: string; @@ -110,6 +112,7 @@ export class MatchingComponent implements OnInit { } onMatch() { + this.isInitiatingMatch = true; const matchRequest: MatchRequest = { topics: this.topics, difficulty: this.difficulty }; console.log(matchRequest); this.matchService.createMatchRequest(matchRequest).subscribe({ @@ -120,6 +123,7 @@ export class MatchingComponent implements OnInit { this.onErrorReceive('Failed to create a match. Please try again later.'); }, complete: () => { + this.isInitiatingMatch = false; this.isProcessingMatch = true; }, }); From d980262b2035a1187e0281dc3897f5b6d311603c Mon Sep 17 00:00:00 2001 From: McNaBry Date: Sun, 20 Oct 2024 16:21:11 +0800 Subject: [PATCH 21/28] Adjust height of matching component --- frontend/src/app/matching/matching.component.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/matching/matching.component.css b/frontend/src/app/matching/matching.component.css index de9a26844b..f24d5cbe45 100644 --- a/frontend/src/app/matching/matching.component.css +++ b/frontend/src/app/matching/matching.component.css @@ -3,7 +3,8 @@ justify-content: center; align-items: center; width: 100%; - padding: calc(88px + 1rem); + min-height: calc(100vh - 160px); + padding: 1rem; } .form-wrapper { From 516214bcdb363254c56bb876168f472211ef6895 Mon Sep 17 00:00:00 2001 From: Samuel Lim Date: Sun, 20 Oct 2024 16:26:10 +0800 Subject: [PATCH 22/28] Extend healthcheck time for broker --- compose.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/compose.yml b/compose.yml index e165caa306..0c6d6d253f 100644 --- a/compose.yml +++ b/compose.yml @@ -97,7 +97,6 @@ services: depends_on: match-broker: condition: service_healthy - networks: - gateway-network - match-db-network @@ -123,9 +122,10 @@ services: - match-db-network healthcheck: test: rabbitmq-diagnostics check_port_connectivity - interval: 5s - timeout: 15s - retries: 20 + interval: 30s + timeout: 30s + retries: 10 + start_period: 30s volumes: question-db: From 2e005c65b6b90d01de07e49f3e99a33eece27540 Mon Sep 17 00:00:00 2001 From: Samuel Lim Date: Sun, 20 Oct 2024 16:39:42 +0800 Subject: [PATCH 23/28] Refactor match service: PUT /request * Simplified the endpoint to solely reset the validity of the match request, removing any unnecessary code. --- frontend/src/_services/match.service.ts | 17 +++++++++++++++-- .../src/app/matching/matching.component.html | 3 +-- .../src/app/matching/matching.component.ts | 1 - .../retry-matching/retry-matching.component.ts | 7 +------ .../src/controllers/matchRequestController.ts | 18 +++++++++--------- services/match/src/models/repository.ts | 7 +++++-- .../src/validation/matchRequestValidation.ts | 5 ----- 7 files changed, 31 insertions(+), 27 deletions(-) diff --git a/frontend/src/_services/match.service.ts b/frontend/src/_services/match.service.ts index ba0c21efad..47530fa3c8 100644 --- a/frontend/src/_services/match.service.ts +++ b/frontend/src/_services/match.service.ts @@ -19,18 +19,31 @@ export class MatchService extends ApiService { super(); } + /** + * Creates a match request with the provided details. The match request will + * be valid for one minute. + */ createMatchRequest(matchRequest: MatchRequest) { return this.http.post(this.apiUrl, matchRequest, this.httpOptions); } + /** + * Retrieves the match request and its current status + */ retrieveMatchRequest(id: string) { return this.http.get(this.apiUrl + '/' + id); } - updateMatchRequest(id: string, matchRequest: MatchRequest) { - return this.http.put(this.apiUrl + '/' + id, matchRequest, this.httpOptions); + /** + * Refreshes the match request, effectively resetting its validity to one minute. + */ + updateMatchRequest(id: string) { + return this.http.put(this.apiUrl + '/' + id, {}, this.httpOptions); } + /** + * Deletes the match request + */ deleteMatchRequest(id: string) { return this.http.delete(this.apiUrl + '/' + id); } diff --git a/frontend/src/app/matching/matching.component.html b/frontend/src/app/matching/matching.component.html index 7154906f88..3cbd2d22c2 100644 --- a/frontend/src/app/matching/matching.component.html +++ b/frontend/src/app/matching/matching.component.html @@ -49,7 +49,7 @@

Matching Criteria

Matching Criteria (matchFailed)="onMatchFailed()" (dialogClose)="onMatchDialogClose()" /> (); @@ -30,10 +28,7 @@ export class RetryMatchingComponent { } onRetryMatch() { - this.matchService.updateMatchRequest(this.matchId, this.userCriteria).subscribe({ - next: response => { - console.log(response); - }, + this.matchService.updateMatchRequest(this.matchId).subscribe({ error: () => { this.messageService.add({ severity: 'error', diff --git a/services/match/src/controllers/matchRequestController.ts b/services/match/src/controllers/matchRequestController.ts index 4f78bde39e..fbaef53617 100644 --- a/services/match/src/controllers/matchRequestController.ts +++ b/services/match/src/controllers/matchRequestController.ts @@ -1,7 +1,7 @@ import { Request, Response } from 'express'; import { handleBadRequest, handleInternalError, handleNotFound, handleSuccess } from '../utils/responses'; import { isValidObjectId } from 'mongoose'; -import { createMatchRequestSchema, updateMatchRequestSchema } from '../validation/matchRequestValidation'; +import { createMatchRequestSchema } from '../validation/matchRequestValidation'; import { createMatchRequest as _createMatchRequest, findMatchRequestAndUpdate, @@ -40,20 +40,20 @@ export const createMatchRequest = async (req: Request, res: Response) => { * @param res */ export const updateMatchRequest = async (req: Request, res: Response) => { - const { error, value } = updateMatchRequestSchema.validate(req.body); - if (error) { - return handleBadRequest(res, error.message); - } - const id = req.params.id; const { id: userId, username } = req.user; - const { topics, difficulty } = value; try { - const matchRequest = await findMatchRequestAndUpdate(id, userId, topics, difficulty); + const matchRequest = await findMatchRequestAndUpdate(id, userId); if (!matchRequest) { return handleNotFound(res, `Request ${id} not found`); } - await produceMatchUpdatedRequest(matchRequest.id, userId, username, topics, difficulty); + await produceMatchUpdatedRequest( + matchRequest.id, + userId, + username, + matchRequest.topics, + matchRequest.difficulty, + ); handleSuccess(res, 201, 'Match request update successfully', matchRequest); } catch (error) { console.error('Error in updateMatchRequest:', error); diff --git a/services/match/src/models/repository.ts b/services/match/src/models/repository.ts index fa8e84c3a9..dc6f5d2650 100644 --- a/services/match/src/models/repository.ts +++ b/services/match/src/models/repository.ts @@ -24,8 +24,11 @@ export async function createMatchRequest(userId: string, username: string, topic return await new MatchRequestModel({ userId, username, topics, difficulty }).save(); } -export async function findMatchRequestAndUpdate(id: string, userId: string, topics: string[], difficulty: Difficulty) { - return await MatchRequestModel.findOneAndUpdate({ _id: id, userId }, { $set: { topics, difficulty } }); +export async function findMatchRequestAndUpdate(id: string, userId: string) { + return await MatchRequestModel.findOneAndUpdate( + { _id: id, userId, pairId: null }, + { $set: { updatedAt: Date.now() } }, + ); } export async function findMatchRequestAndDelete(id: string, userId: string) { diff --git a/services/match/src/validation/matchRequestValidation.ts b/services/match/src/validation/matchRequestValidation.ts index 6bcd970771..9c3aa4d767 100644 --- a/services/match/src/validation/matchRequestValidation.ts +++ b/services/match/src/validation/matchRequestValidation.ts @@ -7,8 +7,3 @@ export const createMatchRequestSchema = Joi.object({ .valid(...Object.values(Difficulty)) .required(), }); - -export const updateMatchRequestSchema = Joi.object({ - topics: Joi.array().items(Joi.string()).min(1), - difficulty: Joi.string().valid(...Object.values(Difficulty)), -}).min(1); From 4b54aa1933b2c84fdf051517a4e6969ca704f5e4 Mon Sep 17 00:00:00 2001 From: Samuel Lim Date: Sun, 20 Oct 2024 16:59:55 +0800 Subject: [PATCH 24/28] Move match DB methods into repository.ts --- services/match/src/events/consumer.ts | 29 +++++++++--------- services/match/src/models/repository.ts | 39 +++++++++++++++++++++---- 2 files changed, 47 insertions(+), 21 deletions(-) diff --git a/services/match/src/events/consumer.ts b/services/match/src/events/consumer.ts index f4eed6a296..5e3173f1bd 100644 --- a/services/match/src/events/consumer.ts +++ b/services/match/src/events/consumer.ts @@ -1,11 +1,18 @@ -import { MatchRequestModel } from '../models/matchRequestModel'; +import { + findAndAssignCollab, + findMatchRequestAndAssignPair, + findMatchRequestByIdAndAssignPair, +} from '../models/repository'; import { CollabCreatedEvent, MatchUpdatedEvent } from '../types/event'; -import { oneMinuteAgo } from '../utils/date'; import { logQueueStatus } from '../utils/logger'; import messageBroker from './broker'; import { produceMatchFound } from './producer'; import { Queues } from './queues'; +function findCommonTopics(topics1: string[], topics2: string[]) { + return topics1.filter(topic => topics2.includes(topic)); +} + async function consumeMatchUpdated(msg: MatchUpdatedEvent) { const { user: { id: userId, username, requestId }, @@ -16,37 +23,27 @@ async function consumeMatchUpdated(msg: MatchUpdatedEvent) { console.log('Attempting to find match for user', username); await logQueueStatus(); - const match = await MatchRequestModel.findOneAndUpdate( - { - _id: { $ne: requestId }, - userId: { $ne: userId }, - pairId: null, - topics: { $in: topics }, - difficulty, - updatedAt: { $gte: oneMinuteAgo() }, - }, - { $set: { pairId: requestId } }, - ); + const match = await findMatchRequestAndAssignPair(requestId, userId, topics, difficulty); if (!match) { console.log('Unable to find match for user', username); await logQueueStatus(); return; } - await MatchRequestModel.findByIdAndUpdate(requestId, { $set: { pairId: match.id } }); + await findMatchRequestByIdAndAssignPair(requestId, match.id); console.log('Succesfully found match for user', username); await logQueueStatus(); const user1 = { id: userId, username, requestId }; const user2 = { id: match.userId, username: match.username, requestId: match.id }; - const commonTopics = topics.filter(topic => match.topics.includes(topic)); + const commonTopics = findCommonTopics(topics, match.topics); await produceMatchFound(user1, user2, commonTopics, difficulty); } async function consumeCollabCreated(msg: CollabCreatedEvent) { const { requestId1, requestId2, collabId } = msg; - await MatchRequestModel.updateMany({ _id: { $in: [requestId1, requestId2] } }, { $set: { collabId } }); + await findAndAssignCollab(requestId1, requestId2, collabId); } export async function initializeConsumers() { diff --git a/services/match/src/models/repository.ts b/services/match/src/models/repository.ts index dc6f5d2650..e8ac017086 100644 --- a/services/match/src/models/repository.ts +++ b/services/match/src/models/repository.ts @@ -1,7 +1,9 @@ -import mongoose from 'mongoose'; +import mongoose, { Types } from 'mongoose'; import { Difficulty, MatchRequestModel } from './matchRequestModel'; import { oneMinuteAgo } from '../utils/date'; +type IdType = string | Types.ObjectId; + export async function connectToDB() { const mongoURI = process.env.NODE_ENV === 'production' ? process.env.DB_CLOUD_URI : process.env.DB_LOCAL_URI; @@ -20,25 +22,52 @@ export async function connectToDB() { }); } -export async function createMatchRequest(userId: string, username: string, topics: string[], difficulty: Difficulty) { +export async function createMatchRequest(userId: IdType, username: string, topics: string[], difficulty: Difficulty) { return await new MatchRequestModel({ userId, username, topics, difficulty }).save(); } -export async function findMatchRequestAndUpdate(id: string, userId: string) { +export async function findMatchRequestAndUpdate(id: IdType, userId: IdType) { return await MatchRequestModel.findOneAndUpdate( { _id: id, userId, pairId: null }, { $set: { updatedAt: Date.now() } }, ); } -export async function findMatchRequestAndDelete(id: string, userId: string) { +export async function findMatchRequestAndDelete(id: IdType, userId: IdType) { return await MatchRequestModel.findOneAndDelete({ _id: id, userId, updatedAt: { $gte: oneMinuteAgo() } }); } -export async function findMatchRequest(id: string, userId: string) { +export async function findMatchRequest(id: IdType, userId: IdType) { return await MatchRequestModel.findOne({ _id: id, userId }); } export async function retrieveAllMatchRequests() { return await MatchRequestModel.find({ pairId: null, updatedAt: { $gte: oneMinuteAgo() } }).sort({ updatedAt: 1 }); } + +export async function findMatchRequestAndAssignPair( + requestId: IdType, + userId: IdType, + topics: string[], + difficulty: Difficulty, +) { + return await MatchRequestModel.findOneAndUpdate( + { + _id: { $ne: requestId }, + userId: { $ne: userId }, + pairId: null, + topics: { $in: topics }, + difficulty, + updatedAt: { $gte: oneMinuteAgo() }, + }, + { $set: { pairId: requestId } }, + ); +} + +export async function findMatchRequestByIdAndAssignPair(id: IdType, pairId: IdType) { + await MatchRequestModel.findByIdAndUpdate(id, { $set: { pairId } }); +} + +export async function findAndAssignCollab(requestId1: IdType, requestId2: IdType, collabId: IdType) { + await MatchRequestModel.updateMany({ _id: { $in: [requestId1, requestId2] } }, { $set: { collabId } }); +} From bf2d6de65787386c8f0ac019d66dcd0a0430d809 Mon Sep 17 00:00:00 2001 From: Samuel Lim Date: Sun, 20 Oct 2024 18:31:41 +0800 Subject: [PATCH 25/28] Add Match Service README * Fix minor typos * Fix DB calls not returning updated objects --- services/match/README.md | 362 ++++++++++++++++++ .../src/controllers/matchRequestController.ts | 2 +- services/match/src/models/repository.ts | 4 +- 3 files changed, 366 insertions(+), 2 deletions(-) diff --git a/services/match/README.md b/services/match/README.md index e69de29bb2..f352e82881 100644 --- a/services/match/README.md +++ b/services/match/README.md @@ -0,0 +1,362 @@ +# Match Service User Guide + +## Pre-requisites + +1. Run the following command to create the `.env` files at the root directory: + +```cmd +cp .env.sample .env +``` + +2. After setting up the .env files, build the Docker images and start the containers using the following command: + +```cmd +docker compose build +docker compose up -d +``` + +3. To stop and remove the containers and associated volumes, use the following command: + +```cmd +docker compose down -v +``` + +## Endpoints + +### Create Match Request + +- This endpoint creates a new match request. The match request will remain valid for 1 minute. +- **HTTP Method**: `POST` +- **Endpoint**: http://localhost:8083/match/request +- **Body** + - `topics` (Required) - The topics associated with the match request. + - `difficulty` (Required) - The difficulty level of the match request. + + ```json + { + "topics[]": "Algorithms", + "topics[]": "Arrays", + "difficulty": "Hard" + } + ``` +- **Headers** + - `Authorization: Bearer ` (Required) + - The endpoint requires the user to include a JWT (JSON Web Token) in the HTTP Request Header for authentication and authorization. This token is generated during the authentication process (i.e., login) and contains information about the user's identity. The server verifies this token to ensure that the client is authorized to access the data. +- **Responses** + + | Response Code | Explanation | + |-----------------------------|---------------------------------------------| + | 201 (Created) | The match request is created successfully. | + | 400 (Bad Request) | Required fields are missing or invalid. | + | 500 (Internal Server Error) | Unexpected error in the database or server. | + + ```json + { + "status": "Success", + "message": "Match request created successfully", + "data": { + "userId": "6713d1778986bf54b29bd0f8", + "username": "user123", + "topics": [ + "Algorithms", + "Arrays" + ], + "difficulty": "Hard", + "_id": "6714d1806da8e6d033ac2be1", + "createdAt": "2024-10-20T09:46:40.877Z", + "updatedAt": "2024-10-20T09:46:40.877Z" + } + } + ``` + +--- + +### Update Match Request + +- This endpoint updates the validity of the existing match request to 1 minute. +- **HTTP Method**: `PUT` +- **Endpoint**: http://localhost:8083/match/request/{requestId} +- **Parameters** + - `requestId` (Required) - The request ID of the match request. + - Example: `http://localhost:8083/match/request/6706b5d706ecde0138ca27a9` +- **Headers** + - `Authorization: Bearer ` (Required) + - The endpoint requires the user to include a JWT (JSON Web Token) in the HTTP Request Header for authentication and authorization. This token is generated during the authentication process (i.e., login) and contains information about the user's identity. The server verifies this token to ensure that the client is authorized to access the data. +- **Responses** + + | Response Code | Explanation | + |-----------------------------|-----------------------------------------------------------------| + | 200 (OK) | The match request is updated successfully. | + | 404 (Not Found) | The match request with the specified `requestId` was not found. | + | 500 (Internal Server Error) | Unexpected error in the database or server. | + + ```json + { + "status": "Success", + "message": "Match request updated successfully", + "data": { + "userId": "6713d1778986bf54b29bd0f8", + "username": "user123", + "topics": [ + "Algorithms", + "Arrays" + ], + "difficulty": "Hard", + "_id": "6714d1806da8e6d033ac2be1", + "createdAt": "2024-10-20T09:46:40.877Z", + "updatedAt": "2024-10-20T09:49:57.332Z" + } + } + ``` + +--- + +### Delete Match Request + +- This endpoint deletes the match request. +- **HTTP Method**: `DELETE` +- **Endpoint**: http://localhost:8083/match/request/{requestId} +- **Parameters** + - `requestId` (Required) - The request ID of the match request. + - Example: `http://localhost:8083/match/request/6706b5d706ecde0138ca27a9` +- **Headers** + - `Authorization: Bearer ` (Required) + - The endpoint requires the user to include a JWT (JSON Web Token) in the HTTP Request Header for authentication and authorization. This token is generated during the authentication process (i.e., login) and contains information about the user's identity. The server verifies this token to ensure that the client is authorized to access the data. +- **Responses** + + | Response Code | Explanation | + |-----------------------------|-----------------------------------------------------------------| + | 200 (OK) | The match request is updated successfully. | + | 404 (Not Found) | The match request with the specified `requestId` was not found. | + | 500 (Internal Server Error) | Unexpected error in the database or server. | + + ```json + { + "status": "Success", + "message": "Match request deleted successfully", + "data": { + "userId": "6713d1778986bf54b29bd0f8", + "username": "user123", + "topics": [ + "Algorithms", + "Arrays" + ], + "difficulty": "Hard", + "_id": "6714d1806da8e6d033ac2be1", + "createdAt": "2024-10-20T09:46:40.877Z", + "updatedAt": "2024-10-20T09:49:57.332Z" + } + } + ``` + +--- + +### Retrieve Match Request + +- This endpoint retrieves the match request and its status. +- **HTTP Method**: `GET` +- **Endpoint**: http://localhost:8083/match/request/{requestId} +- **Parameters** + - `requestId` (Required) - The request ID of the match request. + - Example: `http://localhost:8083/match/request/6706b5d706ecde0138ca27a9` +- **Headers** + - `Authorization: Bearer ` (Required) + - The endpoint requires the user to include a JWT (JSON Web Token) in the HTTP Request Header for authentication and authorization. This token is generated during the authentication process (i.e., login) and contains information about the user's identity. The server verifies this token to ensure that the client is authorized to access the data. +- **Responses** + - `pairId` - The request ID of the matched user. This field is only present with a status of `MATCH_FOUND` or `COLLAB_CREATED`. + - `collabId` - The ID of the collaboration room. This field is only present with a status of `COLLAB_CREATED`. + + | Response Code | Explanation | + |-----------------------------|-----------------------------------------------------------------| + | 200 (OK) | The match request is updated successfully. | + | 404 (Not Found) | The match request with the specified `requestId` was not found. | + | 500 (Internal Server Error) | Unexpected error in the database or server. | + + | Response Status | Explanation | + |-----------------|--------------------------------------------------------------------------------| + | PENDING | Searching for a match. | + | TIME_OUT | Search time ended, and no match was found. | + | MATCH_FOUND | A match has been found and is awaiting the creation of the collaboration room. | + | COLLAB_CREATED | The collaboration room is now accessible with the provided `collabId`. | + + ```json + { + "status": "Success", + "message": "Match request retrieved successfully", + "data": { + "_id": "6714d1806da8e6d033ac2be1", + "userId": "6713d1778986bf54b29bd0f8", + "username": "user123", + "topics": [ + "Algorithms", + "Arrays" + ], + "difficulty": "Hard", + "createdAt": "2024-10-20T09:46:40.877Z", + "updatedAt": "2024-10-20T09:49:57.332Z", + "status": "PENDING" + } + } + ``` + + ```json + { + "status": "Success", + "message": "Match request retrieved successfully", + "data": { + "_id": "6714d1806da8e6d033ac2be1", + "userId": "6713d1778986bf54b29bd0f8", + "username": "user123", + "topics": [ + "Algorithms", + "Arrays" + ], + "difficulty": "Hard", + "createdAt": "2024-10-20T09:46:40.877Z", + "updatedAt": "2024-10-20T09:49:57.332Z", + "status": "TIME_OUT" + } + } + ``` + + ```json + { + "status": "Success", + "message": "Match request retrieved successfully", + "data": { + "_id": "6714d1806da8e6d033ac2be1", + "userId": "6713d1778986bf54b29bd0f8", + "username": "user123", + "topics": [ + "Algorithms", + "Arrays" + ], + "difficulty": "Hard", + "createdAt": "2024-10-20T09:46:40.877Z", + "updatedAt": "2024-10-20T09:49:57.332Z", + "pairId": "6714d49a028e8780b3a73a55", + "status": "MATCH_FOUND" + } + } + ``` + + ```json + { + "status": "Success", + "message": "Match request retrieved successfully", + "data": { + "_id": "6714d1806da8e6d033ac2be1", + "userId": "6713d1778986bf54b29bd0f8", + "username": "user123", + "topics": [ + "Algorithms", + "Arrays" + ], + "difficulty": "Hard", + "createdAt": "2024-10-20T09:46:40.877Z", + "updatedAt": "2024-10-20T09:49:57.332Z", + "pairId": "676e7c9a028e8780b3a73a58", + "status": "COLLAB_CREATED" + } + } + ``` + +--- + +## Producers + +### Match Request Updated Producer + +- This producer emits a message when a match request has been created or updated. +- **Queue**: `MATCH_REQUEST_UPDATED` +- **Data Produced** + - `user` - The user associated with the created or updated match request. + - `topics` - The topics of the created or updated match request. + - `difficulty` - The difficulty of the created or updated match request. + + ```json + { + "user": { + "id": "6713d1778986bf54b29bd0f8", + "username": "user123", + "requestId": "6714d1806da8e6d033ac2be1", + }, + "topics": [ "Algorithms", "Arrays" ], + "difficulty": "Hard" + } + ``` + +--- + +### Match Found Producer + +- This producer emits a message when a match has been successfully found between two users based on their preferences. +- **Queue**: `MATCH_FOUND` +- **Data Produced** + - `user1` - The first user associated with the successful match. + - `user2` - The second user associated with the successful match. + - `topics` - The common topics associated with the successful match. + - `difficulty` - The difficulty associated with the successful match. + + ```json + { + "user1": { + "id": "6713d1778986bf54b29bd0f8", + "username": "user123", + "requestId": "6714d1806da8e6d033ac2be1", + }, + "user2": { + "id": "6713d17f8986bf54b29bd0fe", + "username": "userabc", + "requestId": "6714dab233a91c7f7c9b9b15", + }, + "topics": [ "Algorithms" ], + "difficulty": "Hard" + } + ``` + +--- + +## Consumers + +### Match Request Updated Consumer + +- This consumer attempts to find and assign a compatible match request based on their preferences (topics and difficulty) upon the update of a given match request. +- Two match requests are said to be compatible if they share the same difficulty and have at least one topic in common. +- Upon successfully finding a match, it produces a `MATCH_FOUND` event. +- **Queue**: `MATCH_REQUEST_UPDATED` - This message is emitted when a match request is created or updated. +- **Data Consumed** + - `user` - The user associated with the created or updated match request. + - `topics` - The topics of the created or updated match request. + - `difficulty` - The difficulty of the created or updated match request. + + ```json + { + "user": { + "id": "6713d1778986bf54b29bd0f8", + "username": "user123", + "requestId": "6714d1806da8e6d033ac2be1", + }, + "topics": [ "Algorithms", "Arrays" ], + "difficulty": "Hard" + } + ``` + +--- + +### Collab Created Consumer + +- This consumer assigns the relevant collaboration IDs to the corresponding match requests upon the creation of the collaboration room. +- **Queue**: `COLLAB_CREATED` - This message is emitted when a collaboration room is created. +- **Data Consumed** + - `requestId1` - The first request ID associated with the collaboration room. + - `requestId2` - The second request ID associated with the collaboration room. + - `collabId` - The collaboration ID associated with the collaboration room. + + ```json + { + "requestId1": "6714d1806da8e6d033ac2be1", + "requestId2": "67144180cda8e610333e4b12", + "collabId": "676e7c9a028e8780b3a73a58", + } + ``` \ No newline at end of file diff --git a/services/match/src/controllers/matchRequestController.ts b/services/match/src/controllers/matchRequestController.ts index fbaef53617..4600dc6940 100644 --- a/services/match/src/controllers/matchRequestController.ts +++ b/services/match/src/controllers/matchRequestController.ts @@ -54,7 +54,7 @@ export const updateMatchRequest = async (req: Request, res: Response) => { matchRequest.topics, matchRequest.difficulty, ); - handleSuccess(res, 201, 'Match request update successfully', matchRequest); + handleSuccess(res, 201, 'Match request updated successfully', matchRequest); } catch (error) { console.error('Error in updateMatchRequest:', error); handleInternalError(res, 'Failed to update match request'); diff --git a/services/match/src/models/repository.ts b/services/match/src/models/repository.ts index e8ac017086..a0baee367d 100644 --- a/services/match/src/models/repository.ts +++ b/services/match/src/models/repository.ts @@ -30,6 +30,7 @@ export async function findMatchRequestAndUpdate(id: IdType, userId: IdType) { return await MatchRequestModel.findOneAndUpdate( { _id: id, userId, pairId: null }, { $set: { updatedAt: Date.now() } }, + { new: true }, ); } @@ -61,11 +62,12 @@ export async function findMatchRequestAndAssignPair( updatedAt: { $gte: oneMinuteAgo() }, }, { $set: { pairId: requestId } }, + { new: true }, ); } export async function findMatchRequestByIdAndAssignPair(id: IdType, pairId: IdType) { - await MatchRequestModel.findByIdAndUpdate(id, { $set: { pairId } }); + await MatchRequestModel.findByIdAndUpdate(id, { $set: { pairId } }, { new: true }); } export async function findAndAssignCollab(requestId1: IdType, requestId2: IdType, collabId: IdType) { From b0fe20f247310e58e1ebe22bf17ba88faef5ccc2 Mon Sep 17 00:00:00 2001 From: Samuel Lim Date: Sun, 20 Oct 2024 18:33:12 +0800 Subject: [PATCH 26/28] Enable tests for match service --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 98cef5dbd1..5089e29eb6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,7 +23,7 @@ jobs: strategy: fail-fast: false matrix: - service: [frontend, services/question, services/user] + service: [frontend, services/question, services/user, services/match] steps: - uses: actions/checkout@v4 - name: Use Node.js From fd75363b6e256620464ef57efe55cc97ff4efb42 Mon Sep 17 00:00:00 2001 From: Samuel Lim Date: Sun, 20 Oct 2024 19:32:04 +0800 Subject: [PATCH 27/28] Clean up code --- .../finding-match.component.html | 2 +- .../finding-match/finding-match.component.ts | 24 +++++++++---------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/frontend/src/app/matching/finding-match/finding-match.component.html b/frontend/src/app/matching/finding-match/finding-match.component.html index d0f954a7ae..574fd45191 100644 --- a/frontend/src/app/matching/finding-match/finding-match.component.html +++ b/frontend/src/app/matching/finding-match/finding-match.component.html @@ -13,7 +13,7 @@

Finding a Match...

-

Time Left: {{ timeLeft }}

+

Time Left: {{ matchTimeLeft }}

} @else { diff --git a/frontend/src/app/matching/finding-match/finding-match.component.ts b/frontend/src/app/matching/finding-match/finding-match.component.ts index 0abc35f175..72b423a42e 100644 --- a/frontend/src/app/matching/finding-match/finding-match.component.ts +++ b/frontend/src/app/matching/finding-match/finding-match.component.ts @@ -26,13 +26,11 @@ export class FindingMatchComponent { @Output() matchFailed = new EventEmitter(); @Output() matchSuccess = new EventEmitter(); - isFindingMatch = true; - - timeLeft!: number; - interval!: NodeJS.Timeout; - - matchPoll!: Subscription; - stopPolling$ = new EventEmitter(); + protected isFindingMatch = true; + protected matchTimeLeft = 0; + protected matchTimeInterval!: NodeJS.Timeout; + protected matchPoll!: Subscription; + protected stopPolling$ = new EventEmitter(); constructor( private matchService: MatchService, @@ -111,10 +109,10 @@ export class FindingMatchComponent { } startTimer(time: number) { - this.timeLeft = time; - this.interval = setInterval(() => { - if (this.timeLeft > 0) { - this.timeLeft--; + this.matchTimeLeft = time; + this.matchTimeInterval = setInterval(() => { + if (this.matchTimeLeft > 0) { + this.matchTimeLeft--; } else { this.stopTimer(); } @@ -122,8 +120,8 @@ export class FindingMatchComponent { } stopTimer() { - if (this.interval) { - clearInterval(this.interval); + if (this.matchTimeInterval) { + clearInterval(this.matchTimeInterval); } } } From 899d47c9905549763b3274f72acccf443ba659c2 Mon Sep 17 00:00:00 2001 From: McNaBry Date: Sun, 20 Oct 2024 21:53:10 +0800 Subject: [PATCH 28/28] Fix bug with clearing topics input on match page * When clearing the input field, topics is temporarily set to null before the onClear event is emitted * This causes hasQuestionsValidator to reference a null variable * The fix is to add an additional Validators.required to prevent hasQuestionsValidator from firing before topics is set to an empty array --- frontend/src/app/matching/matching.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/matching/matching.component.ts b/frontend/src/app/matching/matching.component.ts index 3c2b662b70..110a84898a 100644 --- a/frontend/src/app/matching/matching.component.ts +++ b/frontend/src/app/matching/matching.component.ts @@ -62,7 +62,7 @@ export class MatchingComponent implements OnInit { this.fetchTopics(); this.matchForm = new FormGroup( { - topics: new FormControl([], [Validators.minLength(1)]), + topics: new FormControl([], [Validators.required, Validators.minLength(1)]), difficulty: new FormControl(null, [Validators.required]), }, {