diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7ca62c7 --- /dev/null +++ b/.env.example @@ -0,0 +1,20 @@ +PORT=8080 +NODE_ENV=development +DB_HOST=edusaig-db +DB_PORT=5432 +DB_USERNAME=postgres +DB_PASSWORD=qwerty +DB_DATABASE=edusaig +CORS_ALLOW_ORIGIN=http://localhost:5173 +JWT_ACCESS_SECRET=+W5LRxRFUk8MKSHMeRYevRg +JWT_REFRESH_SECRET=2Z7bB8GizN15kaHzB+H8Tg +JWT_ACCESS_EXPIRATION=1d +JWT_REFRESH_EXPIRATION=7d +AWS_ACCESS_KEY_ID=youraccesskey +AWS_SECRET_ACCESS_KEY=yoursecretkey +AWS_REGION=yourregion +AWS_BUCKET_NAME=yourbucketname +ADMIN_EMAIL=admin@gmail.com +ADMIN_PASSWORD=P@ssword! +AI_URL=https://localhost:5000 +API_URL=https://api.edusaig.com \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fbe8cd4..b8543bf 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,13 +1,13 @@ name: Build and deploy to an VM on: - push: + push: branches: - dev workflow_dispatch: permissions: - contents: read + contents: write jobs: build-and-deploy: @@ -30,7 +30,7 @@ jobs: echo "JWT_ACCESS_SECRET=${{ secrets.JWT_ACCESS_SECRET }}" >> .env echo "JWT_REFRESH_SECRET=${{ secrets.JWT_REFRESH_SECRET }}" >> .env echo "JWT_ACCESS_EXPIRATION=${{ secrets.JWT_ACCESS_EXPIRATION }}" >> .env - echo "AI_HOST=${{ secrets.JWT_REFRESH_EXPIRATION }}" >> .env + echo "JWT_REFRESH_EXPIRATION=${{ secrets.JWT_REFRESH_EXPIRATION }}" >> .env echo "AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }}" >> .env echo "AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }}" >> .env echo "AWS_REGION=${{ secrets.AWS_REGION }}" >> .env @@ -38,6 +38,9 @@ jobs: echo "ADMIN_EMAIL=${{ secrets.ADMIN_EMAIL }}" >> .env echo "ADMIN_PASSWORD=${{ secrets.ADMIN_PASSWORD }}" >> .env echo "AI_URL=${{ secrets.AI_URL }}" >> .env + echo "API_URL=${{ secrets.API_URL }}" >> .env + echo "API_URL=${{ secrets.API_URL }}" >> .env + echo "VOLUMES_PATH=${{ secrets.VOLUMES_PATH }}" >> .env cat .env - name: Running Docker Compose diff --git a/.gitignore b/.gitignore index 12fb34f..234e8eb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,52 +1,43 @@ -packages/*/package-lock.json - -# dependencies -node_modules/ - -# IDE -/.idea -/.awcache -/.vscode -/.devcontainer -/.classpath -/.project -/.settings -*.code-workspace - -# Vim -[._]*.s[a-v][a-z] -[._]*.sw[a-p] -[._]s[a-rt-v][a-z] -[._]ss[a-gi-z] -[._]sw[a-p] - -# bundle -packages/**/*.d.ts -packages/**/*.js - -# misc +# compiled output +/dist +/node_modules +/postgres-data + +# Logs +logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +pnpm-lock.yaml + +# OS .DS_Store -lerna-debug.log -npm-debug.log -yarn-error.log -/**/npm-debug.log -/packages/**/.npmignore -/packages/**/LICENSE -*.tsbuildinfo -# example -/quick-start -/example_dist -/example - -# tests -/test -/benchmarks/memory +# Tests /coverage /.nyc_output -/packages/graphql -/benchmarks/memory -build/config\.gypi -.npmrc -pnpm-lock.yaml \ No newline at end of file +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +.data +/files +.env +/ormconfig.json +/src/database/migrations \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f5ac050 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +FROM node:20-alpine AS base + +RUN npm i -g pnpm + +FROM base as dependencies +WORKDIR /app +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile + +FROM base AS build +WORKDIR /app +COPY . . +COPY --from=dependencies /app/node_modules ./node_modules +RUN pnpm run build +RUN pnpm prune --prod + +FROM base AS deploy +RUN apk add --update curl && rm -rf /var/cache/apk/* + +WORKDIR /app +COPY --from=build --chown=node:node /app/package.json /app/pnpm-lock.yaml ./ +COPY --from=build --chown=node:node /app/dist ./dist +COPY --from=build --chown=node:node /app/node_modules ./node_modules +COPY --chown=node:node .env .env + +USER node + +EXPOSE 3000 +CMD ["node", "dist/main.js"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..03bbcbb --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,38 @@ +version: '3.8' + +services: + db: + image: postgres:alpine3.20 + container_name: ${DB_HOST} + restart: unless-stopped + ports: + - '${DB_PORT}:${DB_PORT}' + environment: + POSTGRES_USER: ${DB_USERNAME} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: ${DB_DATABASE} + volumes: + - ${VOLUMES_PATH}:/var/lib/postgresql/data + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U ${DB_USERNAME}'] + interval: 10s + timeout: 5s + retries: 5 + + app: + container_name: edusaig-api + image: edusaig-api + depends_on: + db: + condition: service_healthy + build: + context: . + restart: unless-stopped + env_file: + - .env + environment: + - TZ=Asia/Bangkok + ports: + - '${PORT}:${PORT}' + volumes: + - ./src/database/migrations:/app/dist/database/migrations diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3e14cc5 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,10196 @@ +{ + "name": "edusaig-api", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "edusaig-api", + "version": "0.0.1", + "license": "UNLICENSED", + "dependencies": { + "@nestjs/class-validator": "0.13.1", + "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.3.0", + "@nestjs/core": "^10.0.0", + "@nestjs/jwt": "^10.2.0", + "@nestjs/mapped-types": "^2.0.6", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/swagger": "^8.0.5", + "@nestjs/typeorm": "^10.0.2", + "argon2": "^0.41.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "dotenv": "^16.4.5", + "joi": "^17.13.3", + "mysql2": "^3.11.4", + "pg": "^8.13.1", + "reflect-metadata": "^0.2.0", + "rxjs": "^7.8.1", + "typeorm": "^0.3.20", + "typeorm-extension": "^3.6.3" + }, + "devDependencies": { + "@nestjs/cli": "^10.0.0", + "@nestjs/schematics": "^10.0.0", + "@nestjs/testing": "^10.0.0", + "@types/express": "^5.0.0", + "@types/jest": "^29.5.2", + "@types/node": "^20.3.1", + "@types/supertest": "^6.0.0", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "eslint": "^8.0.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "jest": "^29.5.0", + "prettier": "^3.0.0", + "source-map-support": "^0.5.21", + "supertest": "^7.0.0", + "ts-jest": "^29.1.0", + "ts-loader": "^9.4.3", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.1.3" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.11.tgz", + "integrity": "sha512-vTNDYNsLIWpYk2I969LMQFH29GTsLzxNk/0cLw5q56ARF0v5sIWfHYwGTS88jdDqIpuuettcSczbxeA7EuAmqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.11.tgz", + "integrity": "sha512-I5wviiIqiFwar9Pdk30Lujk8FczEEc18i22A5c6Z9lbmhPQdTroDnEQdsfXjy404wPe8H62s0I15o4pmMGfTYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "jsonc-parser": "3.2.1", + "magic-string": "0.30.8", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-17.3.11.tgz", + "integrity": "sha512-kcOMqp+PHAKkqRad7Zd7PbpqJ0LqLaNZdY1+k66lLWmkEBozgq8v4ASn/puPWf9Bo0HpCiK+EzLf0VHE8Z/y6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "ansi-colors": "4.1.3", + "inquirer": "9.2.15", + "symbol-observable": "4.0.0", + "yargs-parser": "21.1.1" + }, + "bin": { + "schematics": "bin/schematics.js" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/inquirer": { + "version": "9.2.15", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.15.tgz", + "integrity": "sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ljharb/through": "^2.3.12", + "ansi-escapes": "^4.3.2", + "chalk": "^5.3.0", + "cli-cursor": "^3.1.0", + "cli-width": "^4.1.0", + "external-editor": "^3.1.0", + "figures": "^3.2.0", + "lodash": "^4.17.21", + "mute-stream": "1.0.0", + "ora": "^5.4.1", + "run-async": "^3.0.0", + "rxjs": "^7.8.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.2.tgz", + "integrity": "sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", + "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.0", + "@babel/generator": "^7.26.0", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.0", + "@babel/parser": "^7.26.0", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.26.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", + "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.2", + "@babel/types": "^7.26.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", + "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", + "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", + "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", + "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", + "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", + "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", + "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/generator": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/template": "^7.25.9", + "@babel/types": "^7.25.9", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", + "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "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/@eslint/eslintrc/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/@eslint/eslintrc/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/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/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/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@faker-js/faker": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", + "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=6.14.13" + } + }, + "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.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/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/@humanwhocodes/config-array/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/@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/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/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/@jest/reporters/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/@jest/reporters/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/@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/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.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==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@ljharb/through": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.13.tgz", + "integrity": "sha512-/gKJun8NNiWGZJkGzI/Ragc53cOdcLNdzjLaIa+GEjguQs0ulsurx8WN0jijdK9yPqDvziX995sMRLyLt1uZMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@microsoft/tsdoc": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.0.tgz", + "integrity": "sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA==", + "license": "MIT" + }, + "node_modules/@nestjs/class-validator": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@nestjs/class-validator/-/class-validator-0.13.1.tgz", + "integrity": "sha512-Wwpg9KuGnkWOFleBPEFdHQKwqZsF/PFWQaeSaXwatkofgqD7N6AV5J30xIVgScDP+IcE+MdPGZJ38vHQ24KxPQ==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.1.3", + "libphonenumber-js": "^1.9.7", + "validator": "^13.5.2" + } + }, + "node_modules/@nestjs/cli": { + "version": "10.4.7", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.7.tgz", + "integrity": "sha512-4wJTtBJsbvjLIzXl+Qj6DYHv4J7abotuXyk7bes5erL79y+KBT61LulL56SqilzmNnHOAVbXcSXOn9S2aWUn6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "@angular-devkit/schematics-cli": "17.3.11", + "@nestjs/schematics": "^10.0.1", + "chalk": "4.1.2", + "chokidar": "3.6.0", + "cli-table3": "0.6.5", + "commander": "4.1.1", + "fork-ts-checker-webpack-plugin": "9.0.2", + "glob": "10.4.2", + "inquirer": "8.2.6", + "node-emoji": "1.11.0", + "ora": "5.4.1", + "tree-kill": "1.2.2", + "tsconfig-paths": "4.2.0", + "tsconfig-paths-webpack-plugin": "4.1.0", + "typescript": "5.6.3", + "webpack": "5.96.1", + "webpack-node-externals": "3.0.0" + }, + "bin": { + "nest": "bin/nest.js" + }, + "engines": { + "node": ">= 16.14" + }, + "peerDependencies": { + "@swc/cli": "^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0", + "@swc/core": "^1.3.62" + }, + "peerDependenciesMeta": { + "@swc/cli": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/@nestjs/common": { + "version": "10.4.7", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.7.tgz", + "integrity": "sha512-gIOpjD3Mx8gfYGxYm/RHPcJzqdknNNFCyY+AxzBT3gc5Xvvik1Dn5OxaMGw5EbVfhZgJKVP0n83giUOAlZQe7w==", + "license": "MIT", + "dependencies": { + "iterare": "1.2.1", + "tslib": "2.7.0", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/config": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.3.0.tgz", + "integrity": "sha512-pdGTp8m9d0ZCrjTpjkUbZx6gyf2IKf+7zlkrPNMsJzYZ4bFRRTpXrnj+556/5uiI6AfL5mMrJc2u7dB6bvM+VA==", + "license": "MIT", + "dependencies": { + "dotenv": "16.4.5", + "dotenv-expand": "10.0.0", + "lodash": "4.17.21" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/core": { + "version": "10.4.7", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.7.tgz", + "integrity": "sha512-AIpQzW/vGGqSLkKvll1R7uaSNv99AxZI2EFyVJPNGDgFsfXaohfV1Ukl6f+s75Km+6Fj/7aNl80EqzNWQCS8Ig==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@nuxtjs/opencollective": "0.3.2", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "3.3.0", + "tslib": "2.7.0", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/@nestjs/jwt": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.2.0.tgz", + "integrity": "sha512-x8cG90SURkEiLOehNaN2aRlotxT0KZESUliOPKKnjWiyJOcWurkF3w345WOX0P4MgFzUjGoZ1Sy0aZnxeihT0g==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "9.0.5", + "jsonwebtoken": "9.0.2" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/@nestjs/mapped-types": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.6.tgz", + "integrity": "sha512-84ze+CPfp1OWdpRi1/lOu59hOhTz38eVzJvRKrg9ykRFwDz+XleKfMsG0gUqNZYFa6v53XYzeD+xItt8uDW7NQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/platform-express": { + "version": "10.4.7", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.7.tgz", + "integrity": "sha512-q6XDOxZPTZ9cxALcVuKUlRBk+cVEv6dW2S8p2yVre22kpEQxq53/OI8EseDvzObGb6hepZ8+yBY04qoYqSlXNQ==", + "license": "MIT", + "dependencies": { + "body-parser": "1.20.3", + "cors": "2.8.5", + "express": "4.21.1", + "multer": "1.4.4-lts.1", + "tslib": "2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0" + } + }, + "node_modules/@nestjs/schematics": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.2.3.tgz", + "integrity": "sha512-4e8gxaCk7DhBxVUly2PjYL4xC2ifDFexCqq1/u4TtivLGXotVk0wHdYuPYe1tHTHuR1lsOkRbfOCpkdTnigLVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "comment-json": "4.2.5", + "jsonc-parser": "3.3.1", + "pluralize": "8.0.0" + }, + "peerDependencies": { + "typescript": ">=4.8.2" + } + }, + "node_modules/@nestjs/schematics/node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nestjs/swagger": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-8.0.5.tgz", + "integrity": "sha512-ZmBdsbQNs3wIN5kCuvAVbz3/ULh3gi814oHTP49uTqAGi1aT0YSatUyncwQOHBOlRT+rwF+TNjoAsZ+twIk/Jw==", + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "^0.15.0", + "@nestjs/mapped-types": "2.0.6", + "js-yaml": "4.1.0", + "lodash": "4.17.21", + "path-to-regexp": "3.3.0", + "swagger-ui-dist": "5.18.2" + }, + "peerDependencies": { + "@fastify/static": "^6.0.0 || ^7.0.0", + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/testing": { + "version": "10.4.7", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.7.tgz", + "integrity": "sha512-aS3sQ0v4g8cyHDzW3xJv1+8MiFAkxUNXmnau588IFFI/nBIo/kevLNHNPr85keYekkJ/lwNDW72h8UGg8BYd9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + } + } + }, + "node_modules/@nestjs/typeorm": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-10.0.2.tgz", + "integrity": "sha512-H738bJyydK4SQkRCTeh1aFBxoO1E9xdL/HaLGThwrqN95os5mEyAtK7BLADOS+vldP4jDZ2VQPLj4epWwRqCeQ==", + "license": "MIT", + "dependencies": { + "uuid": "9.0.1" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0", + "reflect-metadata": "^0.1.13 || ^0.2.0", + "rxjs": "^7.2.0", + "typeorm": "^0.3.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nuxtjs/opencollective": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz", + "integrity": "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "consola": "^2.15.0", + "node-fetch": "^2.6.1" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/@phc/format": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", + "integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "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/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "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", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@sqltools/formatter": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", + "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==", + "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==", + "devOptional": 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==", + "devOptional": 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==", + "devOptional": 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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "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/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "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-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.1.tgz", + "integrity": "sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", + "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "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/node": { + "version": "20.17.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.6.tgz", + "integrity": "sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/qs": { + "version": "6.9.17", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", + "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==", + "dev": true, + "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/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.2.tgz", + "integrity": "sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@types/validator": { + "version": "13.12.2", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz", + "integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.13.0.tgz", + "integrity": "sha512-nQtBLiZYMUPkclSeC3id+x4uVd1SGtHuElTxL++SfP47jR0zfkZBJHc+gL4qPsgTuypz0k8Y2GheaDYn6Gy3rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.13.0", + "@typescript-eslint/type-utils": "8.13.0", + "@typescript-eslint/utils": "8.13.0", + "@typescript-eslint/visitor-keys": "8.13.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.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.13.0.tgz", + "integrity": "sha512-w0xp+xGg8u/nONcGw1UXAr6cjCPU1w0XVyBs6Zqaj5eLmxkKQAByTdV/uGgNN5tVvN/kKpoQlP2cL7R+ajZZIQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "8.13.0", + "@typescript-eslint/types": "8.13.0", + "@typescript-eslint/typescript-estree": "8.13.0", + "@typescript-eslint/visitor-keys": "8.13.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/scope-manager": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.13.0.tgz", + "integrity": "sha512-XsGWww0odcUT0gJoBZ1DeulY1+jkaHUciUq4jKNv4cpInbvvrtDoyBH9rE/n2V29wQJPk8iCH1wipra9BhmiMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.13.0", + "@typescript-eslint/visitor-keys": "8.13.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.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.13.0.tgz", + "integrity": "sha512-Rqnn6xXTR316fP4D2pohZenJnp+NwQ1mo7/JM+J1LWZENSLkJI8ID8QNtlvFeb0HnFSK94D6q0cnMX6SbE5/vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.13.0", + "@typescript-eslint/utils": "8.13.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/types": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.13.0.tgz", + "integrity": "sha512-4cyFErJetFLckcThRUFdReWJjVsPCqyBlJTi6IDEpc1GWCIIZRFxVppjWLIMcQhNGhdWJJRYFHpHoDWvMlDzng==", + "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.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.13.0.tgz", + "integrity": "sha512-v7SCIGmVsRK2Cy/LTLGN22uea6SaUIlpBcO/gnMGT/7zPtxp90bphcGf4fyrCQl3ZtiBKqVTG32hb668oIYy1g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "8.13.0", + "@typescript-eslint/visitor-keys": "8.13.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/utils": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.13.0.tgz", + "integrity": "sha512-A1EeYOND6Uv250nybnLZapeXpYMl8tkzYUxqmoKAWnI4sei3ihf2XdZVd+vVOmHGcp3t+P7yRrNsyyiXTvShFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.13.0", + "@typescript-eslint/types": "8.13.0", + "@typescript-eslint/typescript-estree": "8.13.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.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.13.0.tgz", + "integrity": "sha512-7N/+lztJqH4Mrf0lb10R/CbI1EaAMMGyF5y0oJvFoAhafwgiRA7TXyd8TFn8FC8k5y2dTsYogg238qavRGNnlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.13.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/@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/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "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.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "devOptional": 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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "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==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "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/anymatch/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/app-root-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", + "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/argon2": { + "version": "0.41.1", + "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.41.1.tgz", + "integrity": "sha512-dqCW8kJXke8Ik+McUcMDltrbuAWETPyU6iq+4AhxqKphWi7pChB/Zgd/Tp/o8xRLbg8ksMj46F/vph9wnxpTzQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@phc/format": "^1.0.0", + "node-addon-api": "^8.1.0", + "node-gyp-build": "^4.8.1" + }, + "engines": { + "node": ">=16.17.0" + } + }, + "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==", + "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-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.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==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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/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/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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/body-parser/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/body-parser/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/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", + "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001669", + "electron-to-chromium": "^1.5.41", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "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/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001679", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001679.tgz", + "integrity": "sha512-j2YqID/YwpLnKzCmBOS4tlZdWprXm3ZmQLBH9ZBXFOhoxLA46fwyBvx6toCBWBmnuwUY/qB3kEU6gFx8qgCroA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "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/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true, + "license": "MIT" + }, + "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/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", + "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, + "node_modules/class-validator": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz", + "integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.10.53", + "validator": "^13.9.0" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-highlight": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", + "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", + "license": "ISC", + "dependencies": { + "chalk": "^4.0.0", + "highlight.js": "^10.7.1", + "mz": "^2.4.0", + "parse5": "^5.1.1", + "parse5-htmlparser2-tree-adapter": "^6.0.0", + "yargs": "^16.0.0" + }, + "bin": { + "highlight": "bin/highlight" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/cli-highlight/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cli-highlight/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/cli-highlight/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cli-highlight/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "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==", + "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==", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/comment-json": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.5.tgz", + "integrity": "sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "core-util-is": "^1.0.3", + "esprima": "^4.0.1", + "has-own-prop": "^2.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/consola": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", + "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", + "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/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "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/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "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", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, + "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/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "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/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "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/destr": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.3.tgz", + "integrity": "sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==", + "license": "MIT" + }, + "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/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ebec": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ebec/-/ebec-2.3.0.tgz", + "integrity": "sha512-bt+0tSL7223VU3PSVi0vtNLZ8pO1AfWolcPPMk2a/a5H+o/ZU9ky0n3A0zhrR4qzJTN61uPsGIO4ShhOukdzxA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "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/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.55", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.55.tgz", + "integrity": "sha512-6maZ2ASDOTBtjt9FhqYPRnbvKU5tjG0IN9SztUOWYw2AzNDNpKJYLJmlK0/En4Hs/aiWnB+JZ+gW19PIGszgKg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "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/enhanced-resolve": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/envix": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/envix/-/envix-1.5.0.tgz", + "integrity": "sha512-IOxTKT+tffjxgvX2O5nq6enbkv6kBQ/QdMy18bZWo0P0rKPvsRp2/EypIPwTvJfnmk3VdOlq/KcRSZCswefM/w==", + "license": "MIT", + "dependencies": { + "std-env": "^3.7.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "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/es-module-lexer": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "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.1", + "@humanwhocodes/config-array": "^0.13.0", + "@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/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": "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/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/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/eslint/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/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/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/eslint/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/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/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "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/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "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.7.1", + "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/express/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/express/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/express/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/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "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/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/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/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/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "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/finalhandler/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/finalhandler/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/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": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "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/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/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.0.2.tgz", + "integrity": "sha512-Uochze2R8peoN1XqlSi/rGUkDQpRogtLFocP9+PGu68zk1BDAKXfdeCdyVZpgTk8V8WFVQXdEz426VKjXLO1Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "cosmiconfig": "^8.2.0", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">=12.13.0", + "yarn": ">=1.0.0" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "webpack": "^5.11.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/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/fork-ts-checker-webpack-plugin/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/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.2.tgz", + "integrity": "sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dezalgo": "^1.0.4", + "hexoid": "^2.0.0", + "once": "^1.4.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "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-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", + "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", + "dev": true, + "license": "Unlicense" + }, + "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/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "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/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz", + "integrity": "sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "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==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "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/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/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "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-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-own-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz", + "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/hexoid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz", + "integrity": "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "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/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "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/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, + "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/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/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "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/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/inquirer": { + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", + "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^6.0.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "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-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "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-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "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/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "engines": { + "node": ">=6" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/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/jake/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/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/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/jest-config/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/jest-config/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/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/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/jest-runtime/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/jest-runtime/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/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/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/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.0.tgz", + "integrity": "sha512-H5UpaUI+aHOqZXlYOaFP/8AzKsg+guWu+Pr3Y8i7+Y3zr1aXAvCvTAQ1RxSc6oVD8R8c7brgNtTVP91E7upH/g==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "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-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "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/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "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/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/libphonenumber-js": { + "version": "1.11.14", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.14.tgz", + "integrity": "sha512-sexvAfwcW1Lqws4zFp8heAtAEXbEDnvkYCEGzvOoMgZR7JhXo/IkE9MkkGACgBed5fWqh3ShBGnJBdDnU9N8EQ==", + "license": "MIT" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + } + }, + "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/locter": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/locter/-/locter-2.1.5.tgz", + "integrity": "sha512-eI57PuVxigQ0GBscGIIFGPB467E5zKODHD3XGuknzLvf7HdnvRw3GdZVGj1J8XKsKOYovZQesX/oOdTwbdjwuQ==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.3", + "ebec": "^2.3.0", + "fast-glob": "^3.3.2", + "flat": "^5.0.2", + "jiti": "^2.4.0", + "yaml": "^2.6.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "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/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "license": "Apache-2.0" + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lru.min": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.1.tgz", + "integrity": "sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/magic-string": { + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "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/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "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/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "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==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "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/multer": { + "version": "1.4.4-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4-lts.1.tgz", + "integrity": "sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/mysql2": { + "version": "3.11.4", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.4.tgz", + "integrity": "sha512-Z2o3tY4Z8EvSRDwknaC40MdZ3+m0sKbpnXrShQLdxPrAvcNli7jLrD2Zd2IzsRMw4eK9Yle500FDmlkIqp+krg==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "license": "MIT", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.2.2.tgz", + "integrity": "sha512-9emqXAKhVoNrQ792nLI/wpzPpJ/bj/YXxW0CvAau1+RdGBcCRF1Dmz7719zgVsQNrzHl9Tzn3ImZ4qWFarWL0A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.2.tgz", + "integrity": "sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "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/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "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/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/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.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/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "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/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "license": "MIT" + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "license": "MIT", + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "license": "MIT" + }, + "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/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "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/pg": { + "version": "8.13.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz", + "integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.7.0", + "pg-pool": "^3.7.0", + "pg-protocol": "^1.7.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", + "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz", + "integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz", + "integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", + "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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-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/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "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/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "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==", + "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/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "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/rapiq": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/rapiq/-/rapiq-0.9.0.tgz", + "integrity": "sha512-k4oT4RarFBrlLMJ49xUTeQpa/us0uU4I70D/UEnK3FWQ4GENzei01rEQAmvPKAIzACo4NMW+YcYJ7EVfSa7EFg==", + "license": "MIT", + "dependencies": { + "ebec": "^1.1.0", + "smob": "^1.4.0" + } + }, + "node_modules/rapiq/node_modules/ebec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ebec/-/ebec-1.1.1.tgz", + "integrity": "sha512-JZ1vcvPQtR+8LGbZmbjG21IxLQq/v47iheJqn2F6yB2CgnGfn8ZVg3myHrf3buIZS8UCwQK0jOSIb3oHX7aH8g==", + "license": "MIT", + "dependencies": { + "smob": "^1.4.0" + } + }, + "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/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/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/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/readdirp/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/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "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/rimraf/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/rimraf/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/rimraf/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/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "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/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "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/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/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/schema-utils/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/schema-utils/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/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "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/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/send/node_modules/debug/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/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/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "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/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, + "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==", + "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==", + "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/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "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/smob": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", + "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/std-env": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", + "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==", + "license": "MIT" + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/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/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/superagent": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", + "integrity": "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^3.5.1", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/supertest": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.0.0.tgz", + "integrity": "sha512-qlsr7fIC0lSddmA3tzojvzubYxvlGtzumcdHgPwbFWMISQwL22MhM2Y3LNt+6w9Yyx7559VW5ab70dgphm8qQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^9.0.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.2.tgz", + "integrity": "sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/synckit": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", + "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", + "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/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.36.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", + "integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.20", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/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/test-exclude/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/test-exclude/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/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/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "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==", + "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/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.0.tgz", + "integrity": "sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-jest": { + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-loader": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", + "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.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==", + "devOptional": 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/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.1.0.tgz", + "integrity": "sha512-xWFISjviPydmtmgeUAuXp4N1fky+VCtfhOkDUFIv5ea7p4wuTomI4QTrXvFBX2S4jZsmyTSrStQl+E+4w+RzxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "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-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typeorm": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.20.tgz", + "integrity": "sha512-sJ0T08dV5eoZroaq9uPKBoNcGslHBR4E4y+EBHs//SiGbblGe7IeduP/IH4ddCcj0qp3PHwDwGnuvqEAnKlq/Q==", + "license": "MIT", + "dependencies": { + "@sqltools/formatter": "^1.2.5", + "app-root-path": "^3.1.0", + "buffer": "^6.0.3", + "chalk": "^4.1.2", + "cli-highlight": "^2.1.11", + "dayjs": "^1.11.9", + "debug": "^4.3.4", + "dotenv": "^16.0.3", + "glob": "^10.3.10", + "mkdirp": "^2.1.3", + "reflect-metadata": "^0.2.1", + "sha.js": "^2.4.11", + "tslib": "^2.5.0", + "uuid": "^9.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "typeorm": "cli.js", + "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js", + "typeorm-ts-node-esm": "cli-ts-node-esm.js" + }, + "engines": { + "node": ">=16.13.0" + }, + "funding": { + "url": "https://opencollective.com/typeorm" + }, + "peerDependencies": { + "@google-cloud/spanner": "^5.18.0", + "@sap/hana-client": "^2.12.25", + "better-sqlite3": "^7.1.2 || ^8.0.0 || ^9.0.0", + "hdb-pool": "^0.1.6", + "ioredis": "^5.0.4", + "mongodb": "^5.8.0", + "mssql": "^9.1.1 || ^10.0.1", + "mysql2": "^2.2.5 || ^3.0.1", + "oracledb": "^6.3.0", + "pg": "^8.5.1", + "pg-native": "^3.0.0", + "pg-query-stream": "^4.0.0", + "redis": "^3.1.1 || ^4.0.0", + "sql.js": "^1.4.0", + "sqlite3": "^5.0.3", + "ts-node": "^10.7.0", + "typeorm-aurora-data-api-driver": "^2.0.0" + }, + "peerDependenciesMeta": { + "@google-cloud/spanner": { + "optional": true + }, + "@sap/hana-client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "hdb-pool": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "mssql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "pg-query-stream": { + "optional": true + }, + "redis": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "ts-node": { + "optional": true + }, + "typeorm-aurora-data-api-driver": { + "optional": true + } + } + }, + "node_modules/typeorm-extension": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/typeorm-extension/-/typeorm-extension-3.6.3.tgz", + "integrity": "sha512-AE+8KqBphlBdVz5JS77o6LZzzi+b+YFFt8So4Qu/KRo/iynAwekrx98Oxuu3FAYNm6DUKDcubOBMZsJeiRvHkA==", + "license": "MIT", + "dependencies": { + "@faker-js/faker": "^8.4.1", + "consola": "^3.2.3", + "envix": "^1.5.0", + "locter": "^2.1.5", + "pascal-case": "^3.1.2", + "rapiq": "^0.9.0", + "reflect-metadata": "^0.2.2", + "smob": "^1.5.0", + "yargs": "^17.7.2" + }, + "bin": { + "typeorm-extension": "bin/cli.cjs", + "typeorm-extension-esm": "bin/cli.mjs" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "typeorm": "~0.3.0" + } + }, + "node_modules/typeorm-extension/node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/typeorm/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "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": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/typeorm/node_modules/mkdirp": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", + "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "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/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "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/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "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/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "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/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/webpack": { + "version": "5.96.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.96.1.tgz", + "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-node-externals": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", + "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "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/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "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/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", + "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "devOptional": 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/package.json b/package.json index 286bdc3..8183d6c 100644 --- a/package.json +++ b/package.json @@ -17,14 +17,38 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "migration:generate": "pnpx typeorm migration:generate -o -d dist/shared/configs/database.config.js dist/database/migrations/migration", + "migration:create": "pnpx typeorm migration:create dist/database/migrations/migration", + "migration:run": "pnpx typeorm migration:run -- -d dist/shared/configs/database.config.js", + "migration:revert": "pnpm run typeorm -- migration:revert", + "migration:generate:prod": "docker compose run app pnpm migration:generate", "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { + "@aws-sdk/client-s3": "^3.693.0", + "@nestjs/axios": "^3.1.2", + "@nestjs/class-validator": "0.13.1", "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.0.0", + "@nestjs/jwt": "^10.2.0", + "@nestjs/mapped-types": "^2.0.6", "@nestjs/platform-express": "^10.0.0", + "@nestjs/swagger": "^8.0.5", + "@nestjs/typeorm": "^10.0.2", + "@smithy/types": "^3.7.1", + "argon2": "^0.41.1", + "axios": "^1.7.7", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "dotenv": "^16.4.5", + "joi": "^17.13.3", + "mysql2": "^3.11.4", + "pg": "^8.13.1", "reflect-metadata": "^0.2.0", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "typeorm": "^0.3.20", + "typeorm-extension": "^3.6.3" }, "devDependencies": { "@nestjs/cli": "^10.0.0", @@ -32,6 +56,7 @@ "@nestjs/testing": "^10.0.0", "@types/express": "^5.0.0", "@types/jest": "^29.5.2", + "@types/multer": "^1.4.12", "@types/node": "^20.3.1", "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..ea79ffb --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,7074 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@aws-sdk/client-s3': + specifier: ^3.693.0 + version: 3.693.0 + '@nestjs/axios': + specifier: ^3.1.2 + version: 3.1.2(@nestjs/common@10.0.0)(axios@1.7.7)(rxjs@7.8.1) + '@nestjs/class-validator': + specifier: 0.13.1 + version: 0.13.1 + '@nestjs/common': + specifier: ^10.0.0 + version: 10.0.0(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.0)(rxjs@7.8.1) + '@nestjs/config': + specifier: ^3.3.0 + version: 3.3.0(@nestjs/common@10.0.0)(rxjs@7.8.1) + '@nestjs/core': + specifier: ^10.0.0 + version: 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(reflect-metadata@0.2.0)(rxjs@7.8.1) + '@nestjs/jwt': + specifier: ^10.2.0 + version: 10.2.0(@nestjs/common@10.0.0) + '@nestjs/mapped-types': + specifier: ^2.0.6 + version: 2.0.6(@nestjs/common@10.0.0)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.0) + '@nestjs/platform-express': + specifier: ^10.0.0 + version: 10.0.0(@nestjs/common@10.0.0)(@nestjs/core@10.0.0) + '@nestjs/swagger': + specifier: ^8.0.5 + version: 8.0.5(@nestjs/common@10.0.0)(@nestjs/core@10.0.0)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.0) + '@nestjs/typeorm': + specifier: ^10.0.2 + version: 10.0.2(@nestjs/common@10.0.0)(@nestjs/core@10.0.0)(reflect-metadata@0.2.0)(rxjs@7.8.1)(typeorm@0.3.20) + '@smithy/types': + specifier: ^3.7.1 + version: 3.7.1 + argon2: + specifier: ^0.41.1 + version: 0.41.1 + axios: + specifier: ^1.7.7 + version: 1.7.7 + class-transformer: + specifier: ^0.5.1 + version: 0.5.1 + class-validator: + specifier: ^0.14.1 + version: 0.14.1 + dotenv: + specifier: ^16.4.5 + version: 16.4.5 + joi: + specifier: ^17.13.3 + version: 17.13.3 + mysql2: + specifier: ^3.11.4 + version: 3.11.4 + pg: + specifier: ^8.13.1 + version: 8.13.1 + reflect-metadata: + specifier: ^0.2.0 + version: 0.2.0 + rxjs: + specifier: ^7.8.1 + version: 7.8.1 + typeorm: + specifier: ^0.3.20 + version: 0.3.20(mysql2@3.11.4)(pg@8.13.1)(ts-node@10.9.1) + typeorm-extension: + specifier: ^3.6.3 + version: 3.6.3(typeorm@0.3.20) + +devDependencies: + '@nestjs/cli': + specifier: ^10.0.0 + version: 10.0.0 + '@nestjs/schematics': + specifier: ^10.0.0 + version: 10.0.0(chokidar@3.5.3)(typescript@5.1.3) + '@nestjs/testing': + specifier: ^10.0.0 + version: 10.0.0(@nestjs/common@10.0.0)(@nestjs/core@10.0.0)(@nestjs/platform-express@10.0.0) + '@types/express': + specifier: ^5.0.0 + version: 5.0.0 + '@types/jest': + specifier: ^29.5.2 + version: 29.5.2 + '@types/multer': + specifier: ^1.4.12 + version: 1.4.12 + '@types/node': + specifier: ^20.3.1 + version: 20.3.1 + '@types/supertest': + specifier: ^6.0.0 + version: 6.0.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.0.0 + version: 8.0.0(@typescript-eslint/parser@8.0.0)(eslint@8.0.0)(typescript@5.1.3) + '@typescript-eslint/parser': + specifier: ^8.0.0 + version: 8.0.0(eslint@8.0.0)(typescript@5.1.3) + eslint: + specifier: ^8.0.0 + version: 8.0.0 + eslint-config-prettier: + specifier: ^9.0.0 + version: 9.0.0(eslint@8.0.0) + eslint-plugin-prettier: + specifier: ^5.0.0 + version: 5.0.0(eslint-config-prettier@9.0.0)(eslint@8.0.0)(prettier@3.0.0) + jest: + specifier: ^29.5.0 + version: 29.5.0(@types/node@20.3.1)(ts-node@10.9.1) + prettier: + specifier: ^3.0.0 + version: 3.0.0 + source-map-support: + specifier: ^0.5.21 + version: 0.5.21 + supertest: + specifier: ^7.0.0 + version: 7.0.0 + ts-jest: + specifier: ^29.1.0 + version: 29.1.0(@babel/core@7.26.0)(jest@29.5.0)(typescript@5.1.3) + ts-loader: + specifier: ^9.4.3 + version: 9.4.3(typescript@5.1.3)(webpack@5.96.1) + ts-node: + specifier: ^10.9.1 + version: 10.9.1(@types/node@20.3.1)(typescript@5.1.3) + tsconfig-paths: + specifier: ^4.2.0 + version: 4.2.0 + typescript: + specifier: ^5.1.3 + version: 5.1.3 + +packages: + + /@ampproject/remapping@2.3.0: + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + dev: true + + /@angular-devkit/core@16.1.0(chokidar@3.5.3): + resolution: {integrity: sha512-mrWpuDvttmhrCGcLc68RIXKtTzUhkBTsE5ZZFZNO1+FSC+vO/ZpyCpPd6C+6coM68NfXYjHlms5XF6KbxeGn/Q==} + engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + chokidar: ^3.5.2 + peerDependenciesMeta: + chokidar: + optional: true + dependencies: + ajv: 8.12.0 + ajv-formats: 2.1.1(ajv@8.12.0) + chokidar: 3.5.3 + jsonc-parser: 3.2.0 + rxjs: 7.8.1 + source-map: 0.7.4 + dev: true + + /@angular-devkit/schematics-cli@16.1.0(chokidar@3.5.3): + resolution: {integrity: sha512-siBpRDmMMV7NB+NvaDHeJ4doHoSkFwIywwFj8GXnBCtobyxrBl1EyG1cKK+FHRydYtyYIk8FEoOpJA9oE9S2hg==} + engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + hasBin: true + dependencies: + '@angular-devkit/core': 16.1.0(chokidar@3.5.3) + '@angular-devkit/schematics': 16.1.0(chokidar@3.5.3) + ansi-colors: 4.1.3 + inquirer: 8.2.4 + symbol-observable: 4.0.0 + yargs-parser: 21.1.1 + transitivePeerDependencies: + - chokidar + dev: true + + /@angular-devkit/schematics@16.1.0(chokidar@3.5.3): + resolution: {integrity: sha512-LM35PH9DT3eQRSZgrkk2bx1ZQjjVh8BCByTlr37/c+FnF9mNbeBsa1YkxrlsN/CwO+045OwEwRHnkM9Zcx0U/A==} + engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + dependencies: + '@angular-devkit/core': 16.1.0(chokidar@3.5.3) + jsonc-parser: 3.2.0 + magic-string: 0.30.0 + ora: 5.4.1 + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + dev: true + + /@aws-crypto/crc32@5.2.0: + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.692.0 + tslib: 2.8.1 + dev: false + + /@aws-crypto/crc32c@5.2.0: + resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.692.0 + tslib: 2.8.1 + dev: false + + /@aws-crypto/sha1-browser@5.2.0: + resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} + dependencies: + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.692.0 + '@aws-sdk/util-locate-window': 3.693.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + dev: false + + /@aws-crypto/sha256-browser@5.2.0: + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.692.0 + '@aws-sdk/util-locate-window': 3.693.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + dev: false + + /@aws-crypto/sha256-js@5.2.0: + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.692.0 + tslib: 2.8.1 + dev: false + + /@aws-crypto/supports-web-crypto@5.2.0: + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + dependencies: + tslib: 2.8.1 + dev: false + + /@aws-crypto/util@5.2.0: + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + dependencies: + '@aws-sdk/types': 3.692.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + dev: false + + /@aws-sdk/client-s3@3.693.0: + resolution: {integrity: sha512-vgGI2e0Q6pzyhqfrSysi+sk/i+Nl+lMon67oqj/57RcCw9daL1/inpS+ADuwHpiPWkrg+U0bOXnmHjkLeTslJg==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-crypto/sha1-browser': 5.2.0 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/client-sso-oidc': 3.693.0(@aws-sdk/client-sts@3.693.0) + '@aws-sdk/client-sts': 3.693.0 + '@aws-sdk/core': 3.693.0 + '@aws-sdk/credential-provider-node': 3.693.0(@aws-sdk/client-sso-oidc@3.693.0)(@aws-sdk/client-sts@3.693.0) + '@aws-sdk/middleware-bucket-endpoint': 3.693.0 + '@aws-sdk/middleware-expect-continue': 3.693.0 + '@aws-sdk/middleware-flexible-checksums': 3.693.0 + '@aws-sdk/middleware-host-header': 3.693.0 + '@aws-sdk/middleware-location-constraint': 3.693.0 + '@aws-sdk/middleware-logger': 3.693.0 + '@aws-sdk/middleware-recursion-detection': 3.693.0 + '@aws-sdk/middleware-sdk-s3': 3.693.0 + '@aws-sdk/middleware-ssec': 3.693.0 + '@aws-sdk/middleware-user-agent': 3.693.0 + '@aws-sdk/region-config-resolver': 3.693.0 + '@aws-sdk/signature-v4-multi-region': 3.693.0 + '@aws-sdk/types': 3.692.0 + '@aws-sdk/util-endpoints': 3.693.0 + '@aws-sdk/util-user-agent-browser': 3.693.0 + '@aws-sdk/util-user-agent-node': 3.693.0 + '@aws-sdk/xml-builder': 3.693.0 + '@smithy/config-resolver': 3.0.12 + '@smithy/core': 2.5.3 + '@smithy/eventstream-serde-browser': 3.0.13 + '@smithy/eventstream-serde-config-resolver': 3.0.10 + '@smithy/eventstream-serde-node': 3.0.12 + '@smithy/fetch-http-handler': 4.1.1 + '@smithy/hash-blob-browser': 3.1.9 + '@smithy/hash-node': 3.0.10 + '@smithy/hash-stream-node': 3.1.9 + '@smithy/invalid-dependency': 3.0.10 + '@smithy/md5-js': 3.0.10 + '@smithy/middleware-content-length': 3.0.12 + '@smithy/middleware-endpoint': 3.2.3 + '@smithy/middleware-retry': 3.0.27 + '@smithy/middleware-serde': 3.0.10 + '@smithy/middleware-stack': 3.0.10 + '@smithy/node-config-provider': 3.1.11 + '@smithy/node-http-handler': 3.3.1 + '@smithy/protocol-http': 4.1.7 + '@smithy/smithy-client': 3.4.4 + '@smithy/types': 3.7.1 + '@smithy/url-parser': 3.0.10 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.27 + '@smithy/util-defaults-mode-node': 3.0.27 + '@smithy/util-endpoints': 2.1.6 + '@smithy/util-middleware': 3.0.10 + '@smithy/util-retry': 3.0.10 + '@smithy/util-stream': 3.3.1 + '@smithy/util-utf8': 3.0.0 + '@smithy/util-waiter': 3.1.9 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-sso-oidc@3.693.0(@aws-sdk/client-sts@3.693.0): + resolution: {integrity: sha512-UEDbYlYtK/e86OOMyFR4zEPyenIxDzO2DRdz3fwVW7RzZ94wfmSwBh/8skzPTuY1G7sI064cjHW0b0QG01Sdtg==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@aws-sdk/client-sts': ^3.693.0 + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/client-sts': 3.693.0 + '@aws-sdk/core': 3.693.0 + '@aws-sdk/credential-provider-node': 3.693.0(@aws-sdk/client-sso-oidc@3.693.0)(@aws-sdk/client-sts@3.693.0) + '@aws-sdk/middleware-host-header': 3.693.0 + '@aws-sdk/middleware-logger': 3.693.0 + '@aws-sdk/middleware-recursion-detection': 3.693.0 + '@aws-sdk/middleware-user-agent': 3.693.0 + '@aws-sdk/region-config-resolver': 3.693.0 + '@aws-sdk/types': 3.692.0 + '@aws-sdk/util-endpoints': 3.693.0 + '@aws-sdk/util-user-agent-browser': 3.693.0 + '@aws-sdk/util-user-agent-node': 3.693.0 + '@smithy/config-resolver': 3.0.12 + '@smithy/core': 2.5.3 + '@smithy/fetch-http-handler': 4.1.1 + '@smithy/hash-node': 3.0.10 + '@smithy/invalid-dependency': 3.0.10 + '@smithy/middleware-content-length': 3.0.12 + '@smithy/middleware-endpoint': 3.2.3 + '@smithy/middleware-retry': 3.0.27 + '@smithy/middleware-serde': 3.0.10 + '@smithy/middleware-stack': 3.0.10 + '@smithy/node-config-provider': 3.1.11 + '@smithy/node-http-handler': 3.3.1 + '@smithy/protocol-http': 4.1.7 + '@smithy/smithy-client': 3.4.4 + '@smithy/types': 3.7.1 + '@smithy/url-parser': 3.0.10 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.27 + '@smithy/util-defaults-mode-node': 3.0.27 + '@smithy/util-endpoints': 2.1.6 + '@smithy/util-middleware': 3.0.10 + '@smithy/util-retry': 3.0.10 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-sso@3.693.0: + resolution: {integrity: sha512-QEynrBC26x6TG9ZMzApR/kZ3lmt4lEIs2D+cHuDxt6fDGzahBUsQFBwJqhizzsM97JJI5YvmJhmihoYjdSSaXA==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.693.0 + '@aws-sdk/middleware-host-header': 3.693.0 + '@aws-sdk/middleware-logger': 3.693.0 + '@aws-sdk/middleware-recursion-detection': 3.693.0 + '@aws-sdk/middleware-user-agent': 3.693.0 + '@aws-sdk/region-config-resolver': 3.693.0 + '@aws-sdk/types': 3.692.0 + '@aws-sdk/util-endpoints': 3.693.0 + '@aws-sdk/util-user-agent-browser': 3.693.0 + '@aws-sdk/util-user-agent-node': 3.693.0 + '@smithy/config-resolver': 3.0.12 + '@smithy/core': 2.5.3 + '@smithy/fetch-http-handler': 4.1.1 + '@smithy/hash-node': 3.0.10 + '@smithy/invalid-dependency': 3.0.10 + '@smithy/middleware-content-length': 3.0.12 + '@smithy/middleware-endpoint': 3.2.3 + '@smithy/middleware-retry': 3.0.27 + '@smithy/middleware-serde': 3.0.10 + '@smithy/middleware-stack': 3.0.10 + '@smithy/node-config-provider': 3.1.11 + '@smithy/node-http-handler': 3.3.1 + '@smithy/protocol-http': 4.1.7 + '@smithy/smithy-client': 3.4.4 + '@smithy/types': 3.7.1 + '@smithy/url-parser': 3.0.10 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.27 + '@smithy/util-defaults-mode-node': 3.0.27 + '@smithy/util-endpoints': 2.1.6 + '@smithy/util-middleware': 3.0.10 + '@smithy/util-retry': 3.0.10 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-sts@3.693.0: + resolution: {integrity: sha512-4S2y7VEtvdnjJX4JPl4kDQlslxXEZFnC50/UXVUYSt/AMc5A/GgspFNA5FVz4E3Gwpfobbf23hR2NBF8AGvYoQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/client-sso-oidc': 3.693.0(@aws-sdk/client-sts@3.693.0) + '@aws-sdk/core': 3.693.0 + '@aws-sdk/credential-provider-node': 3.693.0(@aws-sdk/client-sso-oidc@3.693.0)(@aws-sdk/client-sts@3.693.0) + '@aws-sdk/middleware-host-header': 3.693.0 + '@aws-sdk/middleware-logger': 3.693.0 + '@aws-sdk/middleware-recursion-detection': 3.693.0 + '@aws-sdk/middleware-user-agent': 3.693.0 + '@aws-sdk/region-config-resolver': 3.693.0 + '@aws-sdk/types': 3.692.0 + '@aws-sdk/util-endpoints': 3.693.0 + '@aws-sdk/util-user-agent-browser': 3.693.0 + '@aws-sdk/util-user-agent-node': 3.693.0 + '@smithy/config-resolver': 3.0.12 + '@smithy/core': 2.5.3 + '@smithy/fetch-http-handler': 4.1.1 + '@smithy/hash-node': 3.0.10 + '@smithy/invalid-dependency': 3.0.10 + '@smithy/middleware-content-length': 3.0.12 + '@smithy/middleware-endpoint': 3.2.3 + '@smithy/middleware-retry': 3.0.27 + '@smithy/middleware-serde': 3.0.10 + '@smithy/middleware-stack': 3.0.10 + '@smithy/node-config-provider': 3.1.11 + '@smithy/node-http-handler': 3.3.1 + '@smithy/protocol-http': 4.1.7 + '@smithy/smithy-client': 3.4.4 + '@smithy/types': 3.7.1 + '@smithy/url-parser': 3.0.10 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.27 + '@smithy/util-defaults-mode-node': 3.0.27 + '@smithy/util-endpoints': 2.1.6 + '@smithy/util-middleware': 3.0.10 + '@smithy/util-retry': 3.0.10 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/core@3.693.0: + resolution: {integrity: sha512-v6Z/kWmLFqRLDPEwl9hJGhtTgIFHjZugSfF1Yqffdxf4n1AWgtHS7qSegakuMyN5pP4K2tvUD8qHJ+gGe2Bw2A==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/types': 3.692.0 + '@smithy/core': 2.5.3 + '@smithy/node-config-provider': 3.1.11 + '@smithy/property-provider': 3.1.10 + '@smithy/protocol-http': 4.1.7 + '@smithy/signature-v4': 4.2.3 + '@smithy/smithy-client': 3.4.4 + '@smithy/types': 3.7.1 + '@smithy/util-middleware': 3.0.10 + fast-xml-parser: 4.4.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/credential-provider-env@3.693.0: + resolution: {integrity: sha512-hMUZaRSF7+iBKZfBHNLihFs9zvpM1CB8MBOTnTp5NGCVkRYF3SB2LH+Kcippe0ats4qCyB1eEoyQX99rERp2iQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/core': 3.693.0 + '@aws-sdk/types': 3.692.0 + '@smithy/property-provider': 3.1.10 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/credential-provider-http@3.693.0: + resolution: {integrity: sha512-sL8MvwNJU7ZpD7/d2VVb3by1GknIJUxzTIgYtVkDVA/ojo+KRQSSHxcj0EWWXF5DTSh2Tm+LrEug3y1ZyKHsDA==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/core': 3.693.0 + '@aws-sdk/types': 3.692.0 + '@smithy/fetch-http-handler': 4.1.1 + '@smithy/node-http-handler': 3.3.1 + '@smithy/property-provider': 3.1.10 + '@smithy/protocol-http': 4.1.7 + '@smithy/smithy-client': 3.4.4 + '@smithy/types': 3.7.1 + '@smithy/util-stream': 3.3.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/credential-provider-ini@3.693.0(@aws-sdk/client-sso-oidc@3.693.0)(@aws-sdk/client-sts@3.693.0): + resolution: {integrity: sha512-kvaa4mXhCCOuW7UQnBhYqYfgWmwy7WSBSDClutwSLPZvgrhYj2l16SD2lN4IfYdxARYMJJ1lFYp3/jJG/9Yk4Q==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@aws-sdk/client-sts': ^3.693.0 + dependencies: + '@aws-sdk/client-sts': 3.693.0 + '@aws-sdk/core': 3.693.0 + '@aws-sdk/credential-provider-env': 3.693.0 + '@aws-sdk/credential-provider-http': 3.693.0 + '@aws-sdk/credential-provider-process': 3.693.0 + '@aws-sdk/credential-provider-sso': 3.693.0(@aws-sdk/client-sso-oidc@3.693.0) + '@aws-sdk/credential-provider-web-identity': 3.693.0(@aws-sdk/client-sts@3.693.0) + '@aws-sdk/types': 3.692.0 + '@smithy/credential-provider-imds': 3.2.7 + '@smithy/property-provider': 3.1.10 + '@smithy/shared-ini-file-loader': 3.1.11 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' + - aws-crt + dev: false + + /@aws-sdk/credential-provider-node@3.693.0(@aws-sdk/client-sso-oidc@3.693.0)(@aws-sdk/client-sts@3.693.0): + resolution: {integrity: sha512-42WMsBjTNnjYxYuM3qD/Nq+8b7UdMopUq5OduMDxoM3mFTV6PXMMnfI4Z1TNnR4tYRvPXAnuNltF6xmjKbSJRA==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/credential-provider-env': 3.693.0 + '@aws-sdk/credential-provider-http': 3.693.0 + '@aws-sdk/credential-provider-ini': 3.693.0(@aws-sdk/client-sso-oidc@3.693.0)(@aws-sdk/client-sts@3.693.0) + '@aws-sdk/credential-provider-process': 3.693.0 + '@aws-sdk/credential-provider-sso': 3.693.0(@aws-sdk/client-sso-oidc@3.693.0) + '@aws-sdk/credential-provider-web-identity': 3.693.0(@aws-sdk/client-sts@3.693.0) + '@aws-sdk/types': 3.692.0 + '@smithy/credential-provider-imds': 3.2.7 + '@smithy/property-provider': 3.1.10 + '@smithy/shared-ini-file-loader': 3.1.11 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' + - '@aws-sdk/client-sts' + - aws-crt + dev: false + + /@aws-sdk/credential-provider-process@3.693.0: + resolution: {integrity: sha512-cvxQkrTWHHjeHrPlj7EWXPnFSq8x7vMx+Zn1oTsMpCY445N9KuzjfJTkmNGwU2GT6rSZI9/0MM02aQvl5bBBTQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/core': 3.693.0 + '@aws-sdk/types': 3.692.0 + '@smithy/property-provider': 3.1.10 + '@smithy/shared-ini-file-loader': 3.1.11 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/credential-provider-sso@3.693.0(@aws-sdk/client-sso-oidc@3.693.0): + resolution: {integrity: sha512-479UlJxY+BFjj3pJFYUNC0DCMrykuG7wBAXfsvZqQxKUa83DnH5Q1ID/N2hZLkxjGd4ZW0AC3lTOMxFelGzzpQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/client-sso': 3.693.0 + '@aws-sdk/core': 3.693.0 + '@aws-sdk/token-providers': 3.693.0(@aws-sdk/client-sso-oidc@3.693.0) + '@aws-sdk/types': 3.692.0 + '@smithy/property-provider': 3.1.10 + '@smithy/shared-ini-file-loader': 3.1.11 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' + - aws-crt + dev: false + + /@aws-sdk/credential-provider-web-identity@3.693.0(@aws-sdk/client-sts@3.693.0): + resolution: {integrity: sha512-8LB210Pr6VeCiSb2hIra+sAH4KUBLyGaN50axHtIgufVK8jbKIctTZcVY5TO9Se+1107TsruzeXS7VeqVdJfFA==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@aws-sdk/client-sts': ^3.693.0 + dependencies: + '@aws-sdk/client-sts': 3.693.0 + '@aws-sdk/core': 3.693.0 + '@aws-sdk/types': 3.692.0 + '@smithy/property-provider': 3.1.10 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/middleware-bucket-endpoint@3.693.0: + resolution: {integrity: sha512-cPIa+lxMYiFRHtxKfNIVSFGO6LSgZCk42pu3d7KGwD6hu6vXRD5B2/DD3rPcEH1zgl2j0Kx1oGAV7SRXKHSFag==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/types': 3.692.0 + '@aws-sdk/util-arn-parser': 3.693.0 + '@smithy/node-config-provider': 3.1.11 + '@smithy/protocol-http': 4.1.7 + '@smithy/types': 3.7.1 + '@smithy/util-config-provider': 3.0.0 + tslib: 2.8.1 + dev: false + + /@aws-sdk/middleware-expect-continue@3.693.0: + resolution: {integrity: sha512-MuK/gsJWpHz6Tv0CqTCS+QNOxLa2RfPh1biVCu/uO3l7kA0TjQ/C+tfgKvLXeH103tuDrOVINK+bt2ENmI3SWg==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/types': 3.692.0 + '@smithy/protocol-http': 4.1.7 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/middleware-flexible-checksums@3.693.0: + resolution: {integrity: sha512-xkS6zjuE11ob93H9t65kHzphXcUMnN2SmIm2wycUPg+hi8Q6DJA6U2p//6oXkrr9oHy1QvwtllRd7SAd63sFKQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/crc32c': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/core': 3.693.0 + '@aws-sdk/types': 3.692.0 + '@smithy/is-array-buffer': 3.0.0 + '@smithy/node-config-provider': 3.1.11 + '@smithy/protocol-http': 4.1.7 + '@smithy/types': 3.7.1 + '@smithy/util-middleware': 3.0.10 + '@smithy/util-stream': 3.3.1 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + dev: false + + /@aws-sdk/middleware-host-header@3.693.0: + resolution: {integrity: sha512-BCki6sAZ5jYwIN/t3ElCiwerHad69ipHwPsDCxJQyeiOnJ8HG+lEpnVIfrnI8A0fLQNSF3Gtx6ahfBpKiv1Oug==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/types': 3.692.0 + '@smithy/protocol-http': 4.1.7 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/middleware-location-constraint@3.693.0: + resolution: {integrity: sha512-eDAExTZ9uNIP7vs2JCVCOuWJauGueisBSn+Ovt7UvvuEUp6KOIJqn8oFxWmyUQu2GvbG4OcaTLgbqD95YHTB0Q==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/types': 3.692.0 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/middleware-logger@3.693.0: + resolution: {integrity: sha512-dXnXDPr+wIiJ1TLADACI1g9pkSB21KkMIko2u4CJ2JCBoxi5IqeTnVoa6YcC8GdFNVRl+PorZ3Zqfmf1EOTC6w==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/types': 3.692.0 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/middleware-recursion-detection@3.693.0: + resolution: {integrity: sha512-0LDmM+VxXp0u3rG0xQRWD/q6Ubi7G8I44tBPahevD5CaiDZTkmNTrVUf0VEJgVe0iCKBppACMBDkLB0/ETqkFw==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/types': 3.692.0 + '@smithy/protocol-http': 4.1.7 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/middleware-sdk-s3@3.693.0: + resolution: {integrity: sha512-5A++RBjJ3guyq5pbYs+Oq5hMlA8CK2OWaHx09cxVfhHWl/RoaY8DXrft4gnhoUEBrrubyMw7r9j7RIMLvS58kg==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/core': 3.693.0 + '@aws-sdk/types': 3.692.0 + '@aws-sdk/util-arn-parser': 3.693.0 + '@smithy/core': 2.5.3 + '@smithy/node-config-provider': 3.1.11 + '@smithy/protocol-http': 4.1.7 + '@smithy/signature-v4': 4.2.3 + '@smithy/smithy-client': 3.4.4 + '@smithy/types': 3.7.1 + '@smithy/util-config-provider': 3.0.0 + '@smithy/util-middleware': 3.0.10 + '@smithy/util-stream': 3.3.1 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + dev: false + + /@aws-sdk/middleware-ssec@3.693.0: + resolution: {integrity: sha512-Ro5vzI7SRgEeuoMk3fKqFjGv6mG4c7VsSCDwnkiasmafQFBTPvUIpgmu2FXMHqW/OthvoiOzpSrlJ9Bwlx2f8A==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/types': 3.692.0 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/middleware-user-agent@3.693.0: + resolution: {integrity: sha512-/KUq/KEpFFbQmNmpp7SpAtFAdViquDfD2W0QcG07zYBfz9MwE2ig48ALynXm5sMpRmnG7sJXjdvPtTsSVPfkiw==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/core': 3.693.0 + '@aws-sdk/types': 3.692.0 + '@aws-sdk/util-endpoints': 3.693.0 + '@smithy/core': 2.5.3 + '@smithy/protocol-http': 4.1.7 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/region-config-resolver@3.693.0: + resolution: {integrity: sha512-YLUkMsUY0GLW/nfwlZ69cy1u07EZRmsv8Z9m0qW317/EZaVx59hcvmcvb+W4bFqj5E8YImTjoGfE4cZ0F9mkyw==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/types': 3.692.0 + '@smithy/node-config-provider': 3.1.11 + '@smithy/types': 3.7.1 + '@smithy/util-config-provider': 3.0.0 + '@smithy/util-middleware': 3.0.10 + tslib: 2.8.1 + dev: false + + /@aws-sdk/signature-v4-multi-region@3.693.0: + resolution: {integrity: sha512-s7zbbsoVIriTR4ZGaateKuTqz6ddpazAyHvjk7I9kd+NvGNPiuAI18UdbuiiRI6K5HuYKf1ah6mKWFGPG15/kQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/middleware-sdk-s3': 3.693.0 + '@aws-sdk/types': 3.692.0 + '@smithy/protocol-http': 4.1.7 + '@smithy/signature-v4': 4.2.3 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/token-providers@3.693.0(@aws-sdk/client-sso-oidc@3.693.0): + resolution: {integrity: sha512-nDBTJMk1l/YmFULGfRbToOA2wjf+FkQT4dMgYCv+V9uSYsMzQj8A7Tha2dz9yv4vnQgYaEiErQ8d7HVyXcVEoA==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@aws-sdk/client-sso-oidc': ^3.693.0 + dependencies: + '@aws-sdk/client-sso-oidc': 3.693.0(@aws-sdk/client-sts@3.693.0) + '@aws-sdk/types': 3.692.0 + '@smithy/property-provider': 3.1.10 + '@smithy/shared-ini-file-loader': 3.1.11 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/types@3.692.0: + resolution: {integrity: sha512-RpNvzD7zMEhiKgmlxGzyXaEcg2khvM7wd5sSHVapOcrde1awQSOMGI4zKBQ+wy5TnDfrm170ROz/ERLYtrjPZA==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/util-arn-parser@3.693.0: + resolution: {integrity: sha512-WC8x6ca+NRrtpAH64rWu+ryDZI3HuLwlEr8EU6/dbC/pt+r/zC0PBoC15VEygUaBA+isppCikQpGyEDu0Yj7gQ==} + engines: {node: '>=16.0.0'} + dependencies: + tslib: 2.8.1 + dev: false + + /@aws-sdk/util-endpoints@3.693.0: + resolution: {integrity: sha512-eo4F6DRQ/kxS3gxJpLRv+aDNy76DxQJL5B3DPzpr9Vkq0ygVoi4GT5oIZLVaAVIJmi6k5qq9dLsYZfWLUxJJSg==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/types': 3.692.0 + '@smithy/types': 3.7.1 + '@smithy/util-endpoints': 2.1.6 + tslib: 2.8.1 + dev: false + + /@aws-sdk/util-locate-window@3.693.0: + resolution: {integrity: sha512-ttrag6haJLWABhLqtg1Uf+4LgHWIMOVSYL+VYZmAp2v4PUGOwWmWQH0Zk8RM7YuQcLfH/EoR72/Yxz6A4FKcuw==} + engines: {node: '>=16.0.0'} + dependencies: + tslib: 2.8.1 + dev: false + + /@aws-sdk/util-user-agent-browser@3.693.0: + resolution: {integrity: sha512-6EUfuKOujtddy18OLJUaXfKBgs+UcbZ6N/3QV4iOkubCUdeM1maIqs++B9bhCbWeaeF5ORizJw5FTwnyNjE/mw==} + dependencies: + '@aws-sdk/types': 3.692.0 + '@smithy/types': 3.7.1 + bowser: 2.11.0 + tslib: 2.8.1 + dev: false + + /@aws-sdk/util-user-agent-node@3.693.0: + resolution: {integrity: sha512-td0OVX8m5ZKiXtecIDuzY3Y3UZIzvxEr57Hp21NOwieqKCG2UeyQWWeGPv0FQaU7dpTkvFmVNI+tx9iB8V/Nhg==} + engines: {node: '>=16.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + dependencies: + '@aws-sdk/middleware-user-agent': 3.693.0 + '@aws-sdk/types': 3.692.0 + '@smithy/node-config-provider': 3.1.11 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/xml-builder@3.693.0: + resolution: {integrity: sha512-C/rPwJcqnV8VDr2/VtcQnymSpcfEEgH1Jm6V0VmfXNZFv4Qzf1eCS8nsec0gipYgZB+cBBjfXw5dAk6pJ8ubpw==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@babel/code-frame@7.26.2: + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + dev: true + + /@babel/compat-data@7.26.2: + resolution: {integrity: sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/core@7.26.0: + resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helpers': 7.26.0 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + convert-source-map: 2.0.0 + debug: 4.3.7 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/generator@7.26.2: + resolution: {integrity: sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.0.2 + dev: true + + /@babel/helper-compilation-targets@7.25.9: + resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/compat-data': 7.26.2 + '@babel/helper-validator-option': 7.25.9 + browserslist: 4.24.2 + lru-cache: 5.1.1 + semver: 6.3.1 + dev: true + + /@babel/helper-module-imports@7.25.9: + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0): + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.25.9 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-plugin-utils@7.25.9: + resolution: {integrity: sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-string-parser@7.25.9: + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-identifier@7.25.9: + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-option@7.25.9: + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helpers@7.26.0: + resolution: {integrity: sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 + dev: true + + /@babel/parser@7.26.2: + resolution: {integrity: sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.26.0 + dev: true + + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.26.0): + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.26.0): + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.26.0): + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.26.0): + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.26.0): + resolution: {integrity: sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.26.0): + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.26.0): + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.26.0): + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.26.0): + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.26.0): + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.26.0): + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.26.0): + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.26.0): + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.26.0): + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.26.0): + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + dev: true + + /@babel/template@7.25.9: + resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + dev: true + + /@babel/traverse@7.25.9: + resolution: {integrity: sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 + debug: 4.3.7 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/types@7.26.0: + resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + dev: true + + /@bcoe/v8-coverage@0.2.3: + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + dev: true + + /@colors/colors@1.5.0: + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + requiresBuild: true + dev: true + optional: true + + /@cspotcode/source-map-support@0.8.1: + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + /@eslint-community/eslint-utils@4.4.1(eslint@8.0.0): + resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.0.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@eslint-community/regexpp@4.12.1: + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: true + + /@eslint/eslintrc@1.4.1: + resolution: {integrity: sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.7 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@faker-js/faker@8.4.1: + resolution: {integrity: sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0, npm: '>=6.14.13'} + dev: false + + /@hapi/hoek@9.3.0: + resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} + dev: false + + /@hapi/topo@5.1.0: + resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} + dependencies: + '@hapi/hoek': 9.3.0 + dev: false + + /@humanwhocodes/config-array@0.6.0: + resolution: {integrity: sha512-JQlEKbcgEUjBFhLIF4iqM7u/9lwgHRBcpHrmUNCALK0Q3amXN6lxdoXLnF0sm11E9VqTmBALR87IlUg1bZ8A9A==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + dependencies: + '@humanwhocodes/object-schema': 1.2.1 + debug: 4.3.7 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/object-schema@1.2.1: + resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} + deprecated: Use @eslint/object-schema instead + dev: true + + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + dev: false + + /@istanbuljs/load-nyc-config@1.1.0: + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + dev: true + + /@istanbuljs/schema@0.1.3: + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + dev: true + + /@jest/console@29.7.0: + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.3.1 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + dev: true + + /@jest/core@29.7.0(ts-node@10.9.1): + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.3.1 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@20.3.1)(ts-node@10.9.1) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + dev: true + + /@jest/environment@29.7.0: + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.3.1 + jest-mock: 29.7.0 + dev: true + + /@jest/expect-utils@29.7.0: + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.6.3 + dev: true + + /@jest/expect@29.7.0: + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/fake-timers@29.7.0: + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 20.3.1 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + dev: true + + /@jest/globals@29.7.0: + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/reporters@29.7.0: + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.25 + '@types/node': 20.3.1 + chalk: 4.1.2 + collect-v8-coverage: 1.0.2 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.7 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + dev: true + + /@jest/source-map@29.6.3: + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + callsites: 3.1.0 + graceful-fs: 4.2.11 + dev: true + + /@jest/test-result@29.7.0: + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.2 + dev: true + + /@jest/test-sequencer@29.7.0: + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + dev: true + + /@jest/transform@29.7.0: + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.26.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.25 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.8 + pirates: 4.0.6 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/types@29.6.3: + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 20.3.1 + '@types/yargs': 17.0.33 + chalk: 4.1.2 + dev: true + + /@jridgewell/gen-mapping@0.3.5: + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + dev: true + + /@jridgewell/resolve-uri@3.1.2: + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + /@jridgewell/set-array@1.2.1: + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/source-map@0.3.6: + resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + dev: true + + /@jridgewell/sourcemap-codec@1.5.0: + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + /@jridgewell/trace-mapping@0.3.25: + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + dev: true + + /@jridgewell/trace-mapping@0.3.9: + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + /@lukeed/csprng@1.1.0: + resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} + engines: {node: '>=8'} + + /@microsoft/tsdoc@0.15.0: + resolution: {integrity: sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA==} + dev: false + + /@nestjs/axios@3.1.2(@nestjs/common@10.0.0)(axios@1.7.7)(rxjs@7.8.1): + resolution: {integrity: sha512-pFlfi4ZQsZtTNNhvgssbxjCHUd1nMpV3sXy/xOOB2uEJhw3M8j8SFR08gjFNil2we2Har7VCsXLfCkwbMHECFQ==} + peerDependencies: + '@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + axios: ^1.3.1 + rxjs: ^6.0.0 || ^7.0.0 + dependencies: + '@nestjs/common': 10.0.0(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.0)(rxjs@7.8.1) + axios: 1.7.7 + rxjs: 7.8.1 + dev: false + + /@nestjs/class-validator@0.13.1: + resolution: {integrity: sha512-Wwpg9KuGnkWOFleBPEFdHQKwqZsF/PFWQaeSaXwatkofgqD7N6AV5J30xIVgScDP+IcE+MdPGZJ38vHQ24KxPQ==} + dependencies: + '@types/validator': 13.12.2 + libphonenumber-js: 1.11.14 + validator: 13.12.0 + dev: false + + /@nestjs/cli@10.0.0: + resolution: {integrity: sha512-14pju3ejAAUpFe1iK99v/b7Bw96phBMV58GXTSm3TcdgaI4O7UTLXTbMiUNyU+LGr/1CPIfThcWqFyKhDIC9VQ==} + engines: {node: '>= 16'} + hasBin: true + peerDependencies: + '@swc/cli': ^0.1.62 + '@swc/core': ^1.3.62 + peerDependenciesMeta: + '@swc/cli': + optional: true + '@swc/core': + optional: true + dependencies: + '@angular-devkit/core': 16.1.0(chokidar@3.5.3) + '@angular-devkit/schematics': 16.1.0(chokidar@3.5.3) + '@angular-devkit/schematics-cli': 16.1.0(chokidar@3.5.3) + '@nestjs/schematics': 10.0.0(chokidar@3.5.3)(typescript@5.1.3) + chalk: 4.1.2 + chokidar: 3.5.3 + cli-table3: 0.6.3 + commander: 4.1.1 + fork-ts-checker-webpack-plugin: 8.0.0(typescript@5.1.3)(webpack@5.87.0) + inquirer: 8.2.5 + node-emoji: 1.11.0 + ora: 5.4.1 + os-name: 4.0.1 + rimraf: 4.4.1 + shelljs: 0.8.5 + source-map-support: 0.5.21 + tree-kill: 1.2.2 + tsconfig-paths: 4.2.0 + tsconfig-paths-webpack-plugin: 4.0.1 + typescript: 5.1.3 + webpack: 5.87.0 + webpack-node-externals: 3.0.0 + transitivePeerDependencies: + - esbuild + - uglify-js + - webpack-cli + dev: true + + /@nestjs/common@10.0.0(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.0)(rxjs@7.8.1): + resolution: {integrity: sha512-Fa2GDQJrO5TTTcpISWfm0pdPS62V+8YbxeG5CA01zMUI+dCO3v3oFf+BSjqCGUUo7GDNzDsjAejwGXuqA54RPw==} + peerDependencies: + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 + rxjs: ^7.1.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + dependencies: + class-transformer: 0.5.1 + class-validator: 0.14.1 + iterare: 1.2.1 + reflect-metadata: 0.2.0 + rxjs: 7.8.1 + tslib: 2.5.3 + uid: 2.0.2 + + /@nestjs/config@3.3.0(@nestjs/common@10.0.0)(rxjs@7.8.1): + resolution: {integrity: sha512-pdGTp8m9d0ZCrjTpjkUbZx6gyf2IKf+7zlkrPNMsJzYZ4bFRRTpXrnj+556/5uiI6AfL5mMrJc2u7dB6bvM+VA==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + rxjs: ^7.1.0 + dependencies: + '@nestjs/common': 10.0.0(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.0)(rxjs@7.8.1) + dotenv: 16.4.5 + dotenv-expand: 10.0.0 + lodash: 4.17.21 + rxjs: 7.8.1 + dev: false + + /@nestjs/core@10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(reflect-metadata@0.2.0)(rxjs@7.8.1): + resolution: {integrity: sha512-HFTdj4vsF+2qOaq97ZPRDle6Q/KyL5lmMah0/ZR0ie+e1/tnlvmlqw589xFACTemLJFFOjZMy763v+icO9u72w==} + requiresBuild: true + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/microservices': ^10.0.0 + '@nestjs/platform-express': ^10.0.0 + '@nestjs/websockets': ^10.0.0 + reflect-metadata: ^0.1.12 + rxjs: ^7.1.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + '@nestjs/websockets': + optional: true + dependencies: + '@nestjs/common': 10.0.0(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.0)(rxjs@7.8.1) + '@nestjs/platform-express': 10.0.0(@nestjs/common@10.0.0)(@nestjs/core@10.0.0) + '@nuxtjs/opencollective': 0.3.2 + fast-safe-stringify: 2.1.1 + iterare: 1.2.1 + path-to-regexp: 3.2.0 + reflect-metadata: 0.2.0 + rxjs: 7.8.1 + tslib: 2.5.3 + uid: 2.0.2 + transitivePeerDependencies: + - encoding + + /@nestjs/jwt@10.2.0(@nestjs/common@10.0.0): + resolution: {integrity: sha512-x8cG90SURkEiLOehNaN2aRlotxT0KZESUliOPKKnjWiyJOcWurkF3w345WOX0P4MgFzUjGoZ1Sy0aZnxeihT0g==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + dependencies: + '@nestjs/common': 10.0.0(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.0)(rxjs@7.8.1) + '@types/jsonwebtoken': 9.0.5 + jsonwebtoken: 9.0.2 + dev: false + + /@nestjs/mapped-types@2.0.6(@nestjs/common@10.0.0)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.0): + resolution: {integrity: sha512-84ze+CPfp1OWdpRi1/lOu59hOhTz38eVzJvRKrg9ykRFwDz+XleKfMsG0gUqNZYFa6v53XYzeD+xItt8uDW7NQ==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + class-transformer: ^0.4.0 || ^0.5.0 + class-validator: ^0.13.0 || ^0.14.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + dependencies: + '@nestjs/common': 10.0.0(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.0)(rxjs@7.8.1) + class-transformer: 0.5.1 + class-validator: 0.14.1 + reflect-metadata: 0.2.0 + dev: false + + /@nestjs/platform-express@10.0.0(@nestjs/common@10.0.0)(@nestjs/core@10.0.0): + resolution: {integrity: sha512-jOQBPVpk7B4JFXZZwxHSsY6odIqZlea9CbqKzu/hfDyqRv+AwuJk5gprvvL6RpWAHNyRMH1r5/14bqcXD3+WGw==} + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/core': ^10.0.0 + dependencies: + '@nestjs/common': 10.0.0(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.0)(rxjs@7.8.1) + '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(reflect-metadata@0.2.0)(rxjs@7.8.1) + body-parser: 1.20.2 + cors: 2.8.5 + express: 4.18.2 + multer: 1.4.4-lts.1 + tslib: 2.5.3 + transitivePeerDependencies: + - supports-color + + /@nestjs/schematics@10.0.0(chokidar@3.5.3)(typescript@5.1.3): + resolution: {integrity: sha512-gfUy/N1m1paN33BXq4d7HoCM+zM4rFxYjqAb8jkrBfBHiwyEhHHozfX/aRy/kOnAcy/VP8v4Zs4HKKrbRRlHnw==} + peerDependencies: + typescript: '>=4.8.2' + dependencies: + '@angular-devkit/core': 16.1.0(chokidar@3.5.3) + '@angular-devkit/schematics': 16.1.0(chokidar@3.5.3) + comment-json: 4.2.3 + jsonc-parser: 3.2.0 + pluralize: 8.0.0 + typescript: 5.1.3 + transitivePeerDependencies: + - chokidar + dev: true + + /@nestjs/swagger@8.0.5(@nestjs/common@10.0.0)(@nestjs/core@10.0.0)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.0): + resolution: {integrity: sha512-ZmBdsbQNs3wIN5kCuvAVbz3/ULh3gi814oHTP49uTqAGi1aT0YSatUyncwQOHBOlRT+rwF+TNjoAsZ+twIk/Jw==} + peerDependencies: + '@fastify/static': ^6.0.0 || ^7.0.0 + '@nestjs/common': ^9.0.0 || ^10.0.0 + '@nestjs/core': ^9.0.0 || ^10.0.0 + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + '@fastify/static': + optional: true + class-transformer: + optional: true + class-validator: + optional: true + dependencies: + '@microsoft/tsdoc': 0.15.0 + '@nestjs/common': 10.0.0(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.0)(rxjs@7.8.1) + '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(reflect-metadata@0.2.0)(rxjs@7.8.1) + '@nestjs/mapped-types': 2.0.6(@nestjs/common@10.0.0)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.0) + class-transformer: 0.5.1 + class-validator: 0.14.1 + js-yaml: 4.1.0 + lodash: 4.17.21 + path-to-regexp: 3.3.0 + reflect-metadata: 0.2.0 + swagger-ui-dist: 5.18.2 + dev: false + + /@nestjs/testing@10.0.0(@nestjs/common@10.0.0)(@nestjs/core@10.0.0)(@nestjs/platform-express@10.0.0): + resolution: {integrity: sha512-U5q3+svkddpdSk51ZFCEnFpQuWxAwE4ahsX77FrqqCAYidr7HUtL/BHYOVzI5H9vUH6BvJxMbfo3tiUXQl/2aA==} + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/core': ^10.0.0 + '@nestjs/microservices': ^10.0.0 + '@nestjs/platform-express': ^10.0.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + dependencies: + '@nestjs/common': 10.0.0(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.0)(rxjs@7.8.1) + '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(reflect-metadata@0.2.0)(rxjs@7.8.1) + '@nestjs/platform-express': 10.0.0(@nestjs/common@10.0.0)(@nestjs/core@10.0.0) + tslib: 2.5.3 + dev: true + + /@nestjs/typeorm@10.0.2(@nestjs/common@10.0.0)(@nestjs/core@10.0.0)(reflect-metadata@0.2.0)(rxjs@7.8.1)(typeorm@0.3.20): + resolution: {integrity: sha512-H738bJyydK4SQkRCTeh1aFBxoO1E9xdL/HaLGThwrqN95os5mEyAtK7BLADOS+vldP4jDZ2VQPLj4epWwRqCeQ==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 + reflect-metadata: ^0.1.13 || ^0.2.0 + rxjs: ^7.2.0 + typeorm: ^0.3.0 + dependencies: + '@nestjs/common': 10.0.0(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.0)(rxjs@7.8.1) + '@nestjs/core': 10.0.0(@nestjs/common@10.0.0)(@nestjs/platform-express@10.0.0)(reflect-metadata@0.2.0)(rxjs@7.8.1) + reflect-metadata: 0.2.0 + rxjs: 7.8.1 + typeorm: 0.3.20(mysql2@3.11.4)(pg@8.13.1)(ts-node@10.9.1) + uuid: 9.0.1 + dev: false + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + /@nuxtjs/opencollective@0.3.2: + resolution: {integrity: sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==} + engines: {node: '>=8.0.0', npm: '>=5.0.0'} + hasBin: true + dependencies: + chalk: 4.1.2 + consola: 2.15.3 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + /@phc/format@1.0.0: + resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==} + engines: {node: '>=10'} + dev: false + + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + dev: false + optional: true + + /@pkgr/core@0.1.1: + resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + dev: true + + /@scarf/scarf@1.4.0: + resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + requiresBuild: true + dev: false + + /@sideway/address@4.1.5: + resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} + dependencies: + '@hapi/hoek': 9.3.0 + dev: false + + /@sideway/formula@3.0.1: + resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==} + dev: false + + /@sideway/pinpoint@2.0.0: + resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} + dev: false + + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: true + + /@sinonjs/commons@3.0.1: + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + dependencies: + type-detect: 4.0.8 + dev: true + + /@sinonjs/fake-timers@10.3.0: + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + dependencies: + '@sinonjs/commons': 3.0.1 + dev: true + + /@smithy/abort-controller@3.1.8: + resolution: {integrity: sha512-+3DOBcUn5/rVjlxGvUPKc416SExarAQ+Qe0bqk30YSUjbepwpS7QN0cyKUSifvLJhdMZ0WPzPP5ymut0oonrpQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/chunked-blob-reader-native@3.0.1: + resolution: {integrity: sha512-VEYtPvh5rs/xlyqpm5NRnfYLZn+q0SRPELbvBV+C/G7IQ+ouTuo+NKKa3ShG5OaFR8NYVMXls9hPYLTvIKKDrQ==} + dependencies: + '@smithy/util-base64': 3.0.0 + tslib: 2.8.1 + dev: false + + /@smithy/chunked-blob-reader@4.0.0: + resolution: {integrity: sha512-jSqRnZvkT4egkq/7b6/QRCNXmmYVcHwnJldqJ3IhVpQE2atObVJ137xmGeuGFhjFUr8gCEVAOKwSY79OvpbDaQ==} + dependencies: + tslib: 2.8.1 + dev: false + + /@smithy/config-resolver@3.0.12: + resolution: {integrity: sha512-YAJP9UJFZRZ8N+UruTeq78zkdjUHmzsY62J4qKWZ4SXB4QXJ/+680EfXXgkYA2xj77ooMqtUY9m406zGNqwivQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/node-config-provider': 3.1.11 + '@smithy/types': 3.7.1 + '@smithy/util-config-provider': 3.0.0 + '@smithy/util-middleware': 3.0.10 + tslib: 2.8.1 + dev: false + + /@smithy/core@2.5.3: + resolution: {integrity: sha512-96uW8maifUSmehaeW7uydWn7wBc98NEeNI3zN8vqakGpyCQgzyJaA64Z4FCOUmAdCJkhppd/7SZ798Fo4Xx37g==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/middleware-serde': 3.0.10 + '@smithy/protocol-http': 4.1.7 + '@smithy/types': 3.7.1 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-middleware': 3.0.10 + '@smithy/util-stream': 3.3.1 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + dev: false + + /@smithy/credential-provider-imds@3.2.7: + resolution: {integrity: sha512-cEfbau+rrWF8ylkmmVAObOmjbTIzKyUC5TkBL58SbLywD0RCBC4JAUKbmtSm2w5KUJNRPGgpGFMvE2FKnuNlWQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/node-config-provider': 3.1.11 + '@smithy/property-provider': 3.1.10 + '@smithy/types': 3.7.1 + '@smithy/url-parser': 3.0.10 + tslib: 2.8.1 + dev: false + + /@smithy/eventstream-codec@3.1.9: + resolution: {integrity: sha512-F574nX0hhlNOjBnP+noLtsPFqXnWh2L0+nZKCwcu7P7J8k+k+rdIDs+RMnrMwrzhUE4mwMgyN0cYnEn0G8yrnQ==} + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 3.7.1 + '@smithy/util-hex-encoding': 3.0.0 + tslib: 2.8.1 + dev: false + + /@smithy/eventstream-serde-browser@3.0.13: + resolution: {integrity: sha512-Nee9m+97o9Qj6/XeLz2g2vANS2SZgAxV4rDBMKGHvFJHU/xz88x2RwCkwsvEwYjSX4BV1NG1JXmxEaDUzZTAtw==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/eventstream-serde-universal': 3.0.12 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/eventstream-serde-config-resolver@3.0.10: + resolution: {integrity: sha512-K1M0x7P7qbBUKB0UWIL5KOcyi6zqV5mPJoL0/o01HPJr0CSq3A9FYuJC6e11EX6hR8QTIR++DBiGrYveOu6trw==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/eventstream-serde-node@3.0.12: + resolution: {integrity: sha512-kiZymxXvZ4tnuYsPSMUHe+MMfc4FTeFWJIc0Q5wygJoUQM4rVHNghvd48y7ppuulNMbuYt95ah71pYc2+o4JOA==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/eventstream-serde-universal': 3.0.12 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/eventstream-serde-universal@3.0.12: + resolution: {integrity: sha512-1i8ifhLJrOZ+pEifTlF0EfZzMLUGQggYQ6WmZ4d5g77zEKf7oZ0kvh1yKWHPjofvOwqrkwRDVuxuYC8wVd662A==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/eventstream-codec': 3.1.9 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/fetch-http-handler@4.1.1: + resolution: {integrity: sha512-bH7QW0+JdX0bPBadXt8GwMof/jz0H28I84hU1Uet9ISpzUqXqRQ3fEZJ+ANPOhzSEczYvANNl3uDQDYArSFDtA==} + dependencies: + '@smithy/protocol-http': 4.1.7 + '@smithy/querystring-builder': 3.0.10 + '@smithy/types': 3.7.1 + '@smithy/util-base64': 3.0.0 + tslib: 2.8.1 + dev: false + + /@smithy/hash-blob-browser@3.1.9: + resolution: {integrity: sha512-wOu78omaUuW5DE+PVWXiRKWRZLecARyP3xcq5SmkXUw9+utgN8HnSnBfrjL2B/4ZxgqPjaAJQkC/+JHf1ITVaQ==} + dependencies: + '@smithy/chunked-blob-reader': 4.0.0 + '@smithy/chunked-blob-reader-native': 3.0.1 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/hash-node@3.0.10: + resolution: {integrity: sha512-3zWGWCHI+FlJ5WJwx73Mw2llYR8aflVyZN5JhoqLxbdPZi6UyKSdCeXAWJw9ja22m6S6Tzz1KZ+kAaSwvydi0g==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + '@smithy/util-buffer-from': 3.0.0 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + dev: false + + /@smithy/hash-stream-node@3.1.9: + resolution: {integrity: sha512-3XfHBjSP3oDWxLmlxnt+F+FqXpL3WlXs+XXaB6bV9Wo8BBu87fK1dSEsyH7Z4ZHRmwZ4g9lFMdf08m9hoX1iRA==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + dev: false + + /@smithy/invalid-dependency@3.0.10: + resolution: {integrity: sha512-Lp2L65vFi+cj0vFMu2obpPW69DU+6O5g3086lmI4XcnRCG8PxvpWC7XyaVwJCxsZFzueHjXnrOH/E0pl0zikfA==} + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/is-array-buffer@2.2.0: + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.8.1 + dev: false + + /@smithy/is-array-buffer@3.0.0: + resolution: {integrity: sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==} + engines: {node: '>=16.0.0'} + dependencies: + tslib: 2.8.1 + dev: false + + /@smithy/md5-js@3.0.10: + resolution: {integrity: sha512-m3bv6dApflt3fS2Y1PyWPUtRP7iuBlvikEOGwu0HsCZ0vE7zcIX+dBoh3e+31/rddagw8nj92j0kJg2TfV+SJA==} + dependencies: + '@smithy/types': 3.7.1 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + dev: false + + /@smithy/middleware-content-length@3.0.12: + resolution: {integrity: sha512-1mDEXqzM20yywaMDuf5o9ue8OkJ373lSPbaSjyEvkWdqELhFMyNNgKGWL/rCSf4KME8B+HlHKuR8u9kRj8HzEQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/protocol-http': 4.1.7 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/middleware-endpoint@3.2.3: + resolution: {integrity: sha512-Hdl9296i/EMptaX7agrSzJZDiz5Y8XPUeBbctTmMtnCguGpqfU3jVsTUan0VLaOhsnquqWLL8Bl5HrlbVGT1og==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/core': 2.5.3 + '@smithy/middleware-serde': 3.0.10 + '@smithy/node-config-provider': 3.1.11 + '@smithy/shared-ini-file-loader': 3.1.11 + '@smithy/types': 3.7.1 + '@smithy/url-parser': 3.0.10 + '@smithy/util-middleware': 3.0.10 + tslib: 2.8.1 + dev: false + + /@smithy/middleware-retry@3.0.27: + resolution: {integrity: sha512-H3J/PjJpLL7Tt+fxDKiOD25sMc94YetlQhCnYeNmina2LZscAdu0ZEZPas/kwePHABaEtqp7hqa5S4UJgMs1Tg==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/node-config-provider': 3.1.11 + '@smithy/protocol-http': 4.1.7 + '@smithy/service-error-classification': 3.0.10 + '@smithy/smithy-client': 3.4.4 + '@smithy/types': 3.7.1 + '@smithy/util-middleware': 3.0.10 + '@smithy/util-retry': 3.0.10 + tslib: 2.8.1 + uuid: 9.0.1 + dev: false + + /@smithy/middleware-serde@3.0.10: + resolution: {integrity: sha512-MnAuhh+dD14F428ubSJuRnmRsfOpxSzvRhaGVTvd/lrUDE3kxzCCmH8lnVTvoNQnV2BbJ4c15QwZ3UdQBtFNZA==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/middleware-stack@3.0.10: + resolution: {integrity: sha512-grCHyoiARDBBGPyw2BeicpjgpsDFWZZxptbVKb3CRd/ZA15F/T6rZjCCuBUjJwdck1nwUuIxYtsS4H9DDpbP5w==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/node-config-provider@3.1.11: + resolution: {integrity: sha512-URq3gT3RpDikh/8MBJUB+QGZzfS7Bm6TQTqoh4CqE8NBuyPkWa5eUXj0XFcFfeZVgg3WMh1u19iaXn8FvvXxZw==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/property-provider': 3.1.10 + '@smithy/shared-ini-file-loader': 3.1.11 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/node-http-handler@3.3.1: + resolution: {integrity: sha512-fr+UAOMGWh6bn4YSEezBCpJn9Ukp9oR4D32sCjCo7U81evE11YePOQ58ogzyfgmjIO79YeOdfXXqr0jyhPQeMg==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/abort-controller': 3.1.8 + '@smithy/protocol-http': 4.1.7 + '@smithy/querystring-builder': 3.0.10 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/property-provider@3.1.10: + resolution: {integrity: sha512-n1MJZGTorTH2DvyTVj+3wXnd4CzjJxyXeOgnTlgNVFxaaMeT4OteEp4QrzF8p9ee2yg42nvyVK6R/awLCakjeQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/protocol-http@4.1.7: + resolution: {integrity: sha512-FP2LepWD0eJeOTm0SjssPcgqAlDFzOmRXqXmGhfIM52G7Lrox/pcpQf6RP4F21k0+O12zaqQt5fCDOeBtqY6Cg==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/querystring-builder@3.0.10: + resolution: {integrity: sha512-nT9CQF3EIJtIUepXQuBFb8dxJi3WVZS3XfuDksxSCSn+/CzZowRLdhDn+2acbBv8R6eaJqPupoI/aRFIImNVPQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + '@smithy/util-uri-escape': 3.0.0 + tslib: 2.8.1 + dev: false + + /@smithy/querystring-parser@3.0.10: + resolution: {integrity: sha512-Oa0XDcpo9SmjhiDD9ua2UyM3uU01ZTuIrNdZvzwUTykW1PM8o2yJvMh1Do1rY5sUQg4NDV70dMi0JhDx4GyxuQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/service-error-classification@3.0.10: + resolution: {integrity: sha512-zHe642KCqDxXLuhs6xmHVgRwy078RfqxP2wRDpIyiF8EmsWXptMwnMwbVa50lw+WOGNrYm9zbaEg0oDe3PTtvQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + dev: false + + /@smithy/shared-ini-file-loader@3.1.11: + resolution: {integrity: sha512-AUdrIZHFtUgmfSN4Gq9nHu3IkHMa1YDcN+s061Nfm+6pQ0mJy85YQDB0tZBCmls0Vuj22pLwDPmL92+Hvfwwlg==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/signature-v4@4.2.3: + resolution: {integrity: sha512-pPSQQ2v2vu9vc8iew7sszLd0O09I5TRc5zhY71KA+Ao0xYazIG+uLeHbTJfIWGO3BGVLiXjUr3EEeCcEQLjpWQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/is-array-buffer': 3.0.0 + '@smithy/protocol-http': 4.1.7 + '@smithy/types': 3.7.1 + '@smithy/util-hex-encoding': 3.0.0 + '@smithy/util-middleware': 3.0.10 + '@smithy/util-uri-escape': 3.0.0 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + dev: false + + /@smithy/smithy-client@3.4.4: + resolution: {integrity: sha512-dPGoJuSZqvirBq+yROapBcHHvFjChoAQT8YPWJ820aPHHiowBlB3RL1Q4kPT1hx0qKgJuf+HhyzKi5Gbof4fNA==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/core': 2.5.3 + '@smithy/middleware-endpoint': 3.2.3 + '@smithy/middleware-stack': 3.0.10 + '@smithy/protocol-http': 4.1.7 + '@smithy/types': 3.7.1 + '@smithy/util-stream': 3.3.1 + tslib: 2.8.1 + dev: false + + /@smithy/types@3.7.1: + resolution: {integrity: sha512-XKLcLXZY7sUQgvvWyeaL/qwNPp6V3dWcUjqrQKjSb+tzYiCy340R/c64LV5j+Tnb2GhmunEX0eou+L+m2hJNYA==} + engines: {node: '>=16.0.0'} + dependencies: + tslib: 2.8.1 + dev: false + + /@smithy/url-parser@3.0.10: + resolution: {integrity: sha512-j90NUalTSBR2NaZTuruEgavSdh8MLirf58LoGSk4AtQfyIymogIhgnGUU2Mga2bkMkpSoC9gxb74xBXL5afKAQ==} + dependencies: + '@smithy/querystring-parser': 3.0.10 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/util-base64@3.0.0: + resolution: {integrity: sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/util-buffer-from': 3.0.0 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + dev: false + + /@smithy/util-body-length-browser@3.0.0: + resolution: {integrity: sha512-cbjJs2A1mLYmqmyVl80uoLTJhAcfzMOyPgjwAYusWKMdLeNtzmMz9YxNl3/jRLoxSS3wkqkf0jwNdtXWtyEBaQ==} + dependencies: + tslib: 2.8.1 + dev: false + + /@smithy/util-body-length-node@3.0.0: + resolution: {integrity: sha512-Tj7pZ4bUloNUP6PzwhN7K386tmSmEET9QtQg0TgdNOnxhZvCssHji+oZTUIuzxECRfG8rdm2PMw2WCFs6eIYkA==} + engines: {node: '>=16.0.0'} + dependencies: + tslib: 2.8.1 + dev: false + + /@smithy/util-buffer-from@2.2.0: + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + dev: false + + /@smithy/util-buffer-from@3.0.0: + resolution: {integrity: sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/is-array-buffer': 3.0.0 + tslib: 2.8.1 + dev: false + + /@smithy/util-config-provider@3.0.0: + resolution: {integrity: sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ==} + engines: {node: '>=16.0.0'} + dependencies: + tslib: 2.8.1 + dev: false + + /@smithy/util-defaults-mode-browser@3.0.27: + resolution: {integrity: sha512-GV8NvPy1vAGp7u5iD/xNKUxCorE4nQzlyl057qRac+KwpH5zq8wVq6rE3lPPeuFLyQXofPN6JwxL1N9ojGapiQ==} + engines: {node: '>= 10.0.0'} + dependencies: + '@smithy/property-provider': 3.1.10 + '@smithy/smithy-client': 3.4.4 + '@smithy/types': 3.7.1 + bowser: 2.11.0 + tslib: 2.8.1 + dev: false + + /@smithy/util-defaults-mode-node@3.0.27: + resolution: {integrity: sha512-7+4wjWfZqZxZVJvDutO+i1GvL6bgOajEkop4FuR6wudFlqBiqwxw3HoH6M9NgeCd37km8ga8NPp2JacQEtAMPg==} + engines: {node: '>= 10.0.0'} + dependencies: + '@smithy/config-resolver': 3.0.12 + '@smithy/credential-provider-imds': 3.2.7 + '@smithy/node-config-provider': 3.1.11 + '@smithy/property-provider': 3.1.10 + '@smithy/smithy-client': 3.4.4 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/util-endpoints@2.1.6: + resolution: {integrity: sha512-mFV1t3ndBh0yZOJgWxO9J/4cHZVn5UG1D8DeCc6/echfNkeEJWu9LD7mgGH5fHrEdR7LDoWw7PQO6QiGpHXhgA==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/node-config-provider': 3.1.11 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/util-hex-encoding@3.0.0: + resolution: {integrity: sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==} + engines: {node: '>=16.0.0'} + dependencies: + tslib: 2.8.1 + dev: false + + /@smithy/util-middleware@3.0.10: + resolution: {integrity: sha512-eJO+/+RsrG2RpmY68jZdwQtnfsxjmPxzMlQpnHKjFPwrYqvlcT+fHdT+ZVwcjlWSrByOhGr9Ff2GG17efc192A==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/util-retry@3.0.10: + resolution: {integrity: sha512-1l4qatFp4PiU6j7UsbasUHL2VU023NRB/gfaa1M0rDqVrRN4g3mCArLRyH3OuktApA4ye+yjWQHjdziunw2eWA==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/service-error-classification': 3.0.10 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/util-stream@3.3.1: + resolution: {integrity: sha512-Ff68R5lJh2zj+AUTvbAU/4yx+6QPRzg7+pI7M1FbtQHcRIp7xvguxVsQBKyB3fwiOwhAKu0lnNyYBaQfSW6TNw==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/fetch-http-handler': 4.1.1 + '@smithy/node-http-handler': 3.3.1 + '@smithy/types': 3.7.1 + '@smithy/util-base64': 3.0.0 + '@smithy/util-buffer-from': 3.0.0 + '@smithy/util-hex-encoding': 3.0.0 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + dev: false + + /@smithy/util-uri-escape@3.0.0: + resolution: {integrity: sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==} + engines: {node: '>=16.0.0'} + dependencies: + tslib: 2.8.1 + dev: false + + /@smithy/util-utf8@2.3.0: + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + dev: false + + /@smithy/util-utf8@3.0.0: + resolution: {integrity: sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/util-buffer-from': 3.0.0 + tslib: 2.8.1 + dev: false + + /@smithy/util-waiter@3.1.9: + resolution: {integrity: sha512-/aMXPANhMOlMPjfPtSrDfPeVP8l56SJlz93xeiLmhLe5xvlXA5T3abZ2ilEsDEPeY9T/wnN/vNGn9wa1SbufWA==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/abort-controller': 3.1.8 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@sqltools/formatter@1.2.5: + resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} + dev: false + + /@tsconfig/node10@1.0.11: + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + /@tsconfig/node12@1.0.11: + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + /@tsconfig/node14@1.0.3: + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + /@tsconfig/node16@1.0.4: + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + /@types/babel__core@7.20.5: + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.6 + dev: true + + /@types/babel__generator@7.6.8: + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + dependencies: + '@babel/types': 7.26.0 + dev: true + + /@types/babel__template@7.4.4: + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + dev: true + + /@types/babel__traverse@7.20.6: + resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + dependencies: + '@babel/types': 7.26.0 + dev: true + + /@types/body-parser@1.19.5: + resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + dependencies: + '@types/connect': 3.4.38 + '@types/node': 20.3.1 + dev: true + + /@types/connect@3.4.38: + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + dependencies: + '@types/node': 20.3.1 + dev: true + + /@types/cookiejar@2.1.5: + resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + dev: true + + /@types/eslint-scope@3.7.7: + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} + dependencies: + '@types/eslint': 9.6.1 + '@types/estree': 1.0.6 + dev: true + + /@types/eslint@9.6.1: + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + dependencies: + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 + dev: true + + /@types/estree@1.0.6: + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + dev: true + + /@types/express-serve-static-core@5.0.1: + resolution: {integrity: sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==} + dependencies: + '@types/node': 20.3.1 + '@types/qs': 6.9.17 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.4 + dev: true + + /@types/express@5.0.0: + resolution: {integrity: sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==} + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 5.0.1 + '@types/qs': 6.9.17 + '@types/serve-static': 1.15.7 + dev: true + + /@types/graceful-fs@4.1.9: + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + dependencies: + '@types/node': 20.3.1 + dev: true + + /@types/http-errors@2.0.4: + resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + dev: true + + /@types/istanbul-lib-coverage@2.0.6: + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + dev: true + + /@types/istanbul-lib-report@3.0.3: + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + dev: true + + /@types/istanbul-reports@3.0.4: + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + dependencies: + '@types/istanbul-lib-report': 3.0.3 + dev: true + + /@types/jest@29.5.2: + resolution: {integrity: sha512-mSoZVJF5YzGVCk+FsDxzDuH7s+SCkzrgKZzf0Z0T2WudhBUPoF6ktoTPC4R0ZoCPCV5xUvuU6ias5NvxcBcMMg==} + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + dev: true + + /@types/json-schema@7.0.15: + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + dev: true + + /@types/jsonwebtoken@9.0.5: + resolution: {integrity: sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==} + dependencies: + '@types/node': 20.3.1 + dev: false + + /@types/methods@1.1.4: + resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + dev: true + + /@types/mime@1.3.5: + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + dev: true + + /@types/multer@1.4.12: + resolution: {integrity: sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==} + dependencies: + '@types/express': 5.0.0 + dev: true + + /@types/node@20.3.1: + resolution: {integrity: sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==} + + /@types/parse-json@4.0.2: + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + dev: true + + /@types/qs@6.9.17: + resolution: {integrity: sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==} + dev: true + + /@types/range-parser@1.2.7: + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + dev: true + + /@types/send@0.17.4: + resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} + dependencies: + '@types/mime': 1.3.5 + '@types/node': 20.3.1 + dev: true + + /@types/serve-static@1.15.7: + resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + dependencies: + '@types/http-errors': 2.0.4 + '@types/node': 20.3.1 + '@types/send': 0.17.4 + dev: true + + /@types/stack-utils@2.0.3: + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + dev: true + + /@types/superagent@8.1.9: + resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} + dependencies: + '@types/cookiejar': 2.1.5 + '@types/methods': 1.1.4 + '@types/node': 20.3.1 + form-data: 4.0.1 + dev: true + + /@types/supertest@6.0.0: + resolution: {integrity: sha512-j3/Z2avY+H3yn+xp/ef//QyqqE+dg3rWh14Ewi/QZs6uVK+oOs7lFRXtjp2YHAqHJZ4OFGNmCxZO5vd7AuG/Dg==} + dependencies: + '@types/cookiejar': 2.1.5 + '@types/methods': 1.1.4 + '@types/superagent': 8.1.9 + dev: true + + /@types/validator@13.12.2: + resolution: {integrity: sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==} + + /@types/yargs-parser@21.0.3: + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + dev: true + + /@types/yargs@17.0.33: + resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} + dependencies: + '@types/yargs-parser': 21.0.3 + dev: true + + /@typescript-eslint/eslint-plugin@8.0.0(@typescript-eslint/parser@8.0.0)(eslint@8.0.0)(typescript@5.1.3): + resolution: {integrity: sha512-STIZdwEQRXAHvNUS6ILDf5z3u95Gc8jzywunxSNqX00OooIemaaNIA0vEgynJlycL5AjabYLLrIyHd4iazyvtg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.0.0(eslint@8.0.0)(typescript@5.1.3) + '@typescript-eslint/scope-manager': 8.0.0 + '@typescript-eslint/type-utils': 8.0.0(eslint@8.0.0)(typescript@5.1.3) + '@typescript-eslint/utils': 8.0.0(eslint@8.0.0)(typescript@5.1.3) + '@typescript-eslint/visitor-keys': 8.0.0 + eslint: 8.0.0 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.4.0(typescript@5.1.3) + typescript: 5.1.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@8.0.0(eslint@8.0.0)(typescript@5.1.3): + resolution: {integrity: sha512-pS1hdZ+vnrpDIxuFXYQpLTILglTjSYJ9MbetZctrUawogUsPdz31DIIRZ9+rab0LhYNTsk88w4fIzVheiTbWOQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 8.0.0 + '@typescript-eslint/types': 8.0.0 + '@typescript-eslint/typescript-estree': 8.0.0(typescript@5.1.3) + '@typescript-eslint/visitor-keys': 8.0.0 + debug: 4.3.7 + eslint: 8.0.0 + typescript: 5.1.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/scope-manager@8.0.0: + resolution: {integrity: sha512-V0aa9Csx/ZWWv2IPgTfY7T4agYwJyILESu/PVqFtTFz9RIS823mAze+NbnBI8xiwdX3iqeQbcTYlvB04G9wyQw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dependencies: + '@typescript-eslint/types': 8.0.0 + '@typescript-eslint/visitor-keys': 8.0.0 + dev: true + + /@typescript-eslint/type-utils@8.0.0(eslint@8.0.0)(typescript@5.1.3): + resolution: {integrity: sha512-mJAFP2mZLTBwAn5WI4PMakpywfWFH5nQZezUQdSKV23Pqo6o9iShQg1hP2+0hJJXP2LnZkWPphdIq4juYYwCeg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 8.0.0(typescript@5.1.3) + '@typescript-eslint/utils': 8.0.0(eslint@8.0.0)(typescript@5.1.3) + debug: 4.3.7 + ts-api-utils: 1.4.0(typescript@5.1.3) + typescript: 5.1.3 + transitivePeerDependencies: + - eslint + - supports-color + dev: true + + /@typescript-eslint/types@8.0.0: + resolution: {integrity: sha512-wgdSGs9BTMWQ7ooeHtu5quddKKs5Z5dS+fHLbrQI+ID0XWJLODGMHRfhwImiHoeO2S5Wir2yXuadJN6/l4JRxw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dev: true + + /@typescript-eslint/typescript-estree@8.0.0(typescript@5.1.3): + resolution: {integrity: sha512-5b97WpKMX+Y43YKi4zVcCVLtK5F98dFls3Oxui8LbnmRsseKenbbDinmvxrWegKDMmlkIq/XHuyy0UGLtpCDKg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 8.0.0 + '@typescript-eslint/visitor-keys': 8.0.0 + debug: 4.3.7 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 1.4.0(typescript@5.1.3) + typescript: 5.1.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/utils@8.0.0(eslint@8.0.0)(typescript@5.1.3): + resolution: {integrity: sha512-k/oS/A/3QeGLRvOWCg6/9rATJL5rec7/5s1YmdS0ZU6LHveJyGFwBvLhSRBv6i9xaj7etmosp+l+ViN1I9Aj/Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@8.0.0) + '@typescript-eslint/scope-manager': 8.0.0 + '@typescript-eslint/types': 8.0.0 + '@typescript-eslint/typescript-estree': 8.0.0(typescript@5.1.3) + eslint: 8.0.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/visitor-keys@8.0.0: + resolution: {integrity: sha512-oN0K4nkHuOyF3PVMyETbpP5zp6wfyOvm7tWhTMfoqxSSsPmJIh6JNASuZDlODE8eE+0EB9uar+6+vxr9DBTYOA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dependencies: + '@typescript-eslint/types': 8.0.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@webassemblyjs/ast@1.14.1: + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + dev: true + + /@webassemblyjs/floating-point-hex-parser@1.13.2: + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} + dev: true + + /@webassemblyjs/helper-api-error@1.13.2: + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} + dev: true + + /@webassemblyjs/helper-buffer@1.14.1: + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} + dev: true + + /@webassemblyjs/helper-numbers@1.13.2: + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + dev: true + + /@webassemblyjs/helper-wasm-bytecode@1.13.2: + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} + dev: true + + /@webassemblyjs/helper-wasm-section@1.14.1: + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + dev: true + + /@webassemblyjs/ieee754@1.13.2: + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} + dependencies: + '@xtuc/ieee754': 1.2.0 + dev: true + + /@webassemblyjs/leb128@1.13.2: + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} + dependencies: + '@xtuc/long': 4.2.2 + dev: true + + /@webassemblyjs/utf8@1.13.2: + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} + dev: true + + /@webassemblyjs/wasm-edit@1.14.1: + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + dev: true + + /@webassemblyjs/wasm-gen@1.14.1: + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + dev: true + + /@webassemblyjs/wasm-opt@1.14.1: + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + dev: true + + /@webassemblyjs/wasm-parser@1.14.1: + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + dev: true + + /@webassemblyjs/wast-printer@1.14.1: + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + dev: true + + /@xtuc/ieee754@1.2.0: + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + dev: true + + /@xtuc/long@4.2.2: + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + dev: true + + /accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + /acorn-import-assertions@1.9.0(acorn@8.14.0): + resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} + deprecated: package has been renamed to acorn-import-attributes + peerDependencies: + acorn: ^8 + dependencies: + acorn: 8.14.0 + dev: true + + /acorn-jsx@5.3.2(acorn@8.14.0): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.14.0 + dev: true + + /acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + dependencies: + acorn: 8.14.0 + + /acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + + /ajv-formats@2.1.1(ajv@8.12.0): + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + dependencies: + ajv: 8.12.0 + dev: true + + /ajv-keywords@3.5.2(ajv@6.12.6): + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + dependencies: + ajv: 6.12.6 + dev: true + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + + /ajv@8.12.0: + resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + dev: true + + /ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + dev: true + + /ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.21.3 + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + /ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + dev: false + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + + /ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + dev: true + + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: false + + /any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + dev: false + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /app-root-path@3.1.0: + resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==} + engines: {node: '>= 6.0.0'} + dev: false + + /append-field@1.0.0: + resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + + /arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + /argon2@0.41.1: + resolution: {integrity: sha512-dqCW8kJXke8Ik+McUcMDltrbuAWETPyU6iq+4AhxqKphWi7pChB/Zgd/Tp/o8xRLbg8ksMj46F/vph9wnxpTzQ==} + engines: {node: '>=16.17.0'} + requiresBuild: true + dependencies: + '@phc/format': 1.0.0 + node-addon-api: 8.2.2 + node-gyp-build: 4.8.2 + dev: false + + /argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + dependencies: + sprintf-js: 1.0.3 + dev: true + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + /array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + /array-timsort@1.0.3: + resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + dev: true + + /array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + dev: true + + /asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + dev: true + + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + /aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + dev: false + + /axios@1.7.7: + resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.1 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + + /babel-jest@29.7.0(@babel/core@7.26.0): + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + dependencies: + '@babel/core': 7.26.0 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.26.0) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + dependencies: + '@babel/helper-plugin-utils': 7.25.9 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.20.6 + dev: true + + /babel-preset-current-node-syntax@1.1.0(@babel/core@7.26.0): + resolution: {integrity: sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.26.0) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.26.0) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.26.0) + '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.26.0) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.26.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.26.0) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.26.0) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.26.0) + dev: true + + /babel-preset-jest@29.6.3(@babel/core@7.26.0): + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.0) + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + /binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + dev: true + + /bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: true + + /body-parser@1.20.1: + resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + 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.11.0 + raw-body: 2.5.1 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + /body-parser@1.20.2: + resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + 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.11.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + /bowser@2.11.0: + resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} + dev: false + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + + /braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.1.1 + + /browserslist@4.24.2: + resolution: {integrity: sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001679 + electron-to-chromium: 1.5.55 + node-releases: 2.0.18 + update-browserslist-db: 1.1.1(browserslist@4.24.2) + dev: true + + /bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + dependencies: + fast-json-stable-stringify: 2.1.0 + dev: true + + /bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + dependencies: + node-int64: 0.4.0 + dev: true + + /buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + dev: false + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + /buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: true + + /buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: false + + /busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + dependencies: + streamsearch: 1.1.0 + + /bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + /call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + 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.2 + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + dev: true + + /camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + dev: true + + /camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + dev: true + + /caniuse-lite@1.0.30001679: + resolution: {integrity: sha512-j2YqID/YwpLnKzCmBOS4tlZdWprXm3ZmQLBH9ZBXFOhoxLA46fwyBvx6toCBWBmnuwUY/qB3kEU6gFx8qgCroA==} + dev: true + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + /char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + dev: true + + /chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + dev: true + + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + dev: true + + /ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + dev: true + + /cjs-module-lexer@1.4.1: + resolution: {integrity: sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==} + dev: true + + /class-transformer@0.5.1: + resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} + + /class-validator@0.14.1: + resolution: {integrity: sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==} + dependencies: + '@types/validator': 13.12.2 + libphonenumber-js: 1.11.14 + validator: 13.12.0 + + /cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + dependencies: + restore-cursor: 3.1.0 + dev: true + + /cli-highlight@2.1.11: + resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} + engines: {node: '>=8.0.0', npm: '>=5.0.0'} + hasBin: true + dependencies: + chalk: 4.1.2 + highlight.js: 10.7.3 + mz: 2.7.0 + parse5: 5.1.1 + parse5-htmlparser2-tree-adapter: 6.0.1 + yargs: 16.2.0 + dev: false + + /cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + dev: true + + /cli-table3@0.6.3: + resolution: {integrity: sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==} + engines: {node: 10.* || >= 12.*} + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + dev: true + + /cli-width@3.0.0: + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} + dev: true + + /cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: false + + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + /clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + dev: true + + /co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + dev: true + + /collect-v8-coverage@1.0.2: + resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} + dev: true + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + + /commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + dev: true + + /commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + dev: true + + /comment-json@4.2.3: + resolution: {integrity: sha512-SsxdiOf064DWoZLH799Ata6u7iV658A11PlWtZATDlXPpKGJnbJZ5Z24ybixAi+LUUqJ/GKowAejtC5GFUG7Tw==} + engines: {node: '>= 6'} + dependencies: + array-timsort: 1.0.3 + core-util-is: 1.0.3 + esprima: 4.0.1 + has-own-prop: 2.0.0 + repeat-string: 1.6.1 + dev: true + + /component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + dev: true + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true + + /concat-stream@1.6.2: + resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} + engines: {'0': node >= 0.8} + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 2.3.8 + typedarray: 0.0.6 + + /consola@2.15.3: + resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} + + /consola@3.2.3: + resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} + engines: {node: ^14.18.0 || >=16.10.0} + dev: false + + /content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + dependencies: + safe-buffer: 5.2.1 + + /content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: true + + /cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + /cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + + /cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + dev: true + + /core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + /cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + /cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.0 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + dev: true + + /create-jest@29.7.0(@types/node@20.3.1)(ts-node@10.9.1): + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@20.3.1)(ts-node@10.9.1) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + + /create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + /cross-spawn@7.0.5: + resolution: {integrity: sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + /dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + dev: false + + /debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + + /debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + + /dedent@1.5.3: + resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + dev: true + + /deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: true + + /deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + dev: true + + /defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + dependencies: + clone: 1.0.4 + dev: true + + /define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + /denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + dev: false + + /depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + /destr@2.0.3: + resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==} + dev: false + + /destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + /detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + dev: true + + /dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + dev: true + + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + + /diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + + /dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + dev: true + + /doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /dotenv-expand@10.0.0: + resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} + engines: {node: '>=12'} + dev: false + + /dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + dev: false + + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: false + + /ebec@1.1.1: + resolution: {integrity: sha512-JZ1vcvPQtR+8LGbZmbjG21IxLQq/v47iheJqn2F6yB2CgnGfn8ZVg3myHrf3buIZS8UCwQK0jOSIb3oHX7aH8g==} + dependencies: + smob: 1.5.0 + dev: false + + /ebec@2.3.0: + resolution: {integrity: sha512-bt+0tSL7223VU3PSVi0vtNLZ8pO1AfWolcPPMk2a/a5H+o/ZU9ky0n3A0zhrR4qzJTN61uPsGIO4ShhOukdzxA==} + dev: false + + /ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + /electron-to-chromium@1.5.55: + resolution: {integrity: sha512-6maZ2ASDOTBtjt9FhqYPRnbvKU5tjG0IN9SztUOWYw2AzNDNpKJYLJmlK0/En4Hs/aiWnB+JZ+gW19PIGszgKg==} + dev: true + + /emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + dev: true + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: false + + /encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + /end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + dependencies: + once: 1.4.0 + dev: true + + /enhanced-resolve@5.17.1: + resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} + engines: {node: '>=10.13.0'} + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + dev: true + + /enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + dev: true + + /envix@1.5.0: + resolution: {integrity: sha512-IOxTKT+tffjxgvX2O5nq6enbkv6kBQ/QdMy18bZWo0P0rKPvsRp2/EypIPwTvJfnmk3VdOlq/KcRSZCswefM/w==} + engines: {node: '>=18.0.0'} + dependencies: + std-env: 3.8.0 + dev: false + + /error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + dependencies: + is-arrayish: 0.2.1 + dev: true + + /es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.4 + + /es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + /es-module-lexer@1.5.4: + resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} + dev: true + + /escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + /escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + dev: true + + /escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + dev: true + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /eslint-config-prettier@9.0.0(eslint@8.0.0): + resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + dependencies: + eslint: 8.0.0 + dev: true + + /eslint-plugin-prettier@5.0.0(eslint-config-prettier@9.0.0)(eslint@8.0.0)(prettier@3.0.0): + resolution: {integrity: sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w==} + engines: {node: ^14.18.0 || >=16.0.0} + 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 + dependencies: + eslint: 8.0.0 + eslint-config-prettier: 9.0.0(eslint@8.0.0) + prettier: 3.0.0 + prettier-linter-helpers: 1.0.0 + synckit: 0.8.8 + dev: true + + /eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + dev: true + + /eslint-scope@6.0.0: + resolution: {integrity: sha512-uRDL9MWmQCkaFus8RF5K9/L/2fn+80yoW3jkD53l4shjCh26fCtvJGasxjUqP5OT87SYTxCVA3BwTUzuELx9kA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + + /eslint-utils@3.0.0(eslint@8.0.0): + resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} + engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} + peerDependencies: + eslint: '>=5' + dependencies: + eslint: 8.0.0 + eslint-visitor-keys: 2.1.0 + dev: true + + /eslint-visitor-keys@2.1.0: + resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} + engines: {node: '>=10'} + dev: true + + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /eslint@8.0.0: + resolution: {integrity: sha512-03spzPzMAO4pElm44m60Nj08nYonPGQXmw6Ceai/S4QK82IgwWO1EXx1s9namKzVlbVu3Jf81hb+N+8+v21/HQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint/eslintrc': 1.4.1 + '@humanwhocodes/config-array': 0.6.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.5 + debug: 4.3.7 + doctrine: 3.0.0 + enquirer: 2.4.1 + escape-string-regexp: 4.0.0 + eslint-scope: 6.0.0 + eslint-utils: 3.0.0(eslint@8.0.0) + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + functional-red-black-tree: 1.0.1 + glob-parent: 6.0.2 + globals: 13.24.0 + ignore: 4.0.6 + import-fresh: 3.3.0 + imurmurhash: 0.1.4 + is-glob: 4.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.4 + progress: 2.0.3 + regexpp: 3.2.0 + semver: 7.6.3 + strip-ansi: 6.0.1 + strip-json-comments: 3.1.1 + text-table: 0.2.0 + v8-compile-cache: 2.4.0 + transitivePeerDependencies: + - supports-color + dev: true + + /espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 3.4.3 + dev: true + + /esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: true + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + dev: true + + /estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + dev: true + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: true + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: true + + /etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + /events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + dev: true + + /execa@4.1.0: + resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} + engines: {node: '>=10'} + dependencies: + cross-spawn: 7.0.5 + get-stream: 5.2.0 + human-signals: 1.1.1 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + dev: true + + /execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + dependencies: + cross-spawn: 7.0.5 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + dev: true + + /exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + dev: true + + /expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + dev: true + + /express@4.18.2: + resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} + engines: {node: '>= 0.10.0'} + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.1 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.5.0 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.2.0 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.1 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.7 + proxy-addr: 2.0.7 + qs: 6.11.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.18.0 + serve-static: 1.15.0 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + /external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + dev: true + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: true + + /fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + dev: true + + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true + + /fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + /fast-xml-parser@4.4.1: + resolution: {integrity: sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==} + hasBin: true + dependencies: + strnum: 1.0.5 + dev: false + + /fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + dependencies: + reusify: 1.0.4 + + /fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + dependencies: + bser: 2.1.1 + dev: true + + /figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + dependencies: + escape-string-regexp: 1.0.5 + dev: true + + /file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.2.0 + dev: true + + /fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + + /finalhandler@1.2.0: + resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} + engines: {node: '>= 0.8'} + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + /find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + dev: true + + /flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flatted: 3.3.1 + keyv: 4.5.4 + rimraf: 3.0.2 + dev: true + + /flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + dev: false + + /flatted@3.3.1: + resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + dev: true + + /follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + + /foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + engines: {node: '>=14'} + dependencies: + cross-spawn: 7.0.5 + signal-exit: 4.1.0 + dev: false + + /fork-ts-checker-webpack-plugin@8.0.0(typescript@5.1.3)(webpack@5.87.0): + resolution: {integrity: sha512-mX3qW3idpueT2klaQXBzrIM/pHw+T0B/V9KHEvNrqijTq9NFnMZU6oreVxDYcf33P8a5cW+67PjodNHthGnNVg==} + engines: {node: '>=12.13.0', yarn: '>=1.0.0'} + peerDependencies: + typescript: '>3.6.0' + webpack: ^5.11.0 + dependencies: + '@babel/code-frame': 7.26.2 + chalk: 4.1.2 + chokidar: 3.5.3 + cosmiconfig: 7.1.0 + deepmerge: 4.3.1 + fs-extra: 10.1.0 + memfs: 3.5.3 + minimatch: 3.1.2 + node-abort-controller: 3.1.1 + schema-utils: 3.3.0 + semver: 7.6.3 + tapable: 2.2.1 + typescript: 5.1.3 + webpack: 5.87.0 + dev: true + + /form-data@4.0.1: + resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + /formidable@3.5.2: + resolution: {integrity: sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==} + dependencies: + dezalgo: 1.0.4 + hexoid: 2.0.0 + once: 1.4.0 + dev: true + + /forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + /fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + /fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + dev: true + + /fs-monkey@1.0.6: + resolution: {integrity: sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==} + dev: true + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + /functional-red-black-tree@1.0.1: + resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} + dev: true + + /generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + dependencies: + is-property: 1.0.2 + dev: false + + /gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + dev: true + + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + /get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + + /get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + dev: true + + /get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + dependencies: + pump: 3.0.2 + dev: true + + /get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + dev: true + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + dev: true + + /glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + dependencies: + foreground-child: 3.3.0 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + dev: false + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /glob@9.3.5: + resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + fs.realpath: 1.0.0 + minimatch: 8.0.4 + minipass: 4.2.8 + path-scurry: 1.11.1 + dev: true + + /globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + dev: true + + /globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + + /globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + dev: true + + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.4 + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: true + + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + dev: true + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + /has-own-prop@2.0.0: + resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==} + engines: {node: '>=8'} + dev: true + + /has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + dependencies: + es-define-property: 1.0.0 + + /has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + + /hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + + /hexoid@2.0.0: + resolution: {integrity: sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==} + engines: {node: '>=8'} + dev: true + + /highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + dev: false + + /html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + dev: true + + /http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + /human-signals@1.1.1: + resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} + engines: {node: '>=8.12.0'} + dev: true + + /human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + dev: true + + /iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + + /iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + /ignore@4.0.6: + resolution: {integrity: sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==} + engines: {node: '>= 4'} + dev: true + + /ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + dev: true + + /import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + dev: true + + /import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + dev: true + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + dev: true + + /inflight@1.0.6: + resolution: {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. + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + /inquirer@8.2.4: + resolution: {integrity: sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg==} + engines: {node: '>=12.0.0'} + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + ora: 5.4.1 + run-async: 2.4.1 + rxjs: 7.8.1 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + wrap-ansi: 7.0.0 + dev: true + + /inquirer@8.2.5: + resolution: {integrity: sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==} + engines: {node: '>=12.0.0'} + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + ora: 5.4.1 + run-async: 2.4.1 + rxjs: 7.8.1 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + wrap-ansi: 7.0.0 + dev: true + + /interpret@1.4.0: + resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} + engines: {node: '>= 0.10'} + dev: true + + /ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + /is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + dev: true + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.3.0 + dev: true + + /is-core-module@2.15.1: + resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} + engines: {node: '>= 0.4'} + dependencies: + hasown: 2.0.2 + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + /is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + + /is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + /is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + dev: false + + /is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + dev: true + + /is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + dev: true + + /isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + /istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + dev: true + + /istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + dependencies: + '@babel/core': 7.26.0 + '@babel/parser': 7.26.2 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + dependencies: + '@babel/core': 7.26.0 + '@babel/parser': 7.26.2 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.6.3 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + dev: true + + /istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + dependencies: + debug: 4.3.7 + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + dev: true + + /iterare@1.2.1: + resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} + engines: {node: '>=6'} + + /jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + dev: false + + /jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + dev: true + + /jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.3.1 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.5.3 + is-generator-fn: 2.1.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.1.0 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: true + + /jest-cli@29.7.0(@types/node@20.3.1)(ts-node@10.9.1): + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.1) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@20.3.1)(ts-node@10.9.1) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@20.3.1)(ts-node@10.9.1) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + + /jest-config@29.7.0(@types/node@20.3.1)(ts-node@10.9.1): + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.26.0 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.3.1 + babel-jest: 29.7.0(@babel/core@7.26.0) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + ts-node: 10.9.1(@types/node@20.3.1)(typescript@5.1.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: true + + /jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + + /jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + detect-newline: 3.1.0 + dev: true + + /jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + dev: true + + /jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.3.1 + jest-mock: 29.7.0 + jest-util: 29.7.0 + dev: true + + /jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + + /jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 20.3.1 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + + /jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + + /jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/code-frame': 7.26.2 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + dev: true + + /jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.3.1 + jest-util: 29.7.0 + dev: true + + /jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + dependencies: + jest-resolve: 29.7.0 + dev: true + + /jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + + /jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.8 + resolve.exports: 2.0.2 + slash: 3.0.0 + dev: true + + /jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.3.1 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.3.1 + chalk: 4.1.2 + cjs-module-lexer: 1.4.1 + collect-v8-coverage: 1.0.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.26.0 + '@babel/generator': 7.26.2 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) + '@babel/types': 7.26.0 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.0) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.6.3 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.3.1 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + dev: true + + /jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + dev: true + + /jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.3.1 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + dev: true + + /jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/node': 20.3.1 + merge-stream: 2.0.0 + supports-color: 8.1.1 + dev: true + + /jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@types/node': 20.3.1 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + dev: true + + /jest@29.5.0(@types/node@20.3.1)(ts-node@10.9.1): + resolution: {integrity: sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.1) + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@20.3.1)(ts-node@10.9.1) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + + /jiti@2.4.0: + resolution: {integrity: sha512-H5UpaUI+aHOqZXlYOaFP/8AzKsg+guWu+Pr3Y8i7+Y3zr1aXAvCvTAQ1RxSc6oVD8R8c7brgNtTVP91E7upH/g==} + hasBin: true + dev: false + + /joi@17.13.3: + resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} + 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 + dev: false + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: true + + /js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + dev: true + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + + /jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + dev: true + + /json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + dev: true + + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + dev: true + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true + + /json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + dev: true + + /json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: true + + /json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + dev: true + + /jsonc-parser@3.2.0: + resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} + dev: true + + /jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + dev: true + + /jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.6.3 + dev: false + + /jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + dev: false + + /jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + dev: false + + /keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + dependencies: + json-buffer: 3.0.1 + dev: true + + /kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + dev: true + + /leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + dev: true + + /levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /libphonenumber-js@1.11.14: + resolution: {integrity: sha512-sexvAfwcW1Lqws4zFp8heAtAEXbEDnvkYCEGzvOoMgZR7JhXo/IkE9MkkGACgBed5fWqh3ShBGnJBdDnU9N8EQ==} + + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + dev: true + + /loader-runner@4.3.0: + resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} + engines: {node: '>=6.11.5'} + dev: true + + /locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + dependencies: + p-locate: 4.1.0 + dev: true + + /locter@2.1.5: + resolution: {integrity: sha512-eI57PuVxigQ0GBscGIIFGPB467E5zKODHD3XGuknzLvf7HdnvRw3GdZVGj1J8XKsKOYovZQesX/oOdTwbdjwuQ==} + dependencies: + destr: 2.0.3 + ebec: 2.3.0 + fast-glob: 3.3.2 + flat: 5.0.2 + jiti: 2.4.0 + yaml: 2.6.0 + dev: false + + /lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + dev: false + + /lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + dev: false + + /lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + dev: false + + /lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + dev: false + + /lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + dev: false + + /lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + dev: false + + /lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + dev: true + + /lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: true + + /lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + dev: false + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + /log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + dev: true + + /long@5.2.3: + resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + dev: false + + /lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + dependencies: + tslib: 2.8.1 + dev: false + + /lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + /lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + dev: true + + /lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + dev: false + + /lru.min@1.1.1: + resolution: {integrity: sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + dev: false + + /macos-release@2.5.1: + resolution: {integrity: sha512-DXqXhEM7gW59OjZO8NIjBCz9AQ1BEMrfiOAl4AYByHCtVHRF4KoGNO8mqQeM8lRCtQe/UnJ4imO/d2HdkKsd+A==} + engines: {node: '>=6'} + dev: true + + /magic-string@0.30.0: + resolution: {integrity: sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + dev: true + + /make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + dependencies: + semver: 7.6.3 + dev: true + + /make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + /makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + dependencies: + tmpl: 1.0.5 + dev: true + + /media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + /memfs@3.5.3: + resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} + engines: {node: '>= 4.0.0'} + dependencies: + fs-monkey: 1.0.6 + dev: true + + /merge-descriptors@1.0.1: + resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: true + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + /methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + /micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + + /mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + /mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + dev: true + + /mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + dev: true + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /minimatch@8.0.4: + resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + /minipass@4.2.8: + resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} + engines: {node: '>=8'} + dev: true + + /minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + /mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + dependencies: + minimist: 1.2.8 + + /mkdirp@2.1.6: + resolution: {integrity: sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==} + engines: {node: '>=10'} + hasBin: true + dev: false + + /ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + /multer@1.4.4-lts.1: + resolution: {integrity: sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==} + engines: {node: '>= 6.0.0'} + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 1.6.2 + mkdirp: 0.5.6 + object-assign: 4.1.1 + type-is: 1.6.18 + xtend: 4.0.2 + + /mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + dev: true + + /mysql2@3.11.4: + resolution: {integrity: sha512-Z2o3tY4Z8EvSRDwknaC40MdZ3+m0sKbpnXrShQLdxPrAvcNli7jLrD2Zd2IzsRMw4eK9Yle500FDmlkIqp+krg==} + engines: {node: '>= 8.0'} + dependencies: + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.6.3 + long: 5.2.3 + lru.min: 1.1.1 + named-placeholders: 1.1.3 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + dev: false + + /mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + dev: false + + /named-placeholders@1.1.3: + resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==} + engines: {node: '>=12.0.0'} + dependencies: + lru-cache: 7.18.3 + dev: false + + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + dev: true + + /negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + /neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + dev: true + + /no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + dependencies: + lower-case: 2.0.2 + tslib: 2.8.1 + dev: false + + /node-abort-controller@3.1.1: + resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + dev: true + + /node-addon-api@8.2.2: + resolution: {integrity: sha512-9emqXAKhVoNrQ792nLI/wpzPpJ/bj/YXxW0CvAau1+RdGBcCRF1Dmz7719zgVsQNrzHl9Tzn3ImZ4qWFarWL0A==} + engines: {node: ^18 || ^20 || >= 21} + dev: false + + /node-emoji@1.11.0: + resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} + dependencies: + lodash: 4.17.21 + dev: true + + /node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + + /node-gyp-build@4.8.2: + resolution: {integrity: sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw==} + hasBin: true + dev: false + + /node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + dev: true + + /node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + dev: true + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + dependencies: + path-key: 3.1.1 + dev: true + + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + /object-inspect@1.13.3: + resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==} + engines: {node: '>= 0.4'} + + /on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + dependencies: + mimic-fn: 2.1.0 + dev: true + + /optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + dev: true + + /ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + dev: true + + /os-name@4.0.1: + resolution: {integrity: sha512-xl9MAoU97MH1Xt5K9ERft2YfCAoaO6msy1OBA0ozxEC0x0TmIoE6K3QvgJMMZA9yKGLmHXNY/YZoDbiGDj4zYw==} + engines: {node: '>=10'} + dependencies: + macos-release: 2.5.1 + windows-release: 4.0.0 + dev: true + + /os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + dev: true + + /p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + dependencies: + p-try: 2.2.0 + dev: true + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: true + + /p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + dependencies: + p-limit: 2.3.0 + dev: true + + /p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + dev: true + + /package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + dev: false + + /parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + dev: true + + /parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + dependencies: + '@babel/code-frame': 7.26.2 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + dev: true + + /parse5-htmlparser2-tree-adapter@6.0.1: + resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} + dependencies: + parse5: 6.0.1 + dev: false + + /parse5@5.1.1: + resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} + dev: false + + /parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + dev: false + + /parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + /pascal-case@3.1.2: + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + dev: false + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: true + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: true + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + /path-to-regexp@0.1.7: + resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + + /path-to-regexp@3.2.0: + resolution: {integrity: sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==} + + /path-to-regexp@3.3.0: + resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} + dev: false + + /path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + dev: true + + /pg-cloudflare@1.1.1: + resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==} + requiresBuild: true + dev: false + optional: true + + /pg-connection-string@2.7.0: + resolution: {integrity: sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==} + dev: false + + /pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + dev: false + + /pg-pool@3.7.0(pg@8.13.1): + resolution: {integrity: sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==} + peerDependencies: + pg: '>=8.0' + dependencies: + pg: 8.13.1 + dev: false + + /pg-protocol@1.7.0: + resolution: {integrity: sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==} + dev: false + + /pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + dev: false + + /pg@8.13.1: + resolution: {integrity: sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==} + engines: {node: '>= 8.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + dependencies: + pg-connection-string: 2.7.0 + pg-pool: 3.7.0(pg@8.13.1) + pg-protocol: 1.7.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.1.1 + dev: false + + /pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + dependencies: + split2: 4.2.0 + dev: false + + /picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + dev: true + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + /pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + dev: true + + /pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + dependencies: + find-up: 4.1.0 + dev: true + + /pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + dev: true + + /postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + dev: false + + /postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + dev: false + + /postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + dev: false + + /postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + dependencies: + xtend: 4.0.2 + dev: false + + /prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + dev: true + + /prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + dependencies: + fast-diff: 1.3.0 + dev: true + + /prettier@3.0.0: + resolution: {integrity: sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==} + engines: {node: '>=14'} + hasBin: true + dev: true + + /pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + dev: true + + /process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + /progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + dev: true + + /prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + dev: true + + /proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + /proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: false + + /pump@3.0.2: + resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + dev: true + + /punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + dev: true + + /pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + dev: true + + /qs@6.11.0: + resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.6 + + /qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.6 + dev: true + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + /randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + /rapiq@0.9.0: + resolution: {integrity: sha512-k4oT4RarFBrlLMJ49xUTeQpa/us0uU4I70D/UEnK3FWQ4GENzei01rEQAmvPKAIzACo4NMW+YcYJ7EVfSa7EFg==} + dependencies: + ebec: 1.1.1 + smob: 1.5.0 + dev: false + + /raw-body@2.5.1: + resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + /raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + /react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + dev: true + + /readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: true + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /rechoir@0.6.2: + resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} + engines: {node: '>= 0.10'} + dependencies: + resolve: 1.22.8 + dev: true + + /reflect-metadata@0.2.0: + resolution: {integrity: sha512-vUN0wuk3MuhSVMfU/ImnPQAK8QZcXJ339DtVsP3jDscxCe6dT+PsOe3J1BYS9Ec2Fd4oC6ry6bCBebzTya0IYw==} + deprecated: This version has a critical bug in fallback handling. Please upgrade to reflect-metadata@0.2.2 or newer. + + /reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + dev: false + + /regexpp@3.2.0: + resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} + engines: {node: '>=8'} + dev: true + + /repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + dev: true + + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + /require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + dev: true + + /resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + dependencies: + resolve-from: 5.0.0 + dev: true + + /resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + dev: true + + /resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + dev: true + + /resolve.exports@2.0.2: + resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} + engines: {node: '>=10'} + dev: true + + /resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + dependencies: + is-core-module: 2.15.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + dev: true + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /rimraf@4.4.1: + resolution: {integrity: sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og==} + engines: {node: '>=14'} + hasBin: true + dependencies: + glob: 9.3.5 + dev: true + + /run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + + /rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + dependencies: + tslib: 2.8.1 + + /safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + /schema-utils@3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + dev: true + + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + dev: true + + /semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + + /send@0.18.0: + resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} + engines: {node: '>= 0.8.0'} + 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 + transitivePeerDependencies: + - supports-color + + /seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + dev: false + + /serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + dependencies: + randombytes: 2.1.0 + dev: true + + /serve-static@1.15.0: + resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} + engines: {node: '>= 0.8.0'} + dependencies: + encodeurl: 1.0.2 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.18.0 + transitivePeerDependencies: + - supports-color + + /set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + 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 + + /setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + /sha.js@2.4.11: + resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} + hasBin: true + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: false + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + /shelljs@0.8.5: + resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} + engines: {node: '>=4'} + hasBin: true + dependencies: + glob: 7.2.3 + interpret: 1.4.0 + rechoir: 0.6.2 + dev: true + + /side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.3 + + /signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + dev: true + + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: false + + /sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + dev: true + + /slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + dev: true + + /smob@1.5.0: + resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==} + dev: false + + /source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + + /source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + + /source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + dev: true + + /split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + dev: false + + /sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + dev: true + + /sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + dev: false + + /stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + dependencies: + escape-string-regexp: 2.0.0 + dev: true + + /statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + /std-env@3.8.0: + resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} + dev: false + + /streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + /string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + dev: true + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + dev: false + + /string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + dependencies: + safe-buffer: 5.1.2 + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.1.0 + dev: false + + /strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + dev: true + + /strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + dev: true + + /strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + dev: true + + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: true + + /strnum@1.0.5: + resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} + dev: false + + /superagent@9.0.2: + resolution: {integrity: sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==} + engines: {node: '>=14.18.0'} + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.3.7 + fast-safe-stringify: 2.1.1 + form-data: 4.0.1 + formidable: 3.5.2 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.13.0 + transitivePeerDependencies: + - supports-color + dev: true + + /supertest@7.0.0: + resolution: {integrity: sha512-qlsr7fIC0lSddmA3tzojvzubYxvlGtzumcdHgPwbFWMISQwL22MhM2Y3LNt+6w9Yyx7559VW5ab70dgphm8qQA==} + engines: {node: '>=14.18.0'} + dependencies: + methods: 1.1.2 + superagent: 9.0.2 + transitivePeerDependencies: + - supports-color + dev: true + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + + /supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /swagger-ui-dist@5.18.2: + resolution: {integrity: sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw==} + dependencies: + '@scarf/scarf': 1.4.0 + dev: false + + /symbol-observable@4.0.0: + resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} + engines: {node: '>=0.10'} + dev: true + + /synckit@0.8.8: + resolution: {integrity: sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==} + engines: {node: ^14.18.0 || >=16.0.0} + dependencies: + '@pkgr/core': 0.1.1 + tslib: 2.8.1 + dev: true + + /tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + dev: true + + /terser-webpack-plugin@5.3.10(webpack@5.87.0): + resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + jest-worker: 27.5.1 + schema-utils: 3.3.0 + serialize-javascript: 6.0.2 + terser: 5.36.0 + webpack: 5.87.0 + dev: true + + /terser-webpack-plugin@5.3.10(webpack@5.96.1): + resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + jest-worker: 27.5.1 + schema-utils: 3.3.0 + serialize-javascript: 6.0.2 + terser: 5.36.0 + webpack: 5.96.1 + dev: true + + /terser@5.36.0: + resolution: {integrity: sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==} + engines: {node: '>=10'} + hasBin: true + dependencies: + '@jridgewell/source-map': 0.3.6 + acorn: 8.14.0 + commander: 2.20.3 + source-map-support: 0.5.21 + dev: true + + /test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + dev: true + + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: true + + /thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + dependencies: + thenify: 3.3.1 + dev: false + + /thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + dependencies: + any-promise: 1.3.0 + dev: false + + /through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + dev: true + + /tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + dependencies: + os-tmpdir: 1.0.2 + dev: true + + /tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + + /toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + /tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + dev: true + + /ts-api-utils@1.4.0(typescript@5.1.3): + resolution: {integrity: sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + dependencies: + typescript: 5.1.3 + dev: true + + /ts-jest@29.1.0(@babel/core@7.26.0)(jest@29.5.0)(typescript@5.1.3): + resolution: {integrity: sha512-ZhNr7Z4PcYa+JjMl62ir+zPiNJfXJN6E8hSLnaUKhOgqcn8vb3e537cpkd0FuAfRK3sR1LSqM1MOhliXNgOFPA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + dependencies: + '@babel/core': 7.26.0 + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + jest: 29.5.0(@types/node@20.3.1)(ts-node@10.9.1) + jest-util: 29.7.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.6.3 + typescript: 5.1.3 + yargs-parser: 21.1.1 + dev: true + + /ts-loader@9.4.3(typescript@5.1.3)(webpack@5.96.1): + resolution: {integrity: sha512-n3hBnm6ozJYzwiwt5YRiJZkzktftRpMiBApHaJPoWLA+qetQBAXkHqCLM6nwSdRDimqVtA5ocIkcTRLMTt7yzA==} + engines: {node: '>=12.0.0'} + peerDependencies: + typescript: '*' + webpack: ^5.0.0 + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.17.1 + micromatch: 4.0.8 + semver: 7.6.3 + typescript: 5.1.3 + webpack: 5.96.1 + dev: true + + /ts-node@10.9.1(@types/node@20.3.1)(typescript@5.1.3): + resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + hasBin: true + 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 + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.3.1 + acorn: 8.14.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.1.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + /tsconfig-paths-webpack-plugin@4.0.1: + resolution: {integrity: sha512-m5//KzLoKmqu2MVix+dgLKq70MnFi8YL8sdzQZ6DblmCdfuq/y3OqvJd5vMndg2KEVCOeNz8Es4WVZhYInteLw==} + engines: {node: '>=10.13.0'} + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.17.1 + tsconfig-paths: 4.2.0 + dev: true + + /tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + dev: true + + /tslib@2.5.3: + resolution: {integrity: sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==} + + /tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + /type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + dev: true + + /type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + dev: true + + /type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + dev: true + + /type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + dev: true + + /type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + /typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + + /typeorm-extension@3.6.3(typeorm@0.3.20): + resolution: {integrity: sha512-AE+8KqBphlBdVz5JS77o6LZzzi+b+YFFt8So4Qu/KRo/iynAwekrx98Oxuu3FAYNm6DUKDcubOBMZsJeiRvHkA==} + engines: {node: '>=14.0.0'} + hasBin: true + peerDependencies: + typeorm: ~0.3.0 + dependencies: + '@faker-js/faker': 8.4.1 + consola: 3.2.3 + envix: 1.5.0 + locter: 2.1.5 + pascal-case: 3.1.2 + rapiq: 0.9.0 + reflect-metadata: 0.2.2 + smob: 1.5.0 + typeorm: 0.3.20(mysql2@3.11.4)(pg@8.13.1)(ts-node@10.9.1) + yargs: 17.7.2 + dev: false + + /typeorm@0.3.20(mysql2@3.11.4)(pg@8.13.1)(ts-node@10.9.1): + resolution: {integrity: sha512-sJ0T08dV5eoZroaq9uPKBoNcGslHBR4E4y+EBHs//SiGbblGe7IeduP/IH4ddCcj0qp3PHwDwGnuvqEAnKlq/Q==} + engines: {node: '>=16.13.0'} + hasBin: true + peerDependencies: + '@google-cloud/spanner': ^5.18.0 + '@sap/hana-client': ^2.12.25 + better-sqlite3: ^7.1.2 || ^8.0.0 || ^9.0.0 + hdb-pool: ^0.1.6 + ioredis: ^5.0.4 + mongodb: ^5.8.0 + mssql: ^9.1.1 || ^10.0.1 + mysql2: ^2.2.5 || ^3.0.1 + oracledb: ^6.3.0 + pg: ^8.5.1 + pg-native: ^3.0.0 + pg-query-stream: ^4.0.0 + redis: ^3.1.1 || ^4.0.0 + sql.js: ^1.4.0 + sqlite3: ^5.0.3 + ts-node: ^10.7.0 + typeorm-aurora-data-api-driver: ^2.0.0 + peerDependenciesMeta: + '@google-cloud/spanner': + optional: true + '@sap/hana-client': + optional: true + better-sqlite3: + optional: true + hdb-pool: + optional: true + ioredis: + optional: true + mongodb: + optional: true + mssql: + optional: true + mysql2: + optional: true + oracledb: + optional: true + pg: + optional: true + pg-native: + optional: true + pg-query-stream: + optional: true + redis: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + ts-node: + optional: true + typeorm-aurora-data-api-driver: + optional: true + dependencies: + '@sqltools/formatter': 1.2.5 + app-root-path: 3.1.0 + buffer: 6.0.3 + chalk: 4.1.2 + cli-highlight: 2.1.11 + dayjs: 1.11.13 + debug: 4.3.7 + dotenv: 16.4.5 + glob: 10.4.5 + mkdirp: 2.1.6 + mysql2: 3.11.4 + pg: 8.13.1 + reflect-metadata: 0.2.2 + sha.js: 2.4.11 + ts-node: 10.9.1(@types/node@20.3.1)(typescript@5.1.3) + tslib: 2.8.1 + uuid: 9.0.1 + yargs: 17.7.2 + transitivePeerDependencies: + - supports-color + dev: false + + /typescript@5.1.3: + resolution: {integrity: sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==} + engines: {node: '>=14.17'} + hasBin: true + + /uid@2.0.2: + resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} + engines: {node: '>=8'} + dependencies: + '@lukeed/csprng': 1.1.0 + + /universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + dev: true + + /unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + /update-browserslist-db@1.1.1(browserslist@4.24.2): + resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.24.2 + escalade: 3.2.0 + picocolors: 1.1.1 + dev: true + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.1 + dev: true + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + /utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + /uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + dev: false + + /v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + /v8-compile-cache@2.4.0: + resolution: {integrity: sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==} + dev: true + + /v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + dev: true + + /validator@13.12.0: + resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} + engines: {node: '>= 0.10'} + + /vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + /walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + dependencies: + makeerror: 1.0.12 + dev: true + + /watchpack@2.4.2: + resolution: {integrity: sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==} + engines: {node: '>=10.13.0'} + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + dev: true + + /wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + dependencies: + defaults: 1.0.4 + dev: true + + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + /webpack-node-externals@3.0.0: + resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} + engines: {node: '>=6'} + dev: true + + /webpack-sources@3.2.3: + resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + engines: {node: '>=10.13.0'} + dev: true + + /webpack@5.87.0: + resolution: {integrity: sha512-GOu1tNbQ7p1bDEoFRs2YPcfyGs8xq52yyPBZ3m2VGnXGtV9MxjrkABHm4V9Ia280OefsSLzvbVoXcfLxjKY/Iw==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.6 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.14.0 + acorn-import-assertions: 1.9.0(acorn@8.14.0) + browserslist: 4.24.2 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.17.1 + es-module-lexer: 1.5.4 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.3.0 + tapable: 2.2.1 + terser-webpack-plugin: 5.3.10(webpack@5.87.0) + watchpack: 2.4.2 + webpack-sources: 3.2.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + dev: true + + /webpack@5.96.1: + resolution: {integrity: sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.6 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.14.0 + browserslist: 4.24.2 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.17.1 + es-module-lexer: 1.5.4 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.3.0 + tapable: 2.2.1 + terser-webpack-plugin: 5.3.10(webpack@5.96.1) + watchpack: 2.4.2 + webpack-sources: 3.2.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + dev: true + + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + + /windows-release@4.0.0: + resolution: {integrity: sha512-OxmV4wzDKB1x7AZaZgXMVsdJ1qER1ed83ZrTYd5Bwq2HfJVg3DJS8nqlAG4sMoJ7mu8cuRmLEYyU13BKwctRAg==} + engines: {node: '>=10'} + dependencies: + execa: 4.1.0 + dev: true + + /word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + dev: true + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + dev: false + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + dev: true + + /xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + /yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + dev: true + + /yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + dev: true + + /yaml@2.6.0: + resolution: {integrity: sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==} + engines: {node: '>= 14'} + hasBin: true + dev: false + + /yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + dev: false + + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + /yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + dev: false + + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + /yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: true diff --git a/src/app.controller.ts b/src/app.controller.ts index cce879e..2cab0eb 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,11 +1,13 @@ import { Controller, Get } from '@nestjs/common'; import { AppService } from './app.service'; +import { Public } from './shared/decorators/public.decorator'; @Controller() export class AppController { constructor(private readonly appService: AppService) {} @Get() + @Public() getHello(): string { return this.appService.getHello(); } diff --git a/src/app.module.ts b/src/app.module.ts index 8662803..f6adadd 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,10 +1,98 @@ import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { APP_GUARD } from '@nestjs/core'; +import { JwtModule } from '@nestjs/jwt'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { AuthGuard } from './auth/auth.guard'; +import { AuthModule } from './auth/auth.module'; +import { CategoryModule } from './category/category.module'; +import { ChapterModule } from './chapter/chapter.module'; +import { ChatMessageModule } from './chat-message/chat-message.module'; +import { ChatRoomModule } from './chat-room/chat-room.module'; +import { CourseModuleModule } from './course-module/course-module.module'; +import { CourseModule } from './course/course.module'; +import { DatabaseModule } from './database/database.module'; +import { EnrollmentModule } from './enrollment/enrollment.module'; +import { ExamAnswerModule } from './exam-answer/exam-answer.module'; +import { ExamAttemptModule } from './exam-attempt/exam-attempt.module'; +import { ExamModule } from './exam/exam.module'; +import { FileModule } from './file/file.module'; +import { ProgressModule } from './progress/progress.module'; +import { QuestionOptionModule } from './question-option/question-option.module'; +import { QuestionModule } from './question/question.module'; +import { RewardModule } from './reward/reward.module'; +import { RoadmapModule } from './roadmap/roadmap.module'; +import { databaseConfig } from './shared/configs/database.config'; +import { dotenvConfig } from './shared/configs/dotenv.config'; +import { GLOBAL_CONFIG } from './shared/constants/global-config.constant'; +import { RolesGuard } from './shared/guards/role.guard'; +import { UserBackgroundTopicModule } from './user-background-topic/user-background-topic.module'; +import { UserBackgroundModule } from './user-background/user-background.module'; +import { UserOccupationModule } from './user-occupation/user-occupation.module'; +import { UserStreakModule } from './user-streak/user-streak.module'; +import { UserModule } from './user/user.module'; +import { UserRewardModule } from './user-reward/user-reward.module'; +import { PretestModule } from './pretest/pretest.module'; @Module({ - imports: [], + imports: [ + AuthModule, + ConfigModule.forRoot({ + isGlobal: true, + validationSchema: dotenvConfig, + }), + TypeOrmModule.forRootAsync({ + imports: [ConfigModule], + useFactory: (configService: ConfigService) => ({ + ...databaseConfig, + migrations: ['dist/database/migrations/*.js'], + migrationsRun: true, + synchronize: configService.get(GLOBAL_CONFIG.IS_DEVELOPMENT), + autoLoadEntities: true, + }), + inject: [ConfigService], + }), + JwtModule.register({ + global: true, + }), + DatabaseModule, + UserModule, + UserStreakModule, + CategoryModule, + CourseModule, + CourseModuleModule, + ChapterModule, + FileModule, + ExamModule, + EnrollmentModule, + ProgressModule, + ExamAttemptModule, + QuestionModule, + QuestionOptionModule, + ExamAnswerModule, + UserOccupationModule, + UserBackgroundTopicModule, + UserBackgroundModule, + ChatRoomModule, + ChatMessageModule, + RewardModule, + UserRewardModule, + RoadmapModule, + PretestModule, + ], controllers: [AppController], - providers: [AppService], + providers: [ + AppService, + { + provide: APP_GUARD, + useClass: AuthGuard, + }, + { + provide: APP_GUARD, + useClass: RolesGuard, + }, + ], }) export class AppModule {} diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts new file mode 100644 index 0000000..8cc37d9 --- /dev/null +++ b/src/auth/auth.controller.ts @@ -0,0 +1,44 @@ +import { + Controller, + Injectable, + Post, + Body, + HttpCode, + HttpStatus, +} from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { LoginDto } from './dtos/login.dto'; +import { RegisterDto } from './dtos/register.dto'; +import { Public } from 'src/shared/decorators/public.decorator'; +import { ApiTags, ApiResponse } from '@nestjs/swagger'; +import { AuthResponseDto } from './dtos/auth-response.dto'; + +@Controller('auth') +@ApiTags('Auth') +@Injectable() +export class AuthController { + constructor(private readonly authService: AuthService) {} + + @Post('login') + @ApiResponse({ + status: HttpStatus.OK, + description: 'Login', + type: AuthResponseDto, + }) + @Public() + async login(@Body() loginDto: LoginDto): Promise { + return await this.authService.login(loginDto); + } + + @Post('register') + @ApiResponse({ + status: HttpStatus.CREATED, + description: 'Register', + type: AuthResponseDto, + }) + @HttpCode(HttpStatus.CREATED) + @Public() + async register(@Body() registerDto: RegisterDto): Promise { + return await this.authService.register(registerDto); + } +} diff --git a/src/auth/auth.guard.ts b/src/auth/auth.guard.ts new file mode 100644 index 0000000..e96e76e --- /dev/null +++ b/src/auth/auth.guard.ts @@ -0,0 +1,46 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + UnauthorizedException, +} from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { ConfigService } from '@nestjs/config'; +import { GLOBAL_CONFIG } from 'src/shared/constants/global-config.constant'; +import { JwtPayloadDto } from './dtos/jwt-payload.dto'; +import { IS_PUBLIC_KEY } from 'src/shared/decorators/public.decorator'; +import { Reflector } from '@nestjs/core'; +import { Request } from 'express'; + +@Injectable() +export class AuthGuard implements CanActivate { + constructor( + private readonly jwtService: JwtService, + private readonly configService: ConfigService, + private readonly reflector: Reflector, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + context.getHandler(), + context.getClass(), + ]); + if (isPublic) return true; + const request = context.switchToHttp().getRequest(); + const token = this.extractTokenFromHeader(request); + if (!token) throw new UnauthorizedException('Unauthorized access'); + try { + request.user = await this.jwtService.verifyAsync(token, { + secret: this.configService.get(GLOBAL_CONFIG.JWT_ACCESS_SECRET), + }); + } catch (error) { + throw new UnauthorizedException('Unauthorized access'); + } + return true; + } + + private extractTokenFromHeader(request: Request): string | undefined { + const [type, token] = request.headers.authorization?.split(' ') ?? []; + return type === 'Bearer' ? token : undefined; + } +} diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts new file mode 100644 index 0000000..42cc0f1 --- /dev/null +++ b/src/auth/auth.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { AuthController } from './auth.controller'; +import { AuthService } from './auth.service'; +import { UserModule } from 'src/user/user.module'; +import { UserStreakModule } from 'src/user-streak/user-streak.module'; + +@Module({ + imports: [UserModule, UserStreakModule], + controllers: [AuthController], + providers: [AuthService], +}) +export class AuthModule {} diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts new file mode 100644 index 0000000..8918ea7 --- /dev/null +++ b/src/auth/auth.service.ts @@ -0,0 +1,104 @@ +import { + Injectable, + NotFoundException, + BadRequestException, + InternalServerErrorException, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { JwtService } from '@nestjs/jwt'; +import { LoginDto } from './dtos/login.dto'; +import { RegisterDto } from './dtos/register.dto'; +import { UserService } from 'src/user/user.service'; +import { hash, verify } from 'argon2'; +import { AuthResponseDto } from './dtos/auth-response.dto'; +import { JwtPayloadDto } from './dtos/jwt-payload.dto'; +import { GLOBAL_CONFIG } from 'src/shared/constants/global-config.constant'; +import { UserResponseDto } from 'src/user/dtos/user-response.dto'; +import { UserStreakService } from 'src/user-streak/user-streak.service'; +import { Role } from 'src/shared/enums'; + +@Injectable() +export class AuthService { + constructor( + private readonly configService: ConfigService, + private readonly jwtService: JwtService, + private readonly userService: UserService, + private readonly userStreakService: UserStreakService, + ) {} + + async login(loginDto: LoginDto): Promise { + const user = await this.userService.findOne({ + where: { email: loginDto.email }, + }); + if (!user) throw new NotFoundException('User not found'); + const isPasswordValid = await verify(user.password, loginDto.password); + if (!isPasswordValid) throw new BadRequestException('Invalid password'); + try { + const accessToken = this.generateAccessToken({ + id: user.id, + role: user.role, + }); + const refreshToken = this.generateRefreshToken(); + return { + accessToken, + refreshToken, + user: new UserResponseDto(user), + }; + } catch (error) { + if (error instanceof Error) + throw new InternalServerErrorException(error.message); + } + } + + async register(registerDto: RegisterDto): Promise { + const user = await this.userService.findOne({ + where: { email: registerDto.email }, + }); + if (user) throw new BadRequestException('User already exists'); + const hashedPassword = await hash(registerDto.password); + const createdUser = await this.userService.create({ + ...registerDto, + password: hashedPassword, + }); + try { + const accessToken = this.generateAccessToken({ + id: createdUser.id, + role: createdUser.role, + }); + const refreshToken = this.generateRefreshToken(); + return { + accessToken, + refreshToken, + user: new UserResponseDto(createdUser), + }; + } catch (error) { + if (error instanceof Error) { + await this.userService.delete({ id: createdUser.id }); + throw new InternalServerErrorException(error.message); + } + } + } + + private generateAccessToken(payload: JwtPayloadDto): string { + return this.jwtService.sign(payload, { + secret: this.configService.get(GLOBAL_CONFIG.JWT_ACCESS_SECRET), + expiresIn: this.configService.get( + GLOBAL_CONFIG.JWT_ACCESS_EXPIRATION, + ), + }); + } + + private generateRefreshToken(): string { + return this.jwtService.sign( + {}, + { + secret: this.configService.get( + GLOBAL_CONFIG.JWT_REFRESH_SECRET, + ), + expiresIn: this.configService.get( + GLOBAL_CONFIG.JWT_REFRESH_EXPIRATION, + ), + }, + ); + } +} diff --git a/src/auth/dtos/auth-response.dto.ts b/src/auth/dtos/auth-response.dto.ts new file mode 100644 index 0000000..4921c8a --- /dev/null +++ b/src/auth/dtos/auth-response.dto.ts @@ -0,0 +1,22 @@ +import { UserResponseDto } from 'src/user/dtos/user-response.dto'; +import { ApiProperty } from '@nestjs/swagger'; + +export class AuthResponseDto { + @ApiProperty({ + description: 'Access token', + type: String, + }) + accessToken: string; + + @ApiProperty({ + description: 'Refresh token', + type: String, + }) + refreshToken: string; + + @ApiProperty({ + description: 'User', + type: UserResponseDto, + }) + user: UserResponseDto; +} diff --git a/src/auth/dtos/jwt-payload.dto.ts b/src/auth/dtos/jwt-payload.dto.ts new file mode 100644 index 0000000..b9744b1 --- /dev/null +++ b/src/auth/dtos/jwt-payload.dto.ts @@ -0,0 +1,6 @@ +import { Role } from 'src/shared/enums/roles.enum'; + +export class JwtPayloadDto { + id: string; + role: Role; +} diff --git a/src/auth/dtos/login.dto.ts b/src/auth/dtos/login.dto.ts new file mode 100644 index 0000000..dab189d --- /dev/null +++ b/src/auth/dtos/login.dto.ts @@ -0,0 +1,22 @@ +import { IsString, IsEmail, IsNotEmpty } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class LoginDto { + @IsEmail() + @IsNotEmpty() + @ApiProperty({ + description: 'User email', + type: String, + example: 'johndoe@gmail.com', + }) + email: string; + + @IsString() + @IsNotEmpty() + @ApiProperty({ + description: 'User password', + type: String, + example: 'P@ssw0rd!', + }) + password: string; +} diff --git a/src/auth/dtos/register.dto.ts b/src/auth/dtos/register.dto.ts new file mode 100644 index 0000000..846da5f --- /dev/null +++ b/src/auth/dtos/register.dto.ts @@ -0,0 +1,3 @@ +import { CreateUserDto } from 'src/user/dtos/create-user.dto'; + +export class RegisterDto extends CreateUserDto {} diff --git a/src/auth/interfaces/authenticated-request.interface.ts b/src/auth/interfaces/authenticated-request.interface.ts new file mode 100644 index 0000000..3177ac6 --- /dev/null +++ b/src/auth/interfaces/authenticated-request.interface.ts @@ -0,0 +1,6 @@ +import { JwtPayloadDto } from '../dtos/jwt-payload.dto'; +import { Request } from 'express'; + +export interface AuthenticatedRequest extends Request { + user: JwtPayloadDto; +} diff --git a/src/category/category.controller.ts b/src/category/category.controller.ts new file mode 100644 index 0000000..78ccf24 --- /dev/null +++ b/src/category/category.controller.ts @@ -0,0 +1,157 @@ +import { + Body, + Controller, + Delete, + HttpStatus, + Param, + ParseUUIDPipe, + Patch, + Post, + Injectable, + Get, + Query, + HttpCode, +} from '@nestjs/common'; +import { CategoryService } from './category.service'; +import { + categoryResponseDto, + PaginatedCategoryDto, +} from './dtos/category-response.dto'; +import { CreateCategoryDto } from './dtos/create-cateory.dto'; +import { updateCategoryDto } from './dtos/update-category.dto'; +import { Public } from 'src/shared/decorators/public.decorator'; +import { ApiBearerAuth, ApiResponse, ApiTags, ApiQuery } from '@nestjs/swagger'; +import { Category } from './category.entity'; +import { Roles } from 'src/shared/decorators/role.decorator'; +import { Role } from 'src/shared/enums'; +import { PaginateQueryDto } from 'src/shared/pagination/dtos/paginate-query.dto'; +import { Slug } from './enums/slug.enum'; +import { PaginateCategoryQueryDto } from 'src/category/dtos/paginate-query-slug.dto'; + +@Controller('category') +@Injectable() +@ApiTags('Category') +export class CategoryController { + constructor(private readonly categoryService: CategoryService) {} + + @Post() + @Roles(Role.ADMIN) + @HttpCode(HttpStatus.CREATED) + @ApiResponse({ + status: HttpStatus.CREATED, + type: categoryResponseDto, + description: 'create category', + }) + @ApiBearerAuth() + async create( + @Body() CreateCategoryDto: CreateCategoryDto, + ): Promise { + return this.categoryService.create(CreateCategoryDto); + } + + @Get() + @Public() + @ApiResponse({ + status: HttpStatus.OK, + type: categoryResponseDto, + isArray: true, + description: 'get all categories', + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by title', + }) + @ApiQuery({ + name: 'slug', + type: String, + required: false, + description: `search by slug (${Slug.COURSE} or ${Slug.REWARD})`, + }) + async findAll( + @Query() query: PaginateCategoryQueryDto, + ): Promise { + return await this.categoryService.findAll({ + page: query.page, + limit: query.limit, + search: query.search, + slug: query.slug, + }); + } + + @Get(':id') + @Public() + @ApiResponse({ + status: HttpStatus.OK, + type: categoryResponseDto, + description: 'get one category', + }) + async findOne( + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + const category = await this.categoryService.findOne({ where: { id } }); + return new categoryResponseDto(category); + } + + @Patch(':id') + @Roles(Role.ADMIN) + @ApiBearerAuth() + @ApiResponse({ + status: HttpStatus.OK, + description: 'edit category', + type: categoryResponseDto, + }) + async update( + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + @Body() updateCategoryDto: updateCategoryDto, + ): Promise { + return this.categoryService.update(id, updateCategoryDto); + } + + @Delete(':id') + @Roles(Role.ADMIN) + @ApiBearerAuth() + @ApiResponse({ + status: HttpStatus.OK, + description: 'delete category', + }) + async delete( + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + return this.categoryService.delete({ id }); + } +} diff --git a/src/category/category.entity.ts b/src/category/category.entity.ts new file mode 100644 index 0000000..5c4f186 --- /dev/null +++ b/src/category/category.entity.ts @@ -0,0 +1,46 @@ +import { Slug } from 'src/category/enums/slug.enum'; +import { Course } from 'src/course/course.entity'; +import { Entity, OneToMany } from 'typeorm'; +import { + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity() +export class Category { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ + nullable: false, + unique: true, + }) + title: string; + + @Column() + description: string; + + @Column({ + nullable: false, + type: 'enum', + enum: Slug, + }) + slug: Slug; + + @OneToMany(() => Course, (course) => course.category) + courses: Course[]; + + @CreateDateColumn({ + type: 'timestamp', + nullable: false, + }) + createdAt: Date; + + @UpdateDateColumn({ + type: 'timestamp', + nullable: false, + }) + updatedAt: Date; +} diff --git a/src/category/category.module.ts b/src/category/category.module.ts new file mode 100644 index 0000000..da00eab --- /dev/null +++ b/src/category/category.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { DatabaseModule } from 'src/database/database.module'; +import { CategoryController } from './category.controller'; +import { CategoryService } from './category.service'; +import { categoryProviders } from './category.providers'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Category } from './category.entity'; + +@Module({ + imports: [DatabaseModule, TypeOrmModule.forFeature([Category])], + controllers: [CategoryController], + providers: [...categoryProviders, CategoryService], + exports: [CategoryService], +}) +export class CategoryModule {} diff --git a/src/category/category.providers.ts b/src/category/category.providers.ts new file mode 100644 index 0000000..ab28dd7 --- /dev/null +++ b/src/category/category.providers.ts @@ -0,0 +1,10 @@ +import { DataSource } from 'typeorm'; +import { Category } from './category.entity'; + +export const categoryProviders = [ + { + provide: 'CategoryRepository', + useFactory: (DataSource: DataSource) => DataSource.getRepository(Category), + inject: ['DataSource'], + }, +]; diff --git a/src/category/category.service.ts b/src/category/category.service.ts new file mode 100644 index 0000000..c8c3c75 --- /dev/null +++ b/src/category/category.service.ts @@ -0,0 +1,86 @@ +import { + Inject, + Injectable, + NotFoundException, + BadRequestException, +} from '@nestjs/common'; +import { CreateCategoryDto } from './dtos/create-cateory.dto'; +import { Category } from './category.entity'; +import { FindOneOptions, FindOptionsWhere, ILike, Repository } from 'typeorm'; +import { updateCategoryDto } from './dtos/update-category.dto'; +import { Slug } from './enums/slug.enum'; +import { createPagination } from 'src/shared/pagination'; +import { PaginatedCategoryDto } from './dtos/category-response.dto'; + +@Injectable() +export class CategoryService { + constructor( + @Inject('CategoryRepository') + private readonly categoryRepository: Repository, + ) {} + + async findAll({ + page = 1, + limit = 20, + search = '', + slug = '', + }: { + page?: number; + limit?: number; + search?: string; + slug?: Slug | ''; + }): Promise { + const { find } = await createPagination(this.categoryRepository, { + page, + limit, + }); + const conditionSearch = search ? { title: ILike(`%${search}%`) } : {}; + const connditionSlug = slug ? { slug: slug } : {}; + const categories = await find({ + where: { + ...conditionSearch, + ...connditionSlug, + }, + }).run(); + return categories; + } + + async findOne(options: FindOneOptions): Promise { + const category = await this.categoryRepository.findOne(options); + if (!category) throw new NotFoundException('Category not found'); + return category; + } + + async create(CreateCategoryDto: CreateCategoryDto): Promise { + try { + const category = await this.categoryRepository.save(CreateCategoryDto); + return category; + } catch (error) { + if (error instanceof Error) + throw new BadRequestException('category already exists'); + } + } + + async update( + id: string, + updateCategoryDto: updateCategoryDto, + ): Promise { + try { + await this.categoryRepository.update(id, updateCategoryDto); + return this.categoryRepository.findOne({ where: { id } }); + } catch (error) { + if (error instanceof Error) { + throw new NotFoundException('category not found'); + } + } + } + + async delete(options: FindOptionsWhere): Promise { + try { + await this.categoryRepository.delete(options); + } catch (error) { + if (error instanceof Error) + throw new NotFoundException('category not found'); + } + } +} diff --git a/src/category/dtos/category-response.dto.ts b/src/category/dtos/category-response.dto.ts new file mode 100644 index 0000000..020230f --- /dev/null +++ b/src/category/dtos/category-response.dto.ts @@ -0,0 +1,74 @@ +import { Slug } from 'src/category/enums/slug.enum'; +import { Category } from '../category.entity'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { PaginatedResponse } from 'src/shared/pagination/dtos/paginate-response.dto'; + +export class categoryResponseDto { + @ApiProperty({ + description: 'category id', + type: String, + example: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + }) + id: string; + + @ApiProperty({ + description: 'name of category', + type: String, + example: 'javascript', + }) + title: string; + + @ApiPropertyOptional({ + description: 'description of category (optional)', + type: String, + example: 'high level programming language', + }) + description?: string; + + @ApiProperty({ + description: 'slug', + type: String, + example: Slug.COURSE, + enum: Slug, + }) + slug: Slug; + + @ApiProperty({ + description: 'category created date', + type: Date, + example: new Date(), + }) + createdAt: Date; + + @ApiProperty({ + description: 'category updated at', + type: Date, + example: new Date(), + }) + updatedAt: Date; + + constructor(category: Category) { + this.id = category.id; + this.title = category.title; + this.description = category.description; + this.slug = category.slug; + this.createdAt = category.createdAt; + this.updatedAt = category.updatedAt; + } +} + +export class PaginatedCategoryDto extends PaginatedResponse( + categoryResponseDto, +) { + constructor( + categories: Category[], + total: number, + pageSize: number, + currentPage: number, + ) { + const categoryDtos = categories.map( + (category) => new categoryResponseDto(category), + ); + super(categoryDtos, total, pageSize, currentPage); + } +} diff --git a/src/category/dtos/create-cateory.dto.ts b/src/category/dtos/create-cateory.dto.ts new file mode 100644 index 0000000..05a6e6a --- /dev/null +++ b/src/category/dtos/create-cateory.dto.ts @@ -0,0 +1,40 @@ +import { + IsEnum, + IsNotEmpty, + IsOptional, + IsString, +} from '@nestjs/class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Slug } from 'src/category/enums/slug.enum'; + +export class CreateCategoryDto { + @IsNotEmpty() + @IsString() + @ApiProperty({ + description: 'name of category', + type: String, + example: 'javascript', + }) + title: string; + + @IsOptional() + @IsString() + @ApiPropertyOptional({ + description: 'description of category (optional)', + type: String, + example: 'high level programming language', + }) + description?: string; + + @IsEnum(Slug, { + message: `Invalid role. Role should be either ${Slug.COURSE} or ${Slug.REWARD}`, + }) + @IsNotEmpty() + @ApiProperty({ + description: 'slug', + type: String, + example: Slug.COURSE, + enum: Slug, + }) + slug: Slug; +} diff --git a/src/category/dtos/paginate-query-slug.dto.ts b/src/category/dtos/paginate-query-slug.dto.ts new file mode 100644 index 0000000..cb65d8e --- /dev/null +++ b/src/category/dtos/paginate-query-slug.dto.ts @@ -0,0 +1,21 @@ +import { ApiProperty, IntersectionType } from '@nestjs/swagger'; +import { IsEnum, IsOptional } from 'class-validator'; +import { Slug } from 'src/category/enums/slug.enum'; +import { PaginateQueryDto } from 'src/shared/pagination/dtos/paginate-query.dto'; + +class SlugQueryDto { + @ApiProperty({ + name: 'slug', + enum: Slug, + required: false, + description: `sort by slug (${Slug.COURSE} or ${Slug.REWARD})`, + }) + @IsOptional() + @IsEnum(Slug, { message: `slug must be ${Slug.COURSE} or ${Slug.REWARD}` }) + slug: Slug; +} + +export class PaginateCategoryQueryDto extends IntersectionType( + PaginateQueryDto, + SlugQueryDto, +) {} diff --git a/src/category/dtos/update-category.dto.ts b/src/category/dtos/update-category.dto.ts new file mode 100644 index 0000000..5156292 --- /dev/null +++ b/src/category/dtos/update-category.dto.ts @@ -0,0 +1,35 @@ +import { IsOptional, IsString, IsEnum } from '@nestjs/class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Slug } from 'src/category/enums/slug.enum'; + +export class updateCategoryDto { + @IsOptional() + @IsString() + @ApiProperty({ + description: 'name of category', + type: String, + example: 'javascript', + }) + title?: string; + + @IsOptional() + @IsString() + @ApiPropertyOptional({ + description: 'description of category (optional)', + type: String, + example: 'high level programming language', + }) + description?: string; + + @IsEnum(Slug, { + message: `Invalid role. Role should be either ${Slug.COURSE} or ${Slug.REWARD}`, + }) + @IsOptional() + @ApiProperty({ + description: 'slug', + type: String, + example: Slug.COURSE, + enum: Slug, + }) + slug: Slug; +} diff --git a/src/category/enums/slug.enum.ts b/src/category/enums/slug.enum.ts new file mode 100644 index 0000000..c65dfb1 --- /dev/null +++ b/src/category/enums/slug.enum.ts @@ -0,0 +1,4 @@ +export enum Slug { + COURSE = 'course', + REWARD = 'reward', +} diff --git a/src/chapter/chapter.controller.ts b/src/chapter/chapter.controller.ts new file mode 100644 index 0000000..06a9542 --- /dev/null +++ b/src/chapter/chapter.controller.ts @@ -0,0 +1,394 @@ +import { + BadRequestException, + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Injectable, + Param, + ParseFilePipeBuilder, + ParseUUIDPipe, + Patch, + Post, + Query, + Req, + StreamableFile, + UploadedFile, + UseGuards, + UseInterceptors, +} from '@nestjs/common'; +import { + ApiBearerAuth, + ApiBody, + ApiConsumes, + ApiParam, + ApiQuery, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; +import { AuthenticatedRequest } from 'src/auth/interfaces/authenticated-request.interface'; +import { Roles } from 'src/shared/decorators/role.decorator'; +import { Role } from 'src/shared/enums'; +import { PaginateQueryDto } from 'src/shared/pagination/dtos/paginate-query.dto'; +import { ChapterService } from './chapter.service'; +import { + ChapterResponseDto, + PaginatedChapterResponseDto, +} from './dtos/chapter-response.dto'; +import { CreateChapterDto } from './dtos/create-chapter.dto'; +import { UpdateChapterDto } from './dtos/update-chapter.dto'; +import { CourseOwnership } from 'src/shared/decorators/course-ownership.decorator'; +import { CourseModuleService } from 'src/course-module/course-module.service'; +import { ChatRoomResponseDto } from 'src/chat-room/dtos'; +import { ChatRoomService } from 'src/chat-room/chat-room.service'; +import { FileService } from 'src/file/file.service'; +import { Folder } from 'src/file/enums/folder.enum'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { EnrollmentService } from 'src/enrollment/enrollment.service'; +import { Public } from 'src/shared/decorators/public.decorator'; +import { VideoGuard } from './guards/video.guard'; + +@Controller('chapter') +@ApiTags('Chapters') +@Injectable() +export class ChapterController { + constructor( + private readonly chapterService: ChapterService, + private readonly courseModuleService: CourseModuleService, + private readonly fileService: FileService, + ) { } + + @Get(':id/video') + @ApiParam({ + name: 'id', + type: String, + description: 'chapter id', + }) + @Public() + @ApiResponse({ + status: HttpStatus.OK, + description: 'Get chapter video', + type: StreamableFile, + }) + @ApiQuery({ + name: 'token', + type: String, + required: false, + description: 'Access token', + }) + @UseGuards(VideoGuard) + async getVideo( + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + const chapter = await this.chapterService.findOne({ where: { id } }); + const file = await this.fileService.get(Folder.CHAPTER_VIDEOS, chapter.videoKey); + return new StreamableFile(file, { + disposition: 'inline', + type: `video/${chapter.videoKey.split('.').pop()}`, + }); + } + + + + @Patch(':id/video') + @CourseOwnership({ adminDraftOnly: true }) + @ApiBearerAuth() + @ApiResponse({ + status: HttpStatus.NO_CONTENT, + description: 'Video updated successfully', + }) + @HttpCode(HttpStatus.NO_CONTENT) + @UseInterceptors(FileInterceptor('file')) + @ApiConsumes('multipart/form-data') + @ApiBody({ + schema: { + type: 'object', + properties: { + file: { + type: 'string', + format: 'binary', + }, + }, + }, + }) + @ApiParam({ + name: 'id', + type: String, + description: 'Chapter id', + }) + async uploadVideo( + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + @UploadedFile( + new ParseFilePipeBuilder() + .addFileTypeValidator({ fileType: 'video/*' }) + .build({ + fileIsRequired: true, + errorHttpStatusCode: HttpStatus.BAD_REQUEST, + }), + ) + file: Express.Multer.File, + ): Promise { + const chapter = await this.chapterService.findOne({ + where: { id }, + }); + if (chapter.videoKey) + await this.fileService.update(Folder.CHAPTER_VIDEOS, chapter.videoKey, file); + else { + await this.fileService.upload(Folder.CHAPTER_VIDEOS, id, file); + } + await this.chapterService.update(id, { videoKey: `${id}.${file.originalname.split('.').pop()}` }); + } + + @Get() + @ApiResponse({ + status: HttpStatus.OK, + type: ChapterResponseDto, + description: 'Get all chapters', + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by title', + }) + @Public() + async findAll( + @Query() query: PaginateQueryDto, + ): Promise { + return this.chapterService.findAll({ + page: query.page, + limit: query.limit, + search: query.search + }); + } + + @Get('module/:moduleId') + @ApiParam({ + name: 'moduleId', + type: String, + description: 'Module ID', + }) + @ApiResponse({ + status: HttpStatus.OK, + type: ChapterResponseDto, + description: 'Get all chapters by module id', + isArray: true, + }) + @Public() + async findByModuleId( + @Param('moduleId', ParseUUIDPipe) moduleId: string, + ): Promise { + return this.chapterService.findByModuleId(moduleId); + } + + @Get('with-ownership') + @ApiResponse({ + status: HttpStatus.OK, + type: ChapterResponseDto, + description: 'Get all chapters with ownership', + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiBearerAuth() + async findAllWithOwnership( + @Req() request: AuthenticatedRequest, + @Query() query: PaginateQueryDto, + ): Promise { + return this.chapterService.findAllWithOwnership({ + page: query.page, + limit: query.limit, + search: query.search, + userId: request.user.id, + role: request.user.role, + }); + } + @Get('with-ownership/:id') + @ApiResponse({ + status: HttpStatus.OK, + type: ChapterResponseDto, + description: 'Get a chapter by ID with ownership', + }) + @ApiParam({ + name: 'id', + type: String, + description: 'Chapter ID', + }) + @ApiBearerAuth() + async findOneWithOwnership( + @Req() request: AuthenticatedRequest, + @Param('id', ParseUUIDPipe) id: string, + ): Promise { + return this.chapterService.findOneWithOwnership(request.user.id, request.user.role, { + where: { id }, + }); + } + @Get(':id/summarize') + @ApiResponse({ + status: HttpStatus.OK, + type: ChapterResponseDto, + description: 'Summarize a chapter', + }) + @ApiParam({ + name: 'id', + type: String, + description: 'Chapter ID', + }) + @Public() + async summarize(@Param('id', ParseUUIDPipe) id: string) { + const chapter = await this.chapterService.summarize(id); + return new ChapterResponseDto(chapter); + } + + @Get(':id') + @ApiResponse({ + status: HttpStatus.OK, + type: ChapterResponseDto, + description: 'Get a chapter by ID', + }) + @ApiParam({ + name: 'id', + type: String, + description: 'Chapter ID', + }) + @Public() + async findOne( + @Param('id', ParseUUIDPipe) id: string, + ): Promise { + return this.chapterService.findOne({ where: { id, isPreview: true } }); + } + + @Get(':id/chat-rooms') + @ApiResponse({ + status: HttpStatus.OK, + type: ChatRoomResponseDto, + description: 'Get all chat rooms for a chapter', + isArray: true, + }) + @ApiParam({ + name: 'id', + type: String, + description: 'Chapter ID', + }) + @Roles(Role.STUDENT) + @ApiBearerAuth() + async getChatRooms( + @Req() request: AuthenticatedRequest, + @Param('id', ParseUUIDPipe) id: string, + ): Promise { + const chatRooms = await this.chapterService.getChatRooms( + request.user.id, + id, + ); + return chatRooms.map((chatRoom) => new ChatRoomResponseDto(chatRoom)); + } + + + @Post() + @Roles(Role.TEACHER) + @ApiResponse({ + status: HttpStatus.CREATED, + type: ChapterResponseDto, + description: 'Create a chapter', + }) + @ApiBearerAuth() + async create( + @Req() request: AuthenticatedRequest, + @Body() createChapterDto: CreateChapterDto, + ): Promise { + if (createChapterDto.moduleId != null) { + await this.courseModuleService.validateOwnership( + createChapterDto.moduleId, + request.user.id, + ); + } + return this.chapterService.create(createChapterDto); + } + + + + @Patch(':id') + @CourseOwnership({ adminDraftOnly: true }) + @ApiResponse({ + status: HttpStatus.OK, + type: ChapterResponseDto, + description: 'Update a chapter', + }) + @ApiParam({ + name: 'id', + type: String, + description: 'Chapter ID', + }) + @ApiBearerAuth() + async update( + @Req() request: AuthenticatedRequest, + @Param('id', ParseUUIDPipe) id: string, + @Body() updateChapterDto: UpdateChapterDto, + ): Promise { + if (updateChapterDto.moduleId != null) { + await this.courseModuleService.validateOwnership( + updateChapterDto.moduleId, + request.user.id, + ); + } + return this.chapterService.update(id, updateChapterDto); + } + + @Delete(':id') + @CourseOwnership() + @ApiResponse({ + status: HttpStatus.OK, + type: ChapterResponseDto, + description: 'Delete a chapter', + }) + @ApiParam({ + name: 'id', + type: String, + description: 'Chapter ID', + }) + @ApiBearerAuth() + async remove( + @Param('id', ParseUUIDPipe) id: string, + ): Promise { + return this.chapterService.remove(id); + } +} diff --git a/src/chapter/chapter.entity.ts b/src/chapter/chapter.entity.ts new file mode 100644 index 0000000..1633153 --- /dev/null +++ b/src/chapter/chapter.entity.ts @@ -0,0 +1,95 @@ +import { ChatRoom } from 'src/chat-room/chat-room.entity'; +import { CourseModule } from 'src/course-module/course-module.entity'; +import { Progress } from 'src/progress/progress.entity'; +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity() +export class Chapter { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ + type: String, + nullable: false, + }) + title: string; + + @Column({ + type: String, + nullable: false, + }) + description: string; + + @ManyToOne(() => CourseModule, (module) => module.chapters, { + onDelete: 'CASCADE', + nullable: false, + }) + @JoinColumn({ name: 'module_id' }) + module: CourseModule; + @Column({ name: 'module_id' }) + moduleId: string; + + @OneToMany(() => ChatRoom, (chatRoom) => chatRoom.chapter) + chatRooms: ChatRoom[]; + + @OneToMany(() => Progress, (progress) => progress.chapter) + progresses: Progress[]; + + @Column({ + type: String, + nullable: true, + }) + videoKey: string; + + @Column({ + type: String, + nullable: false, + }) + content: string; + + @Column({ + type: String, + nullable: true, + }) + summary: string; + + @Column({ + type: Number, + nullable: false, + }) + duration: number; + + @Column({ + type: Number, + nullable: false, + }) + orderIndex: number; + + @Column({ + type: Boolean, + nullable: false, + default: true, + }) + isPreview: boolean; + + @CreateDateColumn({ + type: 'timestamp', + name: 'created_at', + }) + createdAt: Date; + + @UpdateDateColumn({ + type: 'timestamp', + name: 'updated_at', + }) + updatedAt: Date; +} diff --git a/src/chapter/chapter.module.ts b/src/chapter/chapter.module.ts new file mode 100644 index 0000000..708a18c --- /dev/null +++ b/src/chapter/chapter.module.ts @@ -0,0 +1,31 @@ +import { Module } from '@nestjs/common'; +import { DatabaseModule } from 'src/database/database.module'; +import { ChapterController } from './chapter.controller'; +import { Chapter } from './chapter.entity'; +import { chapterProviders } from './chapter.provider'; +import { ChapterService } from './chapter.service'; +import { CourseModuleModule } from 'src/course-module/course-module.module'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { CourseModule } from 'src/course-module/course-module.entity'; +import { ChatRoomModule } from 'src/chat-room/chat-room.module'; +import { EnrollmentModule } from 'src/enrollment/enrollment.module'; +import { FileModule } from 'src/file/file.module'; +import { HttpModule } from '@nestjs/axios'; +import { ConfigModule } from '@nestjs/config'; + +@Module({ + imports: [ + DatabaseModule, + CourseModuleModule, + TypeOrmModule.forFeature([Chapter, CourseModule]), + ChatRoomModule, + EnrollmentModule, + FileModule, + HttpModule, + ConfigModule + ], + controllers: [ChapterController], + providers: [...chapterProviders, ChapterService], + exports: [ChapterService], +}) +export class ChapterModule {} diff --git a/src/chapter/chapter.provider.ts b/src/chapter/chapter.provider.ts new file mode 100644 index 0000000..c85b713 --- /dev/null +++ b/src/chapter/chapter.provider.ts @@ -0,0 +1,10 @@ +import { DataSource } from 'typeorm'; +import { Chapter } from './chapter.entity'; + +export const chapterProviders = [ + { + provide: 'ChapterRepository', + useFactory: (dataSource: DataSource) => dataSource.getRepository(Chapter), + inject: ['DataSource'], + }, +]; diff --git a/src/chapter/chapter.service.ts b/src/chapter/chapter.service.ts new file mode 100644 index 0000000..bc11b8c --- /dev/null +++ b/src/chapter/chapter.service.ts @@ -0,0 +1,501 @@ +import { + BadRequestException, + ForbiddenException, + Injectable, + InternalServerErrorException, + NotFoundException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { createPagination } from 'src/shared/pagination'; +import { FindOneOptions, FindOptionsWhere, ILike, In, Not, Repository } from 'typeorm'; +import { Chapter } from './chapter.entity'; +import { PaginatedChapterResponseDto } from './dtos/chapter-response.dto'; +import { CreateChapterDto } from './dtos/create-chapter.dto'; +import { UpdateChapterDto } from './dtos/update-chapter.dto'; +import { CourseStatus, Role } from 'src/shared/enums'; +import { ChatRoomService } from 'src/chat-room/chat-room.service'; +import { ChatRoomStatus, ChatRoomType } from 'src/chat-room/enums'; +import { EnrollmentService } from 'src/enrollment/enrollment.service'; +import { ChatRoom } from 'src/chat-room/chat-room.entity'; +import { EnrollmentStatus } from 'src/enrollment/enums/enrollment-status.enum'; +import { ConfigService } from '@nestjs/config'; +import { HttpService } from '@nestjs/axios'; +import { TranscribeResponseDto } from './dtos/transcribe-response.dto'; +import { firstValueFrom } from 'rxjs'; +import { GLOBAL_CONFIG } from 'src/shared/constants/global-config.constant'; +import { SummarizeResponseDto } from './dtos/summarize-response.dto'; + +@Injectable() +export class ChapterService { + + constructor( + @InjectRepository(Chapter) + private readonly chapterRepository: Repository, + private readonly chatRoomService: ChatRoomService, + private readonly enrollmentService: EnrollmentService, + private readonly configService: ConfigService, + private readonly httpService: HttpService, + ) { } + async findAll({ + page = 1, + limit = 20, + search = '', + }: { + page?: number; + limit?: number; + search?: string; + }): Promise { + const { find } = await createPagination(this.chapterRepository, { + page, + limit, + }); + const baseSearch = { + isPreview: true, + ...(search ? { title: ILike(`%${search}%`) } : {}) + }; + const chapters = await find({ + where: baseSearch, + relations: { + module: true, + }, + + }).run(); + return chapters; + } + + + async findAllWithOwnership({ + page = 1, + limit = 20, + search = '', + userId, + role, + }: { + page?: number; + limit?: number; + search?: string; + userId: string; + role: Role; + }): Promise { + const { find } = await createPagination(this.chapterRepository, { + page, + limit, + }); + + const baseSearch = search ? { title: ILike(`%${search}%`) } : {}; + const whereCondition = this.buildWhereCondition(userId, role, baseSearch); + + const chapters = await find({ + where: whereCondition, + relations: { + module: true, + }, + }).run(); + + return chapters; + } + + async findOne(options: FindOneOptions): Promise { + const chapter = await this.chapterRepository.findOne(options); + if (!chapter) throw new NotFoundException('Chapter not found'); + return chapter; + } + + async findByModuleId(moduleId: string): Promise { + const chapters = await this.chapterRepository.find({ + where: { + module: { + id: moduleId, + course: { + status: CourseStatus.PUBLISHED + } + } + }, + relations: { + module: true, + }, + }); + if (!chapters) throw new NotFoundException('Chapter not found'); + return chapters; + } + async findOneWithOwnership( + userId: string, + role: Role, + options: FindOneOptions, + ): Promise { + const baseWhere = options.where as FindOptionsWhere; + const whereCondition = this.buildWhereCondition(userId, role, baseWhere); + + const chapter = await this.chapterRepository.findOne({ + where: whereCondition, + relations: { + module: true, + }, + }); + + if (!chapter) { + throw new NotFoundException('Chapter not found'); + } + + return chapter; + } + + async validateAndGetNextOrderIndex(moduleId: string): Promise { + const existingChapter = await this.chapterRepository.find({ + where: { moduleId }, + order: { orderIndex: 'DESC' }, + }); + + const nextOrderIndex = existingChapter.map((chapter) => chapter.orderIndex); + const hasDuplicates = + new Set(nextOrderIndex).size !== nextOrderIndex.length; + + if (hasDuplicates) { + throw new BadRequestException('Order index is duplicated'); + } + + return nextOrderIndex.length ? nextOrderIndex[0] + 1 : 1; + } + + async create(createChapterDto: CreateChapterDto): Promise { + let orderIndex = await this.validateAndGetNextOrderIndex( + createChapterDto.moduleId, + ); + + const createdChapter = this.chapterRepository.create({ + ...createChapterDto, + orderIndex: orderIndex, + }); + const savedChapter = await this.chapterRepository.save(createdChapter); + await this.chatRoomService.create({ + title: `${savedChapter.title} Questions`, + type: ChatRoomType.QUESTION, + chapterId: savedChapter.id, + status: ChatRoomStatus.ACTIVE, + }); + await this.chatRoomService.create({ + title: `${savedChapter.title} Discussion`, + type: ChatRoomType.DISCUSSION, + chapterId: savedChapter.id, + status: ChatRoomStatus.ACTIVE, + }); + return savedChapter; + } + + async reorderModules(moduleId: string): Promise { + const modulesToReorder = await this.chapterRepository.find({ + where: { moduleId }, + order: { orderIndex: 'ASC' }, + }); + + for (let i = 0; i < modulesToReorder.length; i++) { + modulesToReorder[i].orderIndex = i + 1; + } + + await this.chapterRepository.save(modulesToReorder); + } + + async update( + id: string, + updateChapterDto: UpdateChapterDto, + ): Promise { + const chapter = await this.chapterRepository.findOne({ where: { id } }); + + if (!chapter) { + throw new NotFoundException('Chapter not found'); + } + if (updateChapterDto.orderIndex != null) { + await this.validateOrderIndex( + chapter.moduleId, + updateChapterDto.orderIndex, + ); + } + if ( + updateChapterDto.orderIndex && + updateChapterDto.orderIndex !== chapter.orderIndex + ) { + const existingChapter = await this.chapterRepository.findOne({ + where: { + moduleId: chapter.moduleId, + orderIndex: updateChapterDto.orderIndex, + }, + }); + + if (existingChapter) { + await this.chapterRepository.update(existingChapter.id, { + orderIndex: chapter.orderIndex, + }); + } + } + + Object.assign(chapter, updateChapterDto); + await this.chapterRepository.save(chapter); + + return chapter; + } + + async remove(id: string): Promise { + const chapter = await this.chapterRepository.findOne({ where: { id } }); + + if (!chapter) { + throw new BadRequestException('Chapter not found'); + } + + const result = await this.chapterRepository.remove(chapter); + + await this.reorderModules(chapter.moduleId); + + return result; + } + + async summarize(id: string): Promise { + try { + const chapter = await this.findOne({ where: { id } }); + if (!chapter) { + throw new NotFoundException('Chapter not found'); + } + + if (chapter.summary != null) { + return chapter; + } + + const transcribeResult = await this.transcribeAudio(id); + if (!this.isValidTranscription(transcribeResult)) { + throw new BadRequestException('Invalid transcription response'); + } + + const summarizeResult = await this.summarizeChapter(transcribeResult.transcription); + if (!this.isValidSummary(summarizeResult)) { + throw new BadRequestException('Invalid summary response'); + } + + await this.chapterRepository.update(id, { summary: summarizeResult.summary }); + + return await this.findOne({ where: { id } }); + + } catch (error) { + if (error instanceof BadRequestException) { + throw error; + } + throw new InternalServerErrorException( + 'Failed to process audio summarization', + { cause: error } + ); + } + } + + private async transcribeAudio(id: string): Promise { + const aiUrl = this.configService.get(GLOBAL_CONFIG.AI_URL); + const apiUrl = this.configService.getOrThrow(GLOBAL_CONFIG.API_URL); + + if (!aiUrl || !apiUrl) { + throw new InternalServerErrorException('Missing configuration for AI or API URL'); + } + + try { + const response = await firstValueFrom( + this.httpService.post( + `${aiUrl}/asr`, + { + url: `${apiUrl}/chapter/${id}/video`, + }, + { + headers: { 'Content-Type': 'application/json' } + } + ) + ); + + if (!response.data) { + throw new BadRequestException('Empty response from transcription service'); + } + + return response.data; + + } catch (error) { + if (error.response) { + throw new InternalServerErrorException( + error.response.data?.message || 'Transcription service error' + ); + } + throw new InternalServerErrorException('Error processing transcription request'); + } + } + + private async summarizeChapter(content: string): Promise { + const aiUrl = this.configService.get(GLOBAL_CONFIG.AI_URL); + + if (!aiUrl) { + throw new InternalServerErrorException('Missing AI service URL configuration'); + } + + try { + const response = await firstValueFrom( + this.httpService.post( + `${aiUrl}/summarize`, + { content }, + { + headers: { 'Content-Type': 'application/json' } + } + ) + ); + + if (!response.data) { + throw new BadRequestException('Empty response from summarization service'); + } + + let summaryText = response.data.summary; + + try { + const parsedSummary = JSON.parse(summaryText); + summaryText = parsedSummary.summary || summaryText; + } catch (e) { + } + + return { summary: summaryText }; + + } catch (error) { + if (error.response) { + throw new InternalServerErrorException( + error.response.data?.message || 'Summarization service error' + ); + } + throw new InternalServerErrorException('Error processing summarization request'); + } + } + + private isValidTranscription(result: TranscribeResponseDto | undefined): result is TranscribeResponseDto { + return ( + !!result && + typeof result.transcription === 'string' && + result.transcription.length > 0 + ); + } + + private isValidSummary(result: SummarizeResponseDto | undefined): result is SummarizeResponseDto { + return ( + !!result && + typeof result.summary === 'string' && + result.summary.length > 0 + ); + } + private async validateOrderIndex( + moduleId: string, + orderIndex: number, + ): Promise { + const existingModules = await this.chapterRepository.find({ + where: { moduleId }, + order: { orderIndex: 'ASC' }, + }); + if (existingModules.length === 0) { + if (orderIndex !== 1) { + throw new BadRequestException( + 'Order index should be 1 when there are no modules in the course', + ); + } + return; + } + const minIndex = 1; + const maxIndex = existingModules[existingModules.length - 1].orderIndex; + + if (orderIndex < minIndex || orderIndex > maxIndex) { + throw new BadRequestException( + `Order index must be between ${minIndex} and ${maxIndex}`, + ); + } + } + async validateOwnership(id: string, userId: string): Promise { + const chapter = await this.chapterRepository.findOne({ + where: { id }, + relations: { module: { course: { teacher: true } } }, + }); + if (!chapter) throw new NotFoundException('Chapter not found'); + if (chapter.module.course.teacher.id !== userId) + throw new BadRequestException('You can only access your own courses'); + } + + async getChatRooms(userId: string, chapterId: string): Promise { + try { + const chapter = await this.chapterRepository.findOne({ + where: { id: chapterId }, + relations: { module: { course: true } }, + }); + const courseId = chapter.module.course.id; + const enrollments = await this.enrollmentService.findOne({ + user: { id: userId }, + course: { id: courseId }, + }); + if (!enrollments) + throw new ForbiddenException('You are not enrolled in this course'); + return await this.chatRoomService.find({ + where: { + chapter: { id: chapterId }, + status: ChatRoomStatus.ACTIVE, + }, + }); + } catch (error) { + if (error instanceof Error) throw new NotFoundException(error.message); + } + } + + private buildWhereCondition( + userId: string, + role: Role, + baseCondition: FindOptionsWhere = {}, + ): FindOptionsWhere | FindOptionsWhere[] { + const conditions: Record< + Role, + () => FindOptionsWhere | FindOptionsWhere[] + > = { + [Role.STUDENT]: () => [ + { + ...baseCondition, + module: { + course: { + status: CourseStatus.PUBLISHED, + enrollments: { + user: { id: userId }, + status: Not(EnrollmentStatus.DROPPED) + } + } + }, + }, + { + ...baseCondition, + isPreview: true, + module: { + course: { + status: CourseStatus.PUBLISHED + } + } + }, + ], + [Role.TEACHER]: () => [ + { + ...baseCondition, + isPreview: true, + module: { + course: { + status: CourseStatus.PUBLISHED + } + } + }, + { + ...baseCondition, + module: { + course: { + teacher: { id: userId } + } + } + } + ], + [Role.ADMIN]: () => baseCondition, + }; + + const buildCondition = conditions[role]; + if (!buildCondition) { + throw new BadRequestException('Invalid role'); + } + + return buildCondition(); + } +} diff --git a/src/chapter/dtos/chapter-response.dto.ts b/src/chapter/dtos/chapter-response.dto.ts new file mode 100644 index 0000000..ef1132e --- /dev/null +++ b/src/chapter/dtos/chapter-response.dto.ts @@ -0,0 +1,102 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { CourseModuleResponseDto } from 'src/course-module/dtos/course-module-response.dto'; +import { PaginatedResponse } from 'src/shared/pagination/dtos/paginate-response.dto'; +import { Chapter } from '../chapter.entity'; + +export class ChapterResponseDto { + @ApiProperty({ + description: 'Chapter ID', + type: String, + example: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + }) + id: string; + + @ApiProperty({ + description: 'Chapter title', + type: String, + example: 'Introduction to Variables', + }) + title: string; + + @ApiProperty({ + description: 'Chapter description', + type: String, + example: 'Learn about variables and data types in programming', + }) + description: string; + + @ApiProperty({ + description: 'Chapter content', + type: String, + example: 'This chapter covers the basics of programming', + }) + content: string; + + @ApiProperty({ + description: 'Chapter summary', + type: String, + example: 'This chapter is an introduction to programming', + }) + summary: string; + + @ApiProperty({ + description: 'Chapter duration', + type: Number, + example: 10, + }) + duration: number; + + @ApiProperty({ + description: 'Chapter module', + type: CourseModuleResponseDto, + example: { + id: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + title: 'Introduction to Programming', + description: 'This module is an introduction to programming', + orderIndex: 1, + courseId: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + }, + }) + module: CourseModuleResponseDto; + + @ApiProperty({ + description: 'Chapter created date', + type: Date, + example: new Date(), + }) + createdAt: Date; + + @ApiProperty({ + description: 'Chapter updated date', + type: Date, + example: new Date(), + }) + updatedAt: Date; + + constructor(chapter: Chapter) { + this.id = chapter.id; + this.title = chapter.title; + this.description = chapter.description; + this.content = chapter.content; + this.summary = chapter.summary; + this.duration = chapter.duration; + this.createdAt = chapter.createdAt; + this.updatedAt = chapter.updatedAt; + } +} + +export class PaginatedChapterResponseDto extends PaginatedResponse( + ChapterResponseDto, +) { + constructor( + chapters: Chapter[], + total: number, + pageSize: number, + currentPage: number, + ) { + const chapterDtos = chapters.map( + (chapter) => new ChapterResponseDto(chapter), + ); + super(chapterDtos, total, pageSize, currentPage); + } +} diff --git a/src/chapter/dtos/create-chapter.dto.ts b/src/chapter/dtos/create-chapter.dto.ts new file mode 100644 index 0000000..3a50dfb --- /dev/null +++ b/src/chapter/dtos/create-chapter.dto.ts @@ -0,0 +1,65 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsBoolean, + IsNotEmpty, + IsNumber, + IsString, + IsUUID, +} from 'class-validator'; + +export class CreateChapterDto { + @IsNotEmpty() + @IsString() + @ApiProperty({ + description: 'Chapter title', + type: String, + example: 'Introduction to Programming', + }) + title: string; + + @IsNotEmpty() + @IsString() + @ApiProperty({ + description: 'Chapter description', + type: String, + example: 'This chapter is an introduction to programming', + }) + description: string; + + + @IsNotEmpty() + @IsString() + @ApiProperty({ + description: 'Chapter content', + type: String, + example: 'This chapter covers the basics of programming', + }) + content: string; + + @IsNotEmpty() + @IsNumber() + @ApiProperty({ + description: 'Chapter duration', + type: Number, + example: 10, + }) + duration: number; + + @IsNotEmpty() + @IsUUID(4) + @ApiProperty({ + description: 'Module ID', + type: String, + example: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + }) + moduleId: string; + + @IsNotEmpty() + @IsBoolean() + @ApiProperty({ + description: 'Chapter preview', + type: Boolean, + example: true, + }) + isPreview: boolean; +} diff --git a/src/chapter/dtos/summarize-response.dto.ts b/src/chapter/dtos/summarize-response.dto.ts new file mode 100644 index 0000000..4f6bc6f --- /dev/null +++ b/src/chapter/dtos/summarize-response.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsNotEmpty, IsString } from "class-validator"; + +export class SummarizeResponseDto { + + @IsNotEmpty() + @IsString() + @ApiProperty({ + description: 'summary', + type: String, + example: 'this is a summary of the chapter', + }) + summary: string; +} \ No newline at end of file diff --git a/src/chapter/dtos/transcribe-response.dto.ts b/src/chapter/dtos/transcribe-response.dto.ts new file mode 100644 index 0000000..def93dc --- /dev/null +++ b/src/chapter/dtos/transcribe-response.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsNotEmpty, IsString } from "class-validator"; + +export class TranscribeResponseDto { + + @IsNotEmpty() + @IsString() + @ApiProperty({ + description: 'transcription', + type: String, + example: ' popular interview question concerns the four core concepts in object-oriented programming.', + }) + transcription: string; +} \ No newline at end of file diff --git a/src/chapter/dtos/update-chapter.dto.ts b/src/chapter/dtos/update-chapter.dto.ts new file mode 100644 index 0000000..154f6aa --- /dev/null +++ b/src/chapter/dtos/update-chapter.dto.ts @@ -0,0 +1,25 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateChapterDto } from './create-chapter.dto'; +import { IsNumber, IsOptional, IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class UpdateChapterDto extends PartialType(CreateChapterDto) { + @IsOptional() + @IsNumber() + @ApiProperty({ + description: 'Chapter order index', + type: Number, + example: 1, + }) + orderIndex?: number; + @IsOptional() + @IsString() + @ApiProperty({ + description: 'Chapter video key', + type: String, + example: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + }) + videoKey?: string; + + +} diff --git a/src/chapter/guards/video.guard.ts b/src/chapter/guards/video.guard.ts new file mode 100644 index 0000000..1f46bfd --- /dev/null +++ b/src/chapter/guards/video.guard.ts @@ -0,0 +1,76 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + UnauthorizedException, + NotFoundException, + ForbiddenException, +} from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { JwtPayloadDto } from 'src/auth/dtos/jwt-payload.dto'; +import { ConfigService } from '@nestjs/config'; +import { GLOBAL_CONFIG } from 'src/shared/constants/global-config.constant'; +import { AuthenticatedRequest } from 'src/auth/interfaces/authenticated-request.interface'; +import { EnrollmentService } from 'src/enrollment/enrollment.service'; +import { Role } from 'src/shared/enums'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Chapter } from '../chapter.entity'; +import { Repository } from 'typeorm'; + +@Injectable() +export class VideoGuard implements CanActivate { + constructor( + @InjectRepository(Chapter) + private readonly chapterRepository: Repository, + private readonly jwtService: JwtService, + private readonly configService: ConfigService, + private readonly enrollmentService: EnrollmentService, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const request: AuthenticatedRequest = context.switchToHttp().getRequest(); + const chapterId = request.params.id; + const chapter = await this.chapterRepository.findOne({ + where: { id: chapterId }, + relations: { + module: { + course: { + teacher: true, + }, + }, + } + }); + if (!chapter) throw new NotFoundException('Chapter not found'); + if (chapter.isPreview) return true; + const token = request.query.token as string; + if (!token) throw new UnauthorizedException('Unauthorized access'); + try { + request.user = await this.jwtService.verifyAsync(token, { + secret: this.configService.getOrThrow( + GLOBAL_CONFIG.JWT_ACCESS_SECRET, + ), + }); + } catch (error) { + throw new UnauthorizedException('Unauthorized access'); + } + const userId = request.user.id; + const courseId = chapter.module.course.id; + switch (request.user.role) { + case Role.TEACHER: + if (chapter.module.course.teacher.id !== userId) + throw new ForbiddenException('Insufficient permissions'); + break; + case Role.STUDENT: + const enrollment = await this.enrollmentService.findOne({ + course: { id: courseId }, + user: { id: userId }, + }); + if (!enrollment) + throw new ForbiddenException('Insufficient permissions'); + break; + default: + throw new ForbiddenException('Insufficient permissions'); + } + return true; + } +} diff --git a/src/chat-message/chat-message.controller.ts b/src/chat-message/chat-message.controller.ts new file mode 100644 index 0000000..e802f2e --- /dev/null +++ b/src/chat-message/chat-message.controller.ts @@ -0,0 +1,138 @@ +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Injectable, + InternalServerErrorException, + Param, + ParseUUIDPipe, + Patch, + Post, + Query, + Req, + UseGuards, +} from '@nestjs/common'; +import { ChatMessageService } from './chat-message.service'; +import { ApiTags, ApiBearerAuth, ApiResponse } from '@nestjs/swagger'; +import { Roles } from 'src/shared/decorators/role.decorator'; +import { Role } from 'src/shared/enums'; +import { CreateChatMessageDto, ChatMessageResponseDto } from './dtos'; +import { AuthenticatedRequest } from 'src/auth/interfaces/authenticated-request.interface'; +import { CreateChatMessageGuard } from './guards/create-chat-message.guard'; +import { HttpService } from '@nestjs/axios'; +import { catchError } from 'rxjs'; +import { AxiosError } from 'axios'; +import { ConfigService } from '@nestjs/config'; +import { GLOBAL_CONFIG } from 'src/shared/constants/global-config.constant'; +import { QADto } from './dtos/qa.dto'; + +@Controller('chat-message') +@Injectable() +@ApiTags('Chat Message') +@ApiBearerAuth() +export class ChatMessageController { + constructor( + private readonly chatMessageService: ChatMessageService, + private readonly httpService: HttpService, + private readonly configService: ConfigService, + ) {} + + @Get(':id') + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns a chat message by id', + type: ChatMessageResponseDto, + }) + @Roles(Role.ADMIN) + async findOne( + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ) { + return await this.chatMessageService.findOne({ where: { id } }); + } + + @Post() + @ApiResponse({ + status: HttpStatus.CREATED, + description: 'Creates a chat message', + type: ChatMessageResponseDto, + }) + @HttpCode(HttpStatus.CREATED) + @UseGuards(CreateChatMessageGuard) + @Roles(Role.STUDENT, Role.ADMIN) + async create( + @Body() createChatMessageDto: CreateChatMessageDto, + @Req() request: AuthenticatedRequest, + ) { + const chatMessages = await this.chatMessageService.findMany({ + chatRoom: { id: createChatMessageDto.chatRoomId }, + }); + const chatMessage = await this.chatMessageService.create( + request.user.id, + createChatMessageDto, + ); + const response = this.httpService + .post( + `${this.configService.getOrThrow(GLOBAL_CONFIG.AI_URL)}/qa`, + new QADto('chapterSummary', chatMessages), + ) + .pipe( + catchError((error: AxiosError) => { + throw new InternalServerErrorException(error.message); + }), + ); + return new ChatMessageResponseDto(chatMessage); + } + + @Patch(':id') + @ApiResponse({ + status: HttpStatus.OK, + description: 'Updates a chat message by id', + type: ChatMessageResponseDto, + }) + async update( + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + @Body() updateChatMessageDto: CreateChatMessageDto, + ) { + const chatMessage = await this.chatMessageService.update( + { id }, + updateChatMessageDto, + ); + return new ChatMessageResponseDto(chatMessage); + } + + @Delete(':id') + @ApiResponse({ + status: HttpStatus.NO_CONTENT, + description: 'Deletes a chat message by id', + }) + @HttpCode(HttpStatus.NO_CONTENT) + async delete( + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ) { + await this.chatMessageService.delete({ id }); + } +} diff --git a/src/chat-message/chat-message.entity.ts b/src/chat-message/chat-message.entity.ts new file mode 100644 index 0000000..8fb4c85 --- /dev/null +++ b/src/chat-message/chat-message.entity.ts @@ -0,0 +1,56 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + OneToOne, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { ChatMessageType } from './enums/chat-message-type.enum'; +import { ChatRoom } from 'src/chat-room/chat-room.entity'; +import { User } from 'src/user/user.entity'; + +@Entity() +export class ChatMessage { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ + nullable: false, + }) + content: string; + + @OneToOne(() => ChatMessage, { + nullable: true, + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'reply' }) + reply?: ChatMessage; + + @Column({ + nullable: false, + default: false, + }) + isEdited: boolean; + + @Column({ + type: 'enum', + enum: ChatMessageType, + nullable: false, + }) + type: ChatMessageType; + + @ManyToOne(() => ChatRoom, (chatRoom) => chatRoom.chatMessages, { + nullable: false, + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'chat_room_id' }) + chatRoom: ChatRoom; + + @ManyToOne(() => User, { + nullable: false, + onDelete: 'CASCADE', + eager: true, + }) + user: User; +} diff --git a/src/chat-message/chat-message.module.ts b/src/chat-message/chat-message.module.ts new file mode 100644 index 0000000..b0c61c4 --- /dev/null +++ b/src/chat-message/chat-message.module.ts @@ -0,0 +1,24 @@ +import { Module, forwardRef } from '@nestjs/common'; +import { DatabaseModule } from 'src/database/database.module'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ChatMessage } from './chat-message.entity'; +import { ChatMessageController } from './chat-message.controller'; +import { ChatMessageService } from './chat-message.service'; +import { chatMessageProviders } from './chat-message.providers'; +import { ChatRoomModule } from 'src/chat-room/chat-room.module'; +import { EnrollmentModule } from 'src/enrollment/enrollment.module'; +import { HttpModule } from '@nestjs/axios'; + +@Module({ + imports: [ + DatabaseModule, + TypeOrmModule.forFeature([ChatMessage]), + forwardRef(() => ChatRoomModule), + EnrollmentModule, + HttpModule, + ], + controllers: [ChatMessageController], + providers: [...chatMessageProviders, ChatMessageService], + exports: [ChatMessageService], +}) +export class ChatMessageModule {} diff --git a/src/chat-message/chat-message.providers.ts b/src/chat-message/chat-message.providers.ts new file mode 100644 index 0000000..5a4515b --- /dev/null +++ b/src/chat-message/chat-message.providers.ts @@ -0,0 +1,11 @@ +import { DataSource } from 'typeorm'; +import { ChatMessage } from './chat-message.entity'; + +export const chatMessageProviders = [ + { + provide: 'ChatMessageRepository', + useFactory: (dataSource: DataSource) => + dataSource.getRepository(ChatMessage), + inject: ['DataSource'], + }, +]; diff --git a/src/chat-message/chat-message.service.ts b/src/chat-message/chat-message.service.ts new file mode 100644 index 0000000..f7f154f --- /dev/null +++ b/src/chat-message/chat-message.service.ts @@ -0,0 +1,115 @@ +import { + Injectable, + Inject, + InternalServerErrorException, + NotFoundException, +} from '@nestjs/common'; +import { Repository, FindOptionsWhere, FindOneOptions } from 'typeorm'; +import { ChatMessage } from './chat-message.entity'; +import { createPagination } from 'src/shared/pagination'; +import { + UpdateChatMessageDto, + CreateChatMessageDto, + PaginatedChatMessageResponseDto, +} from './dtos'; + +@Injectable() +export class ChatMessageService { + constructor( + @Inject('ChatMessageRepository') + private readonly chatMessageRepository: Repository, + ) {} + + async create( + userId: string, + createChatMessageDto: CreateChatMessageDto, + ): Promise { + try { + return await this.chatMessageRepository.save({ + ...createChatMessageDto, + user: { id: userId }, + chatRoom: { id: createChatMessageDto.chatRoomId }, + reply: createChatMessageDto.replyId + ? { id: createChatMessageDto.replyId } + : null, + }); + } catch (error) { + if (error instanceof Error) + throw new InternalServerErrorException(error.message); + } + } + + async findMany(where: FindOptionsWhere): Promise { + try { + return await this.chatMessageRepository.find({ + where, + relations: { + user: true, + chatRoom: true, + }, + }); + } catch (error) { + if (error instanceof Error) + throw new InternalServerErrorException(error.message); + } + } + + async findAll({ + where, + page = 1, + limit = 20, + }: { + where: FindOptionsWhere; + page?: number; + limit?: number; + }): Promise { + const { find } = await createPagination(this.chatMessageRepository, { + page, + limit, + }); + const chatMessages = await find({ + where, + relations: { + user: true, + chatRoom: true, + }, + }).run(); + return new PaginatedChatMessageResponseDto( + chatMessages.data, + chatMessages.meta.total, + chatMessages.meta.pageSize, + chatMessages.meta.currentPage, + ); + } + + async findOne(options: FindOneOptions): Promise { + try { + return await this.chatMessageRepository.findOne(options); + } catch (error) { + if (error instanceof Error) throw new NotFoundException(error.message); + } + } + + async update( + criteria: FindOptionsWhere, + updateChatMessageDto: UpdateChatMessageDto, + ): Promise { + try { + await this.chatMessageRepository.update(criteria, { + ...updateChatMessageDto, + isEdited: true, + }); + return await this.findOne({ where: criteria }); + } catch (error) { + if (error instanceof Error) throw new NotFoundException(error.message); + } + } + + async delete(criteria: FindOptionsWhere): Promise { + try { + await this.chatMessageRepository.delete(criteria); + } catch (error) { + if (error instanceof Error) throw new NotFoundException(error.message); + } + } +} diff --git a/src/chat-message/dtos/create-chat-message.dto.ts b/src/chat-message/dtos/create-chat-message.dto.ts new file mode 100644 index 0000000..3447038 --- /dev/null +++ b/src/chat-message/dtos/create-chat-message.dto.ts @@ -0,0 +1,49 @@ +import { + IsString, + IsNotEmpty, + IsOptional, + IsUUID, + IsEnum, +} from 'class-validator'; +import { ChatMessageType } from '../enums/chat-message-type.enum'; +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateChatMessageDto { + @IsString() + @IsNotEmpty() + @ApiProperty({ + description: 'ChatMessage content', + type: String, + example: 'Hello World!', + }) + content: string; + + @IsOptional() + @IsUUID('4') + @ApiProperty({ + description: 'ChatMessage reply ID', + type: String, + example: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + required: false, + }) + replyId?: string; + + @IsEnum(ChatMessageType) + @IsNotEmpty() + @ApiProperty({ + description: 'ChatMessage type', + type: String, + example: ChatMessageType.TEXT, + enum: ChatMessageType, + }) + type: ChatMessageType; + + @IsNotEmpty() + @IsUUID('4') + @ApiProperty({ + description: 'ChatRoom ID', + type: String, + example: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + }) + chatRoomId: string; +} diff --git a/src/chat-message/dtos/index.ts b/src/chat-message/dtos/index.ts new file mode 100644 index 0000000..0b1efc7 --- /dev/null +++ b/src/chat-message/dtos/index.ts @@ -0,0 +1,4 @@ +export * from './create-chat-message.dto'; +export * from './paginated-chat-message-response.dto'; +export * from './update-chat-message.dto'; +export * from './create-chat-message.dto'; diff --git a/src/chat-message/dtos/paginated-chat-message-response.dto.ts b/src/chat-message/dtos/paginated-chat-message-response.dto.ts new file mode 100644 index 0000000..24efef1 --- /dev/null +++ b/src/chat-message/dtos/paginated-chat-message-response.dto.ts @@ -0,0 +1,77 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { PaginatedResponse } from 'src/shared/pagination/dtos/paginate-response.dto'; +import { ChatMessage } from '../chat-message.entity'; +import { ChatMessageType } from '../enums/chat-message-type.enum'; +import { UserResponseDto } from 'src/user/dtos/user-response.dto'; + +export class ChatMessageResponseDto { + @ApiProperty({ + description: 'ChatMessage ID', + type: String, + example: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + }) + id: string; + + @ApiProperty({ + description: 'ChatMessage content', + type: String, + example: 'Hello World!', + }) + content: string; + + @ApiProperty({ + description: 'ChatMessage type', + type: String, + example: ChatMessageType.TEXT, + enum: ChatMessageType, + }) + type: ChatMessageType; + + @ApiProperty({ + description: 'ChatMessage user', + type: UserResponseDto, + }) + user: UserResponseDto; + + @ApiProperty({ + description: 'ChatRoom ID', + type: String, + example: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + }) + chatRoomId: string; + + @ApiProperty({ + description: 'ChatMessage reply', + type: ChatMessageResponseDto, + required: false, + example: ChatMessageResponseDto, + }) + reply?: ChatMessageResponseDto; + + constructor(chatMessage: ChatMessage) { + this.id = chatMessage.id; + this.content = chatMessage.content; + this.type = chatMessage.type; + this.user = new UserResponseDto(chatMessage.user); + this.chatRoomId = chatMessage.chatRoom.id; + this.reply = chatMessage.reply + ? new ChatMessageResponseDto(chatMessage.reply) + : null; + } +} + +export class PaginatedChatMessageResponseDto extends PaginatedResponse( + ChatMessageResponseDto, +) { + constructor( + chatMessages: ChatMessage[], + total: number, + pageSize: number, + currentPage: number, + ) { + const chatMessageDtos = chatMessages.map( + (chatMessage) => new ChatMessageResponseDto(chatMessage), + ); + super(chatMessageDtos, total, pageSize, currentPage); + } +} diff --git a/src/chat-message/dtos/qa.dto.ts b/src/chat-message/dtos/qa.dto.ts new file mode 100644 index 0000000..51d4d92 --- /dev/null +++ b/src/chat-message/dtos/qa.dto.ts @@ -0,0 +1,21 @@ +import { Role } from 'src/shared/enums'; +import { ChatMessage } from '../chat-message.entity'; + +export class QADto { + chapter_summary: string; + chat_history: any[]; + + constructor(chapterSummary: string, chatMessage: ChatMessage[]) { + this.chapter_summary = chapterSummary; + this.chat_history = chatMessage.map((chat) => { + if (chat.user.role === Role.ADMIN) { + return { + agent: chat.content, + }; + } + return { + user: chat.content, + }; + }); + } +} diff --git a/src/chat-message/dtos/update-chat-message.dto.ts b/src/chat-message/dtos/update-chat-message.dto.ts new file mode 100644 index 0000000..da73e49 --- /dev/null +++ b/src/chat-message/dtos/update-chat-message.dto.ts @@ -0,0 +1,30 @@ +import { + IsString, + IsNotEmpty, + IsOptional, + IsUUID, + IsEnum, +} from 'class-validator'; +import { ChatMessageType } from '../enums/chat-message-type.enum'; +import { ApiProperty } from '@nestjs/swagger'; + +export class UpdateChatMessageDto { + @IsString() + @IsNotEmpty() + @ApiProperty({ + description: 'ChatMessage content', + type: String, + example: 'Hello World!', + }) + content: string; + + @IsEnum(ChatMessageType) + @IsNotEmpty() + @ApiProperty({ + description: 'ChatMessage type', + type: String, + example: ChatMessageType.TEXT, + enum: ChatMessageType, + }) + type: ChatMessageType; +} diff --git a/src/chat-message/enums/chat-message-type.enum.ts b/src/chat-message/enums/chat-message-type.enum.ts new file mode 100644 index 0000000..5c85d1c --- /dev/null +++ b/src/chat-message/enums/chat-message-type.enum.ts @@ -0,0 +1,5 @@ +export enum ChatMessageType { + TEXT = 'text', + CODE = 'code', + IMAGE = 'image', +} diff --git a/src/chat-message/guards/create-chat-message.guard.ts b/src/chat-message/guards/create-chat-message.guard.ts new file mode 100644 index 0000000..c730670 --- /dev/null +++ b/src/chat-message/guards/create-chat-message.guard.ts @@ -0,0 +1,46 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + ForbiddenException, +} from '@nestjs/common'; +import { AuthenticatedRequest } from 'src/auth/interfaces/authenticated-request.interface'; +import { Role } from 'src/shared/enums'; +import { CreateChatMessageDto } from '../dtos'; +import { ChatRoomService } from 'src/chat-room/chat-room.service'; +import { EnrollmentService } from 'src/enrollment/enrollment.service'; + +@Injectable() +export class CreateChatMessageGuard implements CanActivate { + constructor( + private readonly chatRoomService: ChatRoomService, + private readonly enrollmentService: EnrollmentService, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const request: AuthenticatedRequest = context.switchToHttp().getRequest(); + if (request.user.role === Role.STUDENT) { + const createChatMessageDto = request.body as CreateChatMessageDto; + const chatRoom = await this.chatRoomService.findOne({ + where: { id: createChatMessageDto.chatRoomId }, + relations: { + chapter: { + module: { + course: true, + }, + }, + }, + }); + const courseId = chatRoom.chapter.module.course.id; + try { + await this.enrollmentService.findOne({ + user: { id: request.user.id }, + course: { id: courseId }, + }); + } catch { + throw new ForbiddenException('You are not enrolled in this course'); + } + } + return true; + } +} diff --git a/src/chat-room/chat-room.controller.ts b/src/chat-room/chat-room.controller.ts new file mode 100644 index 0000000..9e63abc --- /dev/null +++ b/src/chat-room/chat-room.controller.ts @@ -0,0 +1,222 @@ +import { + Controller, + Injectable, + Query, + HttpStatus, + Param, + ParseUUIDPipe, + Post, + HttpCode, + Body, + Patch, + Delete, + UseGuards, + Req, +} from '@nestjs/common'; +import { Get } from '@nestjs/common'; +import { ChatRoomService } from './chat-room.service'; +import { PaginateQueryDto } from 'src/shared/pagination/dtos/paginate-query.dto'; +import { + ApiTags, + ApiBearerAuth, + ApiResponse, + ApiQuery, + ApiParam, +} from '@nestjs/swagger'; +import { + ChatRoomResponseDto, + PaginatedChatRoomResponseDto, +} from './dtos/paginated-chat-room-response.dto'; +import { Roles } from 'src/shared/decorators/role.decorator'; +import { Role } from 'src/shared/enums'; +import { CreateChatRoomDto } from './dtos/create-chat-room.dto'; +import { UpdateChatRoomDto } from './dtos/update-chat-room.dto'; +import { ChatRoomOwnershipGuard } from './guards/chat-room-ownership.guard'; +import { AuthenticatedRequest } from 'src/auth/interfaces/authenticated-request.interface'; +import { PaginatedChatMessageResponseDto } from 'src/chat-message/dtos'; +import { ChatMessageService } from 'src/chat-message/chat-message.service'; + +@Controller('chat-room') +@Injectable() +@ApiTags('Chat Room') +@ApiBearerAuth() +export class ChatRoomController { + constructor( + private readonly chatRoomService: ChatRoomService, + private readonly chatMessageService: ChatMessageService, + ) {} + + @Get(':id/messages') + @ApiResponse({ + status: HttpStatus.OK, + description: 'Get chat room messages', + type: PaginatedChatMessageResponseDto, + }) + @ApiParam({ + name: 'id', + type: String, + required: true, + }) + @Roles(Role.STUDENT) + @UseGuards(ChatRoomOwnershipGuard) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + }) + async findMessages( + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + @Query() query: PaginateQueryDto, + ) { + return await this.chatMessageService.findAll({ + ...query, + where: { + chatRoom: { id }, + }, + }); + } + + @Get() + @ApiResponse({ + status: HttpStatus.OK, + description: 'Get all chat rooms', + type: PaginatedChatRoomResponseDto, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + }) + @Roles(Role.TEACHER) + async findAll( + @Query() query: PaginateQueryDto, + @Req() request: AuthenticatedRequest, + ) { + return await this.chatRoomService.findAll({ + userId: request.user.id, + ...query, + }); + } + + @Get(':id') + @ApiResponse({ + status: HttpStatus.OK, + description: 'Get chat room by id', + type: ChatRoomResponseDto, + }) + @ApiParam({ + name: 'id', + type: String, + required: true, + }) + @Roles(Role.TEACHER, Role.STUDENT) + @UseGuards(ChatRoomOwnershipGuard) + async findById( + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ) { + return new ChatRoomResponseDto( + await this.chatRoomService.findOne({ where: { id } }), + ); + } + + @Post() + @ApiResponse({ + status: HttpStatus.CREATED, + description: 'Create chat room', + type: ChatRoomResponseDto, + }) + @Roles(Role.ADMIN) + @HttpCode(HttpStatus.CREATED) + async create(@Body() createChatRoomDto: CreateChatRoomDto) { + return new ChatRoomResponseDto( + await this.chatRoomService.create(createChatRoomDto), + ); + } + + @Patch(':id') + @ApiResponse({ + status: HttpStatus.OK, + description: 'Update chat room', + type: ChatRoomResponseDto, + }) + @ApiParam({ + name: 'id', + type: String, + required: true, + }) + @Roles(Role.ADMIN) + async update( + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + @Body() updateChatRoomDto: UpdateChatRoomDto, + ) { + return new ChatRoomResponseDto( + await this.chatRoomService.update({ id }, updateChatRoomDto), + ); + } + + @Delete(':id') + @ApiResponse({ + status: HttpStatus.NO_CONTENT, + description: 'Delete chat room', + }) + @ApiParam({ + name: 'id', + type: String, + required: true, + }) + @Roles(Role.ADMIN) + @HttpCode(HttpStatus.NO_CONTENT) + async delete( + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ) { + await this.chatRoomService.delete({ id }); + } +} diff --git a/src/chat-room/chat-room.entity.ts b/src/chat-room/chat-room.entity.ts new file mode 100644 index 0000000..3fcb3e5 --- /dev/null +++ b/src/chat-room/chat-room.entity.ts @@ -0,0 +1,67 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + JoinColumn, + ManyToOne, + OneToMany, +} from 'typeorm'; +import { Chapter } from 'src/chapter/chapter.entity'; +import { ChatRoomType, ChatRoomStatus } from './enums'; +import { ChatMessage } from 'src/chat-message/chat-message.entity'; + +@Entity() +export class ChatRoom { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => Chapter, (chapter) => chapter.chatRooms, { + onDelete: 'CASCADE', + nullable: false, + eager: true, + }) + @JoinColumn({ name: 'chapter_id' }) + chapter: Chapter; + + @Column({ + nullable: false, + }) + title: string; + + @Column({ + nullable: false, + type: 'enum', + enum: ChatRoomType, + default: ChatRoomType.QUESTION, + }) + type: ChatRoomType; + + @Column({ + nullable: false, + type: 'enum', + enum: ChatRoomStatus, + default: ChatRoomStatus.ACTIVE, + }) + status: ChatRoomStatus; + + @CreateDateColumn({ + type: 'timestamp', + }) + createdAt: Date; + + @Column({ + default: 0, + type: 'int', + }) + participantCount: number; + + @UpdateDateColumn({ + type: 'timestamp', + }) + updatedAt: Date; + + @OneToMany(() => ChatMessage, (chatMessage) => chatMessage.chatRoom) + chatMessages: ChatMessage[]; +} diff --git a/src/chat-room/chat-room.module.ts b/src/chat-room/chat-room.module.ts new file mode 100644 index 0000000..f49f440 --- /dev/null +++ b/src/chat-room/chat-room.module.ts @@ -0,0 +1,22 @@ +import { forwardRef, Module } from '@nestjs/common'; +import { DatabaseModule } from 'src/database/database.module'; +import { ChatRoomController } from './chat-room.controller'; +import { ChatRoomService } from './chat-room.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ChatRoom } from './chat-room.entity'; +import { chatRoomProviders } from './chat-room.providers'; +import { EnrollmentModule } from 'src/enrollment/enrollment.module'; +import { ChatMessageModule } from 'src/chat-message/chat-message.module'; + +@Module({ + imports: [ + DatabaseModule, + TypeOrmModule.forFeature([ChatRoom]), + EnrollmentModule, + forwardRef(() => ChatMessageModule), + ], + controllers: [ChatRoomController], + providers: [...chatRoomProviders, ChatRoomService], + exports: [ChatRoomService], +}) +export class ChatRoomModule {} diff --git a/src/chat-room/chat-room.providers.ts b/src/chat-room/chat-room.providers.ts new file mode 100644 index 0000000..74b045a --- /dev/null +++ b/src/chat-room/chat-room.providers.ts @@ -0,0 +1,10 @@ +import { DataSource } from 'typeorm'; +import { ChatRoom } from './chat-room.entity'; + +export const chatRoomProviders = [ + { + provide: 'ChatRoomRepository', + useFactory: (dataSource: DataSource) => dataSource.getRepository(ChatRoom), + inject: ['DataSource'], + }, +]; diff --git a/src/chat-room/chat-room.service.ts b/src/chat-room/chat-room.service.ts new file mode 100644 index 0000000..882d071 --- /dev/null +++ b/src/chat-room/chat-room.service.ts @@ -0,0 +1,104 @@ +import { + Injectable, + Inject, + InternalServerErrorException, + NotFoundException, +} from '@nestjs/common'; +import { + Repository, + FindOneOptions, + FindOptionsWhere, + ILike, + FindManyOptions, +} from 'typeorm'; +import { ChatRoom } from './chat-room.entity'; +import { createPagination } from 'src/shared/pagination'; +import { + UpdateChatRoomDto, + PaginatedChatRoomResponseDto, + CreateChatRoomDto, +} from './dtos'; + +@Injectable() +export class ChatRoomService { + constructor( + @Inject('ChatRoomRepository') + private readonly chatRoomRepository: Repository, + ) {} + + async create(createChatRoomDto: CreateChatRoomDto): Promise { + try { + return await this.chatRoomRepository.save({ + ...createChatRoomDto, + chapter: { id: createChatRoomDto.chapterId }, + }); + } catch (error) { + if (error instanceof Error) + throw new InternalServerErrorException(error.message); + } + } + + async find(criteria: FindManyOptions): Promise { + try { + return await this.chatRoomRepository.find(criteria); + } catch (error) { + if (error instanceof Error) + throw new NotFoundException('Chat room not found'); + } + } + + async findAll({ + userId, + page = 1, + limit = 20, + search = '', + }: { + userId: string; + page?: number; + limit?: number; + search?: string; + }): Promise { + const { find } = await createPagination(this.chatRoomRepository, { + page, + limit, + }); + const chatRooms = await find({ + where: { + title: ILike(`%${search}%`), + chapter: { module: { course: { teacher: { id: userId } } } }, + }, + }).run(); + return new PaginatedChatRoomResponseDto( + chatRooms.data, + chatRooms.meta.total, + chatRooms.meta.pageSize, + chatRooms.meta.currentPage, + ); + } + + async findOne(options: FindOneOptions): Promise { + const chatRoom = await this.chatRoomRepository.findOne(options); + if (!chatRoom) throw new NotFoundException('Chat room not found'); + return chatRoom; + } + + async update( + criteria: FindOptionsWhere, + updateChatRoomDto: UpdateChatRoomDto, + ): Promise { + try { + await this.chatRoomRepository.update(criteria, updateChatRoomDto); + return await this.findOne({ where: criteria }); + } catch (error) { + if (error instanceof Error) throw new NotFoundException(error.message); + } + } + + async delete(criteria: FindOptionsWhere): Promise { + try { + await this.chatRoomRepository.delete(criteria); + } catch (error) { + if (error instanceof Error) throw new NotFoundException(error.message); + } + } +} diff --git a/src/chat-room/dtos/create-chat-room.dto.ts b/src/chat-room/dtos/create-chat-room.dto.ts new file mode 100644 index 0000000..554c50a --- /dev/null +++ b/src/chat-room/dtos/create-chat-room.dto.ts @@ -0,0 +1,41 @@ +import { IsString, IsEnum, IsUUID, IsNotEmpty } from 'class-validator'; +import { ChatRoomType, ChatRoomStatus } from '../enums'; +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateChatRoomDto { + @IsString() + @IsNotEmpty() + @ApiProperty({ + description: 'ChatRoom title', + type: String, + example: 'Chat Room 1', + }) + title: string; + + @IsEnum(ChatRoomType) + @ApiProperty({ + description: 'ChatRoom type', + type: String, + example: ChatRoomType.QUESTION, + enum: ChatRoomType, + }) + type: ChatRoomType; + + @IsEnum(ChatRoomStatus) + @ApiProperty({ + description: 'ChatRoom status', + type: String, + example: ChatRoomStatus.ACTIVE, + enum: ChatRoomStatus, + }) + status: ChatRoomStatus; + + @IsUUID('4') + @IsNotEmpty() + @ApiProperty({ + description: 'Chapter ID', + type: String, + example: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + }) + chapterId: string; +} diff --git a/src/chat-room/dtos/index.ts b/src/chat-room/dtos/index.ts new file mode 100644 index 0000000..7b44003 --- /dev/null +++ b/src/chat-room/dtos/index.ts @@ -0,0 +1,3 @@ +export * from './create-chat-room.dto'; +export * from './paginated-chat-room-response.dto'; +export * from './update-chat-room.dto'; diff --git a/src/chat-room/dtos/paginated-chat-room-response.dto.ts b/src/chat-room/dtos/paginated-chat-room-response.dto.ts new file mode 100644 index 0000000..544d43b --- /dev/null +++ b/src/chat-room/dtos/paginated-chat-room-response.dto.ts @@ -0,0 +1,76 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { PaginatedResponse } from 'src/shared/pagination/dtos/paginate-response.dto'; +import { ChatRoom } from '../chat-room.entity'; +import { ChatRoomType, ChatRoomStatus } from '../enums'; +import { ChapterResponseDto } from 'src/chapter/dtos/chapter-response.dto'; + +export class ChatRoomResponseDto { + @ApiProperty({ + description: 'ChatRoom ID', + type: String, + example: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + }) + id: string; + + @ApiProperty({ + description: 'Chapter', + type: String, + example: 'Chat Room 1', + }) + chapter: ChapterResponseDto; + + @ApiProperty({ + description: 'ChatRoom title', + type: String, + example: 'Chat Room 1', + }) + title: string; + + @ApiProperty({ + description: 'ChatRoom status', + type: String, + example: ChatRoomStatus.ACTIVE, + enum: ChatRoomStatus, + }) + status: ChatRoomStatus; + + @ApiProperty({ + description: 'ChatRoom type', + type: String, + example: ChatRoomType.QUESTION, + enum: ChatRoomType, + }) + type: ChatRoomType; + + @ApiProperty({ + description: 'ChatRoom participant count', + type: Number, + example: 5, + }) + paticipantCount: number; + + constructor(chatRoom: ChatRoom) { + this.id = chatRoom.id; + this.title = chatRoom.title; + this.status = chatRoom.status; + this.type = chatRoom.type; + this.paticipantCount = chatRoom.participantCount; + this.chapter = new ChapterResponseDto(chatRoom.chapter); + } +} + +export class PaginatedChatRoomResponseDto extends PaginatedResponse( + ChatRoomResponseDto, +) { + constructor( + chatRooms: ChatRoom[], + total: number, + pageSize: number, + currentPage: number, + ) { + const chatRoomDtos = chatRooms.map( + (chatRoom) => new ChatRoomResponseDto(chatRoom), + ); + super(chatRoomDtos, total, pageSize, currentPage); + } +} diff --git a/src/chat-room/dtos/update-chat-room.dto.ts b/src/chat-room/dtos/update-chat-room.dto.ts new file mode 100644 index 0000000..a6262fa --- /dev/null +++ b/src/chat-room/dtos/update-chat-room.dto.ts @@ -0,0 +1,32 @@ +import { IsString, IsEnum, IsNotEmpty } from 'class-validator'; +import { ChatRoomType, ChatRoomStatus } from '../enums'; +import { ApiProperty } from '@nestjs/swagger'; + +export class UpdateChatRoomDto { + @IsString() + @IsNotEmpty() + @ApiProperty({ + description: 'ChatRoom name', + type: String, + example: 'Chat Room 1', + }) + name: string; + + @IsEnum(ChatRoomType) + @ApiProperty({ + description: 'ChatRoom type', + type: String, + example: ChatRoomType.QUESTION, + enum: ChatRoomType, + }) + type: ChatRoomType; + + @IsEnum(ChatRoomStatus) + @ApiProperty({ + description: 'ChatRoom status', + type: String, + example: ChatRoomStatus.ACTIVE, + enum: ChatRoomStatus, + }) + status: ChatRoomStatus; +} diff --git a/src/chat-room/enums/chat-room-status.enum.ts b/src/chat-room/enums/chat-room-status.enum.ts new file mode 100644 index 0000000..17c1404 --- /dev/null +++ b/src/chat-room/enums/chat-room-status.enum.ts @@ -0,0 +1,5 @@ +export enum ChatRoomStatus { + ACTIVE = 'active', + CLOSED = 'closed', + ARCHIVED = 'archived', +} diff --git a/src/chat-room/enums/chat-room-type.enum.ts b/src/chat-room/enums/chat-room-type.enum.ts new file mode 100644 index 0000000..ba3c594 --- /dev/null +++ b/src/chat-room/enums/chat-room-type.enum.ts @@ -0,0 +1,4 @@ +export enum ChatRoomType { + QUESTION = 'question', + DISCUSSION = 'discussion', +} diff --git a/src/chat-room/enums/index.ts b/src/chat-room/enums/index.ts new file mode 100644 index 0000000..0ba0c66 --- /dev/null +++ b/src/chat-room/enums/index.ts @@ -0,0 +1,2 @@ +export * from './chat-room-status.enum'; +export * from './chat-room-type.enum'; diff --git a/src/chat-room/guards/chat-room-ownership.guard.ts b/src/chat-room/guards/chat-room-ownership.guard.ts new file mode 100644 index 0000000..d71b9ed --- /dev/null +++ b/src/chat-room/guards/chat-room-ownership.guard.ts @@ -0,0 +1,69 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + NotFoundException, + ForbiddenException, +} from '@nestjs/common'; +import { AuthenticatedRequest } from 'src/auth/interfaces/authenticated-request.interface'; +import { ChatRoomService } from '../chat-room.service'; +import { EnrollmentService } from 'src/enrollment/enrollment.service'; +import { Role } from 'src/shared/enums'; + +@Injectable() +export class ChatRoomOwnershipGuard implements CanActivate { + constructor( + private readonly chatRoomService: ChatRoomService, + private readonly enrollmentService: EnrollmentService, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const request: AuthenticatedRequest = context.switchToHttp().getRequest(); + const userId = request.user.id; + const chatRoomId = request.params.id; + switch (request.user.role) { + case Role.STUDENT: + const chatRoom = await this.chatRoomService.findOne({ + where: { id: chatRoomId }, + relations: { + chapter: { + module: { + course: true, + }, + }, + }, + }); + const course = chatRoom.chapter.module.course; + const enrollment = await this.enrollmentService.findOne({ + user: { id: request.user.id }, + course: { id: course.id }, + }); + if (!enrollment) + throw new NotFoundException('You are not enrolled in this course'); + break; + case Role.TEACHER: + const teacher = await this.chatRoomService.findOne({ + where: { id: chatRoomId }, + relations: { + chapter: { + module: { + course: { + teacher: true, + }, + }, + }, + }, + }); + if (teacher.chapter.module.course.teacher.id !== userId) + throw new NotFoundException( + 'You are not the owner of this chat room', + ); + break; + default: + throw new ForbiddenException( + 'You are not allowed to access this resource', + ); + } + return true; + } +} diff --git a/src/course-module/course-module.controller.ts b/src/course-module/course-module.controller.ts new file mode 100644 index 0000000..fd5cc11 --- /dev/null +++ b/src/course-module/course-module.controller.ts @@ -0,0 +1,250 @@ +import { + BadRequestException, + Body, + Controller, + Delete, + Get, + HttpStatus, + Param, + ParseUUIDPipe, + Patch, + Post, + Query, + Req, +} from '@nestjs/common'; +import { + ApiBearerAuth, + ApiParam, + ApiQuery, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; +import { AuthenticatedRequest } from 'src/auth/interfaces/authenticated-request.interface'; +import { Role } from 'src/shared/enums'; +import { PaginateQueryDto } from 'src/shared/pagination/dtos/paginate-query.dto'; +import { CourseModuleService } from './course-module.service'; +import { + CourseModuleResponseDto, + PaginatedCourseModuleResponseDto, +} from './dtos/course-module-response.dto'; +import { CreateCourseModuleDto } from './dtos/create-course-module.dto'; +import { UpdateCourseModuleDto } from './dtos/update-course-module.dto'; +import { CourseService } from 'src/course/course.service'; +import { CourseOwnership } from 'src/shared/decorators/course-ownership.decorator'; +import { Roles } from 'src/shared/decorators/role.decorator'; +import { Public } from 'src/shared/decorators/public.decorator'; + +@Controller('course-module') +@ApiTags('Course Modules') +export class CourseModuleController { + constructor( + private readonly courseModuleService: CourseModuleService, + private readonly courseService: CourseService, + ) {} + + @Get() + @ApiResponse({ + status: HttpStatus.OK, + type: CourseModuleResponseDto, + description: 'Get all course', + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by email', + }) + @Public() + async findAll( + @Query() query: PaginateQueryDto, + ): Promise { + return this.courseModuleService.findAll({ + page: query.page, + limit: query.limit, + search: query.search, + }); + } + + @Get('with-ownership') + @ApiResponse({ + status: HttpStatus.OK, + type: CourseModuleResponseDto, + description: 'Get all course modules with ownership', + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by title', + }) + @ApiBearerAuth() + async findAllWithOwnership( + @Req() request: AuthenticatedRequest, + @Query() query: PaginateQueryDto, + ): Promise { + return this.courseModuleService.findAllWithOwnership({ + page: query.page, + limit: query.limit, + search: query.search, + userId: request.user.id, + role: request.user.role, + }); + } + + @Get('with-ownership/:id') + @ApiParam({ + name: 'id', + type: String, + description: 'Course Module ID', + }) + @ApiResponse({ + status: HttpStatus.OK, + type: CourseModuleResponseDto, + description: 'Get owned course module', + }) + @ApiBearerAuth() + async findOneWithOwnership( + @Req() request: AuthenticatedRequest, + @Param('id', new ParseUUIDPipe()) id: string, + ): Promise { + return this.courseModuleService.findOneWithOwnership( + request.user.id, + request.user.role, + { where: { id } }, + ); + } + + @Get(':id') + @ApiParam({ + name: 'id', + type: String, + description: 'Course Module ID', + }) + @ApiResponse({ + status: HttpStatus.OK, + type: CourseModuleResponseDto, + description: 'Get a course module', + }) + @Public() + async findOne(@Param('id', new ParseUUIDPipe()) id: string): Promise { + return this.courseModuleService.findOne({ where: { id } }); + } + + @Get('course/:courseId') + @ApiParam({ + name: 'courseId', + type: String, + description: 'Course ID', + }) + @ApiResponse({ + status: HttpStatus.OK, + type: CourseModuleResponseDto, + description: 'Get course modules by course ID', + isArray: true, + }) + @Public() + async findByCourseId( + @Param('courseId', new ParseUUIDPipe()) courseId: string, + ): Promise { + return this.courseModuleService.findByCourseId(courseId); + } + + @Post() + @Roles(Role.TEACHER) + @ApiResponse({ + status: HttpStatus.CREATED, + type: CourseModuleResponseDto, + description: 'Create a course module', + }) + @ApiBearerAuth() + async create( + @Req() request: AuthenticatedRequest, + @Body() createCourseModuleDto: CreateCourseModuleDto, + ): Promise { + if (createCourseModuleDto.courseId != null) { + await this.courseService.validateOwnership( + createCourseModuleDto.courseId, + request.user.id, + ); + } + + return this.courseModuleService.create(createCourseModuleDto); + } + + @Patch(':id') + @ApiParam({ + name: 'id', + type: String, + description: 'Course Module ID', + }) + @CourseOwnership({ adminDraftOnly: true }) + @ApiBearerAuth() + async update( + @Req() request: AuthenticatedRequest, + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + @Body() updateCourseModuleDto: UpdateCourseModuleDto, + ): Promise { + if (updateCourseModuleDto.courseId != null) { + await this.courseService.validateOwnership( + updateCourseModuleDto.courseId, + request.user.id, + ); + } + return this.courseModuleService.update(id, updateCourseModuleDto); + } + + @Delete(':id') + @CourseOwnership() + @ApiParam({ + name: 'id', + type: String, + description: 'Course Module ID', + }) + @ApiResponse({ + status: HttpStatus.OK, + type: CourseModuleResponseDto, + description: 'Delete a course module', + }) + @ApiBearerAuth() + async remove( + @Param('id', new ParseUUIDPipe()) id: string, + ): Promise { + return this.courseModuleService.remove(id); + } + + + +} diff --git a/src/course-module/course-module.entity.ts b/src/course-module/course-module.entity.ts new file mode 100644 index 0000000..476d454 --- /dev/null +++ b/src/course-module/course-module.entity.ts @@ -0,0 +1,68 @@ +import { Chapter } from 'src/chapter/chapter.entity'; +import { Course } from 'src/course/course.entity'; +import { Exam } from 'src/exam/exam.entity'; +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + OneToOne, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity() +export class CourseModule { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ + type: String, + nullable: false, + }) + title: string; + + @Column({ + type: String, + nullable: false, + }) + description: string; + + @Column({ + type: Number, + nullable: false, + }) + orderIndex: number; + + @OneToMany(() => Chapter, (chapter) => chapter.module) + chapters: Chapter[]; + + @ManyToOne(() => Course, (course) => course.modules, { + onDelete: 'CASCADE', + nullable: false, + }) + @JoinColumn({ name: 'course_id' }) + course: Course; + + @Column({ name: 'course_id' }) + courseId: string; + + @OneToOne(() => Exam, (exam) => exam.courseModule, { + cascade: true, + }) + exam: Exam; + + @CreateDateColumn({ + type: 'timestamp', + name: 'created_at', + }) + createdAt: Date; + + @UpdateDateColumn({ + type: 'timestamp', + name: 'updated_at', + }) + updatedAt: Date; +} diff --git a/src/course-module/course-module.module.ts b/src/course-module/course-module.module.ts new file mode 100644 index 0000000..0cfdf1c --- /dev/null +++ b/src/course-module/course-module.module.ts @@ -0,0 +1,19 @@ +import { Module } from '@nestjs/common'; +import { DatabaseModule } from 'src/database/database.module'; +import { CourseModuleController } from './course-module.controller'; +import { courseModuleProviders } from './course-module.provider'; +import { CourseModuleService } from './course-module.service'; +import { CourseModule } from 'src/course/course.module'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +@Module({ + imports: [ + DatabaseModule, + CourseModule, + TypeOrmModule.forFeature([CourseModule]), + ], + controllers: [CourseModuleController], + providers: [...courseModuleProviders, CourseModuleService], + exports: [CourseModuleService], +}) +export class CourseModuleModule {} diff --git a/src/course-module/course-module.provider.ts b/src/course-module/course-module.provider.ts new file mode 100644 index 0000000..b011ea0 --- /dev/null +++ b/src/course-module/course-module.provider.ts @@ -0,0 +1,11 @@ +import { DataSource } from 'typeorm'; +import { CourseModule } from './course-module.entity'; + +export const courseModuleProviders = [ + { + provide: 'CourseModuleRepository', + useFactory: (dataSource: DataSource) => + dataSource.getRepository(CourseModule), + inject: ['DataSource'], + }, +]; diff --git a/src/course-module/course-module.service.ts b/src/course-module/course-module.service.ts new file mode 100644 index 0000000..ce272a5 --- /dev/null +++ b/src/course-module/course-module.service.ts @@ -0,0 +1,316 @@ +import { + BadRequestException, + Inject, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { createPagination } from 'src/shared/pagination'; +import { FindOneOptions, FindOptionsWhere, ILike, Not, Repository } from 'typeorm'; +import { CourseModule } from './course-module.entity'; +import { PaginatedCourseModuleResponseDto } from './dtos/course-module-response.dto'; +import { CreateCourseModuleDto } from './dtos/create-course-module.dto'; +import { UpdateCourseModuleDto } from './dtos/update-course-module.dto'; +import { CourseStatus, Role } from 'src/shared/enums'; +import { EnrollmentStatus } from 'src/enrollment/enums/enrollment-status.enum'; + +@Injectable() +export class CourseModuleService { + constructor( + @Inject('CourseModuleRepository') + private readonly courseModuleRepository: Repository, + ) { } + + async findAll({ + page = 1, + limit = 20, + search = '', + }: { + page?: number; + limit?: number; + search?: string; + }): Promise { + const { find } = await createPagination(this.courseModuleRepository, { page, limit }); + const baseSearch = { + course : { + status: CourseStatus.PUBLISHED + }, + ...(search ? { title: ILike(`%${search}%`) } : {}), + }; + const courseModules = await find({ + where: baseSearch, + relations: { + course: true, + }, + + }).run(); + return courseModules + } + async findOne( + options: FindOneOptions, + ): Promise { + const courseModule = await this.courseModuleRepository.findOne({ + ...options, + relations: { + course: true, + }, + where: { + ...options.where, + course: { + status: CourseStatus.PUBLISHED + }, + }, + }); + + if (!courseModule) throw new NotFoundException('Course Module not found'); + return courseModule; + } + + async findByCourseId(courseId: string): Promise { + const courseModules = await this.courseModuleRepository.find({ + where: { + course: { + id: courseId, + status: CourseStatus.PUBLISHED + } + }, + relations: { + course: true, + }, + }); + + return courseModules; + } + async validateAndGetNextOrderIndex(courseId: string): Promise { + const existingModules = await this.courseModuleRepository.find({ + where: { courseId }, + order: { orderIndex: 'DESC' }, + }); + + const orderIndices = existingModules.map((module) => module.orderIndex); + const hasDuplicates = new Set(orderIndices).size !== orderIndices.length; + + if (hasDuplicates) { + throw new BadRequestException( + 'Duplicate orderIndex values detected in course modules', + ); + } + + return existingModules.length > 0 ? existingModules[0].orderIndex + 1 : 1; + } + + async create( + createCourseModuleDto: CreateCourseModuleDto, + ): Promise { + let orderIndex = await this.validateAndGetNextOrderIndex( + createCourseModuleDto.courseId, + ); + const courseModule = this.courseModuleRepository.create({ + ...createCourseModuleDto, + orderIndex: orderIndex, + }); + await this.courseModuleRepository.save(courseModule); + + return courseModule; + } + + async reorderModules(courseId: string, startIndex: number): Promise { + const modulesToReorder = await this.courseModuleRepository.find({ + where: { courseId }, + order: { orderIndex: 'ASC' }, + }); + + for (let i = 0; i < modulesToReorder.length; i++) { + modulesToReorder[i].orderIndex = i + 1; + } + + await this.courseModuleRepository.save(modulesToReorder); + } + + async update( + id: string, + updateCourseModuleDto: UpdateCourseModuleDto, + ): Promise { + const courseModule = await this.courseModuleRepository.findOne({ + where: { id }, + }); + + if (!courseModule) { + throw new BadRequestException('Course module not found'); + } + if (updateCourseModuleDto.orderIndex != null) { + await this.validateOrderIndex( + courseModule.courseId, + updateCourseModuleDto.orderIndex, + ); + } + if ( + updateCourseModuleDto.orderIndex && + updateCourseModuleDto.orderIndex !== courseModule.orderIndex + ) { + const existingModule = await this.courseModuleRepository.findOne({ + where: { + courseId: courseModule.courseId, + orderIndex: updateCourseModuleDto.orderIndex, + }, + }); + + if (existingModule) { + await this.courseModuleRepository.update(existingModule.id, { + orderIndex: courseModule.orderIndex, + }); + } + } + + Object.assign(courseModule, updateCourseModuleDto); + await this.courseModuleRepository.save(courseModule); + + return courseModule; + } + + async remove(id: string): Promise { + const courseModule = await this.courseModuleRepository.findOne({ + where: { id }, + }); + + if (!courseModule) { + throw new BadRequestException('Course module not found'); + } + + const result = await this.courseModuleRepository.remove(courseModule); + + await this.reorderModules(courseModule.courseId, courseModule.orderIndex); + + return result; + } + private async validateOrderIndex( + courseId: string, + orderIndex: number, + ): Promise { + const existingModules = await this.courseModuleRepository.find({ + where: { courseId }, + order: { orderIndex: 'ASC' }, + }); + if (existingModules.length === 0) { + if (orderIndex !== 1) { + throw new BadRequestException( + 'Order index should be 1 when there are no modules in the course', + ); + } + return; + } + const minIndex = 1; + const maxIndex = existingModules[existingModules.length - 1].orderIndex; + + if (orderIndex < minIndex || orderIndex > maxIndex) { + throw new BadRequestException(`Order index is invalid`); + } + } + async validateOwnership(id: string, userId: string): Promise { + const courseModule = await this.courseModuleRepository.findOne({ + where: { id }, + relations: { course: { teacher: true } }, + }); + if (!courseModule) throw new NotFoundException('Course Module not found'); + if (courseModule.course.teacher.id !== userId) + throw new BadRequestException('You can only access your own courses'); + } + private buildWhereCondition( + userId: string, + role: Role, + baseCondition: FindOptionsWhere = {}, + ) { + const conditions: Record< + Role, + () => FindOptionsWhere | FindOptionsWhere[] + > = { + [Role.STUDENT]: () => ({ + ...baseCondition, + course: { + status: CourseStatus.PUBLISHED, + enrollments: { + user: { id: userId }, + status: Not(EnrollmentStatus.DROPPED), + }, + }, + }), + [Role.TEACHER]: () => [ + { + ...baseCondition, + course: { + status: CourseStatus.PUBLISHED, + }, + }, + { + ...baseCondition, + course: { + teacher: { + id: userId, + }, + }, + }, + ], + [Role.ADMIN]: () => baseCondition, + }; + + const buildCondition = conditions[role]; + + if (!buildCondition) { + throw new BadRequestException('Invalid role'); + } + + return buildCondition(); + } + + async findAllWithOwnership({ + page = 1, + limit = 20, + search = '', + userId, + role, + }: { + page?: number; + limit?: number; + search?: string; + userId: string; + role: Role; + }): Promise { + const { find } = await createPagination(this.courseModuleRepository, { + page, + limit, + }); + + const baseSearch = search ? { title: ILike(`%${search}%`) } : {}; + const whereCondition = this.buildWhereCondition(userId, role, baseSearch); + + const courseModules = await find({ + where: whereCondition, + relations: { + course: true, + }, + }).run(); + + return courseModules; + } + + async findOneWithOwnership( + userId: string, + role: Role, + options: FindOneOptions, + ): Promise { + const baseWhere = options.where as FindOptionsWhere; + const whereCondition = this.buildWhereCondition(userId, role, baseWhere); + + const courseModule = await this.courseModuleRepository.findOne({ + where: whereCondition, + relations: { + course: true, + }, + }); + + if (!courseModule) { + throw new NotFoundException('Course Module not found'); + } + + return courseModule; + } +} diff --git a/src/course-module/dtos/course-module-response.dto.ts b/src/course-module/dtos/course-module-response.dto.ts new file mode 100644 index 0000000..f8b0292 --- /dev/null +++ b/src/course-module/dtos/course-module-response.dto.ts @@ -0,0 +1,86 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { CourseResponseDto } from 'src/course/dtos'; +import { PaginatedResponse } from 'src/shared/pagination/dtos/paginate-response.dto'; +import { CourseModule } from '../course-module.entity'; + +export class CourseModuleResponseDto { + @ApiProperty({ + description: 'Course Module ID', + type: String, + example: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + }) + id: string; + + @ApiProperty({ + description: 'Course Module title', + type: String, + example: 'Introduction to Variables', + }) + title: string; + + @ApiProperty({ + description: 'Course Module description', + type: String, + example: 'Learn about variables and data types in programming', + }) + description: string; + + @ApiProperty({ + description: 'Course Data', + type: CourseResponseDto, + example: { + id: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + title: 'Introduction to Programming', + description: 'This course is an introduction to programming', + thumbnail: 'https://www.example.com/thumbnail.jpg', + duration: 60, + level: 'BEGINNER', + price: 100, + status: 'DRAFT', + createdAt: new Date(), + updatedAt: new Date(), + }, + }) + course: CourseResponseDto; + + @ApiProperty({ + description: 'Course Module created date', + type: Date, + example: new Date(), + }) + createdAt: Date; + + @ApiProperty({ + description: 'Course Module updated date', + type: Date, + example: new Date(), + }) + updatedAt: Date; + + constructor(courseModule: CourseModule) { + this.id = courseModule.id; + this.title = courseModule.title; + this.description = courseModule.description; + this.course = courseModule.course + ? new CourseResponseDto(courseModule.course) + : null; + this.createdAt = courseModule.createdAt; + this.updatedAt = courseModule.updatedAt; + } +} + +export class PaginatedCourseModuleResponseDto extends PaginatedResponse( + CourseModuleResponseDto, +) { + constructor( + courseModules: CourseModule[], + total: number, + pageSize: number, + currentPage: number, + ) { + const courseModuleDtos = courseModules.map( + (courseModule) => new CourseModuleResponseDto(courseModule), + ); + super(courseModuleDtos, total, pageSize, currentPage); + } +} diff --git a/src/course-module/dtos/create-course-module.dto.ts b/src/course-module/dtos/create-course-module.dto.ts new file mode 100644 index 0000000..0917bda --- /dev/null +++ b/src/course-module/dtos/create-course-module.dto.ts @@ -0,0 +1,29 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsUUID } from 'class-validator'; + +export class CreateCourseModuleDto { + @IsNotEmpty() + @ApiProperty({ + description: 'Course Module title', + type: String, + example: 'Introduction to Programming', + }) + title: string; + + @IsNotEmpty() + @ApiProperty({ + description: 'Course Module description', + type: String, + example: 'This module is an introduction to programming', + }) + description: string; + + @IsNotEmpty() + @IsUUID(4) + @ApiProperty({ + description: 'Course ID', + type: String, + example: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + }) + courseId: string; +} diff --git a/src/course-module/dtos/update-course-module.dto.ts b/src/course-module/dtos/update-course-module.dto.ts new file mode 100644 index 0000000..8347954 --- /dev/null +++ b/src/course-module/dtos/update-course-module.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty, PartialType } from '@nestjs/swagger'; +import { CreateCourseModuleDto } from './create-course-module.dto'; +import { IsNumber, IsOptional } from 'class-validator'; + +export class UpdateCourseModuleDto extends PartialType(CreateCourseModuleDto) { + @IsOptional() + @IsNumber() + @ApiProperty({ + description: 'Course Module order index', + type: Number, + example: 1, + }) + orderIndex?: number; +} diff --git a/src/course/course.controller.ts b/src/course/course.controller.ts new file mode 100644 index 0000000..125cc02 --- /dev/null +++ b/src/course/course.controller.ts @@ -0,0 +1,504 @@ +import { + BadRequestException, + Body, + Controller, + Delete, + ForbiddenException, + Get, + HttpCode, + HttpStatus, + Injectable, + Param, + ParseFilePipeBuilder, + ParseUUIDPipe, + Patch, + Post, + Query, + Req, + StreamableFile, + UploadedFile, + UseInterceptors, +} from '@nestjs/common'; +import { + ApiBearerAuth, + ApiBody, + ApiConsumes, + ApiParam, + ApiQuery, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; +import { AuthenticatedRequest } from 'src/auth/interfaces/authenticated-request.interface'; +import { CategoryService } from 'src/category/category.service'; +import { Roles } from 'src/shared/decorators/role.decorator'; +import { CourseStatus, Role } from 'src/shared/enums'; +import { PaginateQueryDto } from 'src/shared/pagination/dtos/paginate-query.dto'; +import { CourseService } from './course.service'; +import { + CourseResponseDto, + CreateCourseDto, + PaginatedCourseResponeDto, + UpdateCourseDto, +} from './dtos/index'; +import { CourseOwnership } from 'src/shared/decorators/course-ownership.decorator'; +import { Public } from 'src/shared/decorators/public.decorator'; +import { FileService } from 'src/file/file.service'; +import { Folder } from 'src/file/enums/folder.enum'; +import { FileInterceptor } from '@nestjs/platform-express'; + +@Controller('course') +@ApiTags('Course') +@Injectable() +export class CourseController { + constructor( + private readonly courseService: CourseService, + private readonly categoryService: CategoryService, + private readonly fileService: FileService, + ) { } + @Get() + @ApiResponse({ + status: HttpStatus.OK, + type: CourseResponseDto, + description: 'Get all course', + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by email', + }) + @Public() + async findAll( + @Query() query: PaginateQueryDto, + ): Promise { + return this.courseService.findAll({ + page: query.page, + limit: query.limit, + search: query.search, + }); + } + @Get('new-arrivals') + @ApiResponse({ + status: HttpStatus.OK, + type: CourseResponseDto, + description: 'Get all new arrivals course', + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by email', + }) + @Public() + async newArrival( + @Query() query: PaginateQueryDto, + ): Promise { + return this.courseService.newArrival({ + page: query.page, + limit: query.limit, + search: query.search, + }); + } + @Get('most-enroll') + @ApiResponse({ + status: HttpStatus.OK, + type: CourseResponseDto, + description: 'Get all most enroll course', + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by email', + }) + @Public() + async mostEnroll( + @Query() query: PaginateQueryDto, + ): Promise { + return this.courseService.mostEnroll({ + page: query.page, + limit: query.limit, + search: query.search, + }); + } + + + @Get('by-teacher/:teacherId') + @ApiResponse({ + status: HttpStatus.OK, + type: CourseResponseDto, + description: 'Get all course by teacher id', + isArray: true, + }) + @ApiParam({ + name: 'teacherId', + type: String, + description: 'Teacher id', + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by email', + }) + @Roles(Role.TEACHER, Role.ADMIN) + @ApiBearerAuth() + async getAllCourseByTeacherId( + @Param( + 'teacherId', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) teacherId: string, + @Req() request: AuthenticatedRequest, + @Query() query: PaginateQueryDto, + ): Promise { + if(request.user.role === Role.TEACHER && request.user.id !== teacherId) { + throw new ForbiddenException('You are not allowed to access this resource'); + } + + return this.courseService.getAllCourseByTeacherId({ + page: query.page, + limit: query.limit, + search: query.search, + teacherId, + }); + } + @Get(':id/thumbnail') + @Public() + @ApiParam({ + name: 'id', + type: String, + description: 'Course id', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Get course thumbnail', + type: StreamableFile, + }) + async getThumbnail( + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + const course = await this.courseService.findOne({ + where: { id }, + }); + const file = await this.fileService.get(Folder.COURSE_THUMBNAILS, course.thumbnailKey); + return new StreamableFile(file, { + disposition: 'inline', + type: `image/${course.thumbnailKey.split('.').pop()}`, + }); + } + + + + @Patch(':id/thumbnail') + @CourseOwnership({ adminDraftOnly: true }) + @ApiBearerAuth() + @ApiResponse({ + status: HttpStatus.NO_CONTENT, + description: 'Thumbnail updated successfully', + }) + @HttpCode(HttpStatus.NO_CONTENT) + @UseInterceptors(FileInterceptor('file')) + @ApiConsumes('multipart/form-data') + @ApiBody({ + schema: { + type: 'object', + properties: { + file: { + type: 'string', + format: 'binary', + }, + }, + }, + }) + @ApiParam({ + name: 'id', + type: String, + description: 'Course id', + }) + async uploadThumbnail( + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + @UploadedFile( + new ParseFilePipeBuilder() + .addFileTypeValidator({ fileType: 'image/*' }) + .build({ + fileIsRequired: true, + errorHttpStatusCode: HttpStatus.BAD_REQUEST, + }), + ) + file: Express.Multer.File, + ): Promise { + const course = await this.courseService.findOne({ + where: { id }, + }); + if (course.thumbnailKey) + await this.fileService.update(Folder.COURSE_THUMBNAILS, course.thumbnailKey, file); + else { + await this.fileService.upload(Folder.COURSE_THUMBNAILS, id, file); + } + await this.courseService.update(id, { thumbnailKey: `${id}.${file.originalname.split('.').pop()}` }); + } + + + + + @Get('with-ownership') + @ApiResponse({ + status: HttpStatus.OK, + type: CourseResponseDto, + description: 'Get all course with ownership', + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by email', + }) + @ApiBearerAuth() + async findAllWithOwnership( + @Req() request: AuthenticatedRequest, + @Query() query: PaginateQueryDto, + ): Promise { + return this.courseService.findAllWithOwnership({ + page: query.page, + limit: query.limit, + search: query.search, + userId: request.user.id, + role: request.user.role, + }); + } + @Get('with-ownership/:id') + @ApiParam({ + name: 'id', + type: String, + description: 'Course id', + }) + @ApiResponse({ + status: HttpStatus.OK, + type: CourseResponseDto, + description: 'Get owned course by id', + }) + @ApiBearerAuth() + async findOneWithOwnership( + @Req() request: AuthenticatedRequest, + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + const course = await this.courseService.findOneWithOwnership( + request.user.id, + request.user.role, + { where: { id } }, + ); + return new CourseResponseDto(course); + } + + @Get(':id') + @ApiParam({ + name: 'id', + type: String, + description: 'Course id', + }) + @ApiResponse({ + status: HttpStatus.OK, + type: CourseResponseDto, + description: 'Get course by id', + }) + @Public() + async findOne( + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + const course = await this.courseService.findOne({ + where: { + id, + status: CourseStatus.PUBLISHED + } + }); + return new CourseResponseDto(course); + } + + @Post() + @Roles(Role.TEACHER) + @ApiBearerAuth() + @ApiResponse({ + status: HttpStatus.CREATED, + type: CourseResponseDto, + description: 'Create course', + }) + async create( + @Req() request: AuthenticatedRequest, + @Body() createCourseDto: CreateCourseDto, + ) { + const category = await this.categoryService.findOne({ + where: { id: createCourseDto.categoryId }, + }); + + if (!category) { + throw new BadRequestException('Category not found'); + } + const course = await this.courseService.create( + request.user.id, + createCourseDto, + ); + + return course; + } + + @Patch(':id') + @CourseOwnership({ adminDraftOnly: true }) + @ApiBearerAuth() + @ApiParam({ + name: 'id', + type: String, + description: 'Course id', + }) + @ApiResponse({ + status: HttpStatus.OK, + type: CourseResponseDto, + description: 'Update course by id', + }) + async update( + @Req() request: AuthenticatedRequest, + @Body() updateCourseDto: UpdateCourseDto, + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + const category = await this.categoryService.findOne({ + where: { id: updateCourseDto.categoryId }, + }); + + if (!category) { + throw new BadRequestException('Category not found'); + } + + const course = await this.courseService.update(id, updateCourseDto); + + return new CourseResponseDto(course); + } + @Delete(':id') + @CourseOwnership() + @ApiParam({ + name: 'id', + type: String, + description: 'Course id', + }) + @ApiBearerAuth() + @ApiResponse({ + status: HttpStatus.OK, + description: 'Delete course by id', + }) + async delete( + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + return await this.courseService.remove(id); + } + + + +} diff --git a/src/course/course.entity.ts b/src/course/course.entity.ts new file mode 100644 index 0000000..a4765d5 --- /dev/null +++ b/src/course/course.entity.ts @@ -0,0 +1,100 @@ +import { Category } from 'src/category/category.entity'; +import { CourseModule } from 'src/course-module/course-module.entity'; +import { Enrollment } from 'src/enrollment/enrollment.entity'; +import { Roadmap } from 'src/roadmap/roadmap.entity'; +import { CourseLevel } from 'src/shared/enums/course-level.enum'; +import { CourseStatus } from 'src/shared/enums/course-status.enum'; +import { User } from 'src/user/user.entity'; +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + JoinTable, + ManyToMany, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity() +export class Course { + @PrimaryGeneratedColumn('uuid') + id: string; + @Column({ + type: String, + nullable: false, + }) + title: string; + @Column({ + type: String, + nullable: false, + }) + description: string; + + @ManyToOne(() => User) + @JoinColumn({ name: 'teacher_id' }) + teacher: User; + + @ManyToOne(() => Category) + @JoinColumn({ name: 'category_id' }) + category: Category; + + @ManyToMany(() => Roadmap, (roadmap) => roadmap.courses) + roadmaps: Roadmap[]; + + @OneToMany(() => CourseModule, (courseModule) => courseModule.course, { + cascade: true, + }) + modules: CourseModule[]; + + @OneToMany(() => Enrollment, (enrollment) => enrollment.course) + enrollments: Enrollment[]; + + @Column({ + type: String, + nullable: true, + }) + thumbnailKey: string; + + @Column({ + type: Number, + nullable: false, + }) + duration: number; + + @Column({ + type: 'enum', + nullable: false, + enum: CourseLevel, + default: CourseLevel.BEGINNER, + }) + level: CourseLevel; + + @Column({ + type: Number, + nullable: false, + }) + price: number; + + @Column({ + type: 'enum', + nullable: false, + enum: CourseStatus, + default: CourseStatus.DRAFT, + }) + status: CourseStatus; + + @CreateDateColumn({ + type: 'timestamp', + nullable: false, + }) + createdAt: Date; + + @UpdateDateColumn({ + type: 'timestamp', + nullable: false, + }) + updatedAt: Date; +} diff --git a/src/course/course.module.ts b/src/course/course.module.ts new file mode 100644 index 0000000..0ed9c1a --- /dev/null +++ b/src/course/course.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { CategoryModule } from 'src/category/category.module'; +import { DatabaseModule } from 'src/database/database.module'; +import { CourseController } from './course.controller'; +import { courseProviders } from './course.provider'; +import { CourseService } from './course.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Course } from './course.entity'; +import { FileModule } from 'src/file/file.module'; + +@Module({ + imports: [DatabaseModule, CategoryModule, TypeOrmModule.forFeature([Course]), FileModule], + controllers: [CourseController], + providers: [...courseProviders, CourseService], + exports: [CourseService], +}) +export class CourseModule {} diff --git a/src/course/course.provider.ts b/src/course/course.provider.ts new file mode 100644 index 0000000..492e8a3 --- /dev/null +++ b/src/course/course.provider.ts @@ -0,0 +1,10 @@ +import { DataSource } from 'typeorm'; +import { Course } from './course.entity'; + +export const courseProviders = [ + { + provide: 'CourseRepository', + useFactory: (dataSource: DataSource) => dataSource.getRepository(Course), + inject: ['DataSource'], + }, +]; diff --git a/src/course/course.service.ts b/src/course/course.service.ts new file mode 100644 index 0000000..52edc4d --- /dev/null +++ b/src/course/course.service.ts @@ -0,0 +1,310 @@ +import { + BadRequestException, + Inject, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { FindOneOptions, FindOptionsWhere, ILike, Not, Repository } from 'typeorm'; +import { Course } from './course.entity'; +import { + CreateCourseDto, + PaginatedCourseResponeDto, + UpdateCourseDto, +} from './dtos/index'; +import { CourseStatus, Role } from 'src/shared/enums'; +import { EnrollmentStatus } from 'src/enrollment/enums/enrollment-status.enum'; +import { createPagination } from 'src/shared/pagination'; + +interface FindAllParams { + page?: number; + limit?: number; + search?: string; +} + +interface FindAllWithOwnershipParams extends FindAllParams { + userId: string; + role: Role; +} + +@Injectable() +export class CourseService { + constructor( + @Inject('CourseRepository') + private readonly courseRepository: Repository, + ) {} + + private readonly defaultRelations = { + teacher: true, + category: true, + }; + + private readonly defaultPagination = { + page: 1, + limit: 20, + }; + + async findAll({ + page = this.defaultPagination.page, + limit = this.defaultPagination.limit, + search = '', + }: FindAllParams): Promise { + const { find } = await this.createPaginatedQuery({ page, limit }); + const whereClause = this.buildSearchClause(search, { + status: CourseStatus.PUBLISHED + }); + + return find({ + where: whereClause, + relations: this.defaultRelations, + }).run(); + } + + async newArrival({ + page = this.defaultPagination.page, + limit = this.defaultPagination.limit, + search = '', + }: FindAllParams): Promise { + const { find } = await this.createPaginatedQuery({ page, limit }); + const whereClause = this.buildSearchClause(search, { + status: CourseStatus.PUBLISHED + }); + + return find({ + where: whereClause, + relations: this.defaultRelations, + order: { + createdAt: 'DESC' + } + }).run(); + } + + + async mostEnroll({ + page = this.defaultPagination.page, + limit = this.defaultPagination.limit, + search = '', + }: FindAllParams): Promise { + const skip = (page - 1) * limit; + + const queryBuilder = this.courseRepository + .createQueryBuilder('course') + .leftJoinAndSelect('course.teacher', 'teacher') + .leftJoinAndSelect('course.category', 'category') + .leftJoin('course.enrollments', 'enrollments') + .where('course.status = :status', { status: CourseStatus.PUBLISHED }); + + if (search) { + queryBuilder.andWhere('course.title ILIKE :search', { search: `%${search}%` }); + } + + const total = await queryBuilder.getCount(); + + const courses = await queryBuilder + .addSelect('COUNT(enrollments.id)', 'enrollmentCount') + .groupBy('course.id') + .addGroupBy('teacher.id') + .addGroupBy('category.id') + .orderBy('COUNT(enrollments.id)', 'DESC') + .offset(skip) + .limit(limit) + .getRawAndEntities(); + + const transformedCourses = courses.entities.map((course, index) => ({ + ...course, + enrollmentCount: parseInt(courses.raw[index].enrollment_count) || 0 + })); + + return new PaginatedCourseResponeDto( + transformedCourses, + total, + limit, + page + ); + } + + + async findAllWithOwnership({ + page = this.defaultPagination.page, + limit = this.defaultPagination.limit, + search = '', + userId, + role, + }: FindAllWithOwnershipParams): Promise { + const { find } = await this.createPaginatedQuery({ page, limit }); + const baseSearch = this.buildSearchClause(search); + const whereCondition = this.buildWhereCondition(userId, role, baseSearch); + + return find({ + where: whereCondition, + relations: this.defaultRelations, + }).run(); + } + + async getAllCourseByTeacherId({ + page = this.defaultPagination.page, + limit = this.defaultPagination.limit, + search = '', + teacherId, + }: FindAllParams & { teacherId: string }): Promise { + const { find } = await this.createPaginatedQuery({ page, limit }); + const whereClause = this.buildSearchClause(search, { + teacher: { id: teacherId } + }); + + return find({ + where: whereClause, + relations: this.defaultRelations, + }).run(); + } + + async findOne(options: FindOneOptions): Promise { + const course = await this.courseRepository.findOne({ + ...options, + relations: this.defaultRelations, + }); + + return this.ensureCourseExists(course); + } + + async findOneWithOwnership( + userId: string, + role: Role, + options: FindOneOptions, + ): Promise { + const baseWhere = options.where as FindOptionsWhere; + const whereCondition = this.buildWhereCondition(userId, role, baseWhere); + + const course = await this.courseRepository.findOne({ + where: whereCondition, + relations: this.defaultRelations, + }); + + return this.ensureCourseExists(course); + } + + async create( + userId: string, + createCourseDto: CreateCourseDto, + ): Promise { + try { + const course = this.courseRepository.create({ + ...createCourseDto, + teacher: { id: userId }, + category: { id: createCourseDto.categoryId }, + }); + + return await this.courseRepository.save(course); + } catch (error) { + if (error instanceof Error) { + throw new BadRequestException(error.message); + } + throw error; + } + } + + async update(id: string, updateCourseDto: UpdateCourseDto): Promise { + const existingCourse = await this.findOne({ where: { id } }); + + this.validateStatusTransition( + existingCourse.status, + updateCourseDto.status, + ); + + const updatedCourse = await this.courseRepository.save({ + ...existingCourse, + ...updateCourseDto, + category: { id: updateCourseDto.categoryId }, + }); + + return updatedCourse; + } + + async remove(id: string): Promise { + const course = await this.findOne({ where: { id } }); + return await this.courseRepository.remove(course); + } + + async validateOwnership(id: string, userId: string): Promise { + const course = await this.courseRepository.findOne({ + where: { id }, + relations: { teacher: true }, + }); + this.ensureCourseExists(course); + + if (course.teacher.id !== userId) { + throw new BadRequestException('You can only access your own courses'); + } + } + + private async createPaginatedQuery({ page, limit }: { page: number; limit: number }) { + return await createPagination(this.courseRepository, { page, limit }); + } + + private buildSearchClause(search: string, additionalCriteria = {}): FindOptionsWhere { + return { + ...additionalCriteria, + ...(search ? { title: ILike(`%${search}%`) } : {}), + }; + } + + private buildWhereCondition( + userId: string, + role: Role, + baseCondition: FindOptionsWhere = {}, + ): FindOptionsWhere | FindOptionsWhere[] { + const roleConditions: Record< + Role, + () => FindOptionsWhere | FindOptionsWhere[] + > = { + [Role.STUDENT]: () => ({ + ...baseCondition, + status: CourseStatus.PUBLISHED, + enrollments: { + user: { id: userId }, + status: Not(EnrollmentStatus.DROPPED), + }, + }), + [Role.TEACHER]: () => [ + { + ...baseCondition, + status: CourseStatus.PUBLISHED, + }, + { + ...baseCondition, + teacher: { id: userId }, + }, + ], + [Role.ADMIN]: () => baseCondition, + }; + + const buildCondition = roleConditions[role]; + if (!buildCondition) { + throw new BadRequestException('Invalid role'); + } + + return buildCondition(); + } + + private validateStatusTransition( + currentStatus: CourseStatus, + newStatus?: CourseStatus, + ): void { + if (!newStatus) return; + + if ( + currentStatus !== CourseStatus.DRAFT && + newStatus === CourseStatus.DRAFT + ) { + throw new BadRequestException( + `Cannot change status back to draft when current status is ${currentStatus}`, + ); + } + } + + private ensureCourseExists(course: Course | null): Course { + if (!course) { + throw new NotFoundException('Course not found'); + } + return course; + } +} \ No newline at end of file diff --git a/src/course/dtos/course-response.dto.ts b/src/course/dtos/course-response.dto.ts new file mode 100644 index 0000000..a11c986 --- /dev/null +++ b/src/course/dtos/course-response.dto.ts @@ -0,0 +1,130 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { categoryResponseDto } from 'src/category/dtos/category-response.dto'; +import { CourseLevel, CourseStatus } from 'src/shared/enums'; +import { PaginatedResponse } from 'src/shared/pagination/dtos/paginate-response.dto'; +import { UserResponseDto } from 'src/user/dtos/user-response.dto'; +import { Course } from '../course.entity'; + +export class CourseResponseDto { + @ApiProperty({ + description: 'Course ID', + type: String, + example: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + }) + id: string; + + @ApiProperty({ + description: 'Course title', + type: String, + example: 'Introduction to Programming', + }) + title: string; + + @ApiProperty({ + description: 'Course description', + type: String, + example: 'This course is an introduction to programming', + }) + description: string; + + @ApiProperty({ + description: 'Teacher Data', + type: UserResponseDto, + example: { + id: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + email: 'johndoe@gmail.com', + role: 'teacher', + createdAt: new Date(), + updatedAt: new Date(), + fullname: 'John Doe', + }, + }) + teacher: UserResponseDto; + + @ApiProperty({ + description: 'Category Data', + type: categoryResponseDto, + example: { + id: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + title: 'Programming', + description: 'Programming courses', + slug: 'programming', + createdAt: new Date(), + updatedAt: new Date(), + }, + }) + category: categoryResponseDto; + + + @ApiProperty({ + description: 'Course duration', + type: Number, + example: 60, + }) + duration: number; + + @ApiProperty({ + description: 'Course level', + type: String, + example: CourseLevel.BEGINNER, + enum: CourseLevel, + }) + level: CourseLevel; + + @ApiProperty({ + description: 'Course price', + type: Number, + example: 100, + }) + price: number; + + @ApiProperty({ + description: 'Course status', + type: String, + example: CourseStatus.DRAFT, + enum: CourseStatus, + }) + status: CourseStatus; + + @ApiProperty({ + description: 'Course created date', + type: Date, + example: new Date(), + }) + createdAt: Date; + + @ApiProperty({ + description: 'Course updated date', + type: Date, + example: new Date(), + }) + updatedAt: Date; + + constructor(course: Course) { + this.id = course.id; + this.title = course.title; + this.description = course.description; + this.teacher = new UserResponseDto(course.teacher); + this.category = new categoryResponseDto(course.category); + this.duration = course.duration; + this.level = course.level; + this.price = course.price; + this.status = course.status; + this.createdAt = course.createdAt; + this.updatedAt = course.updatedAt; + } +} + +export class PaginatedCourseResponeDto extends PaginatedResponse( + CourseResponseDto, +) { + constructor( + courses: Course[], + total: number, + pageSize: number, + currentPage: number, + ) { + const courseDtos = courses.map((course) => new CourseResponseDto(course)); + super(courseDtos, total, pageSize, currentPage); + } +} diff --git a/src/course/dtos/create-course.dto.ts b/src/course/dtos/create-course.dto.ts new file mode 100644 index 0000000..7bab581 --- /dev/null +++ b/src/course/dtos/create-course.dto.ts @@ -0,0 +1,70 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsNotEmpty, IsUUID } from 'class-validator'; +import { CourseLevel, CourseStatus } from 'src/shared/enums/index'; +export class CreateCourseDto { + @IsNotEmpty() + @ApiProperty({ + description: 'Course title', + type: String, + example: 'Introduction to Programming', + }) + title: string; + + @IsNotEmpty() + @ApiProperty({ + description: 'Course description', + type: String, + example: 'This course is an introduction to programming', + }) + description: string; + + @IsUUID(4) + @IsNotEmpty() + @ApiProperty({ + description: 'Category Id', + type: String, + example: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + }) + categoryId: string; + + @IsNotEmpty() + @ApiProperty({ + description: 'Course duration', + type: Number, + example: 60, + }) + duration: number; + + @IsNotEmpty() + @IsEnum(CourseLevel, { + message: `Invalid level. Level should be either ${CourseLevel.BEGINNER}, ${CourseLevel.INTERMEDIATE}, or ${CourseLevel.ADVANCED}`, + }) + @ApiProperty({ + description: 'Course level', + type: String, + example: CourseLevel.BEGINNER, + enum: CourseLevel, + }) + level: CourseLevel; + + @IsNotEmpty() + @ApiProperty({ + description: 'Course price', + type: Number, + example: 100, + }) + price: number; + + @IsNotEmpty() + @IsEnum(CourseStatus, { + message: `Invalid status. Status should be either ${CourseStatus.DRAFT}, ${CourseStatus.PUBLISHED}, or ${CourseStatus.ARCHIVED}`, + }) + @IsNotEmpty() + @ApiProperty({ + description: 'Course status', + type: String, + example: CourseStatus.DRAFT, + enum: CourseStatus, + }) + status: CourseStatus; +} diff --git a/src/course/dtos/index.ts b/src/course/dtos/index.ts new file mode 100644 index 0000000..07fdde7 --- /dev/null +++ b/src/course/dtos/index.ts @@ -0,0 +1,3 @@ +export * from './create-course.dto'; +export * from './update-course.dto'; +export * from './course-response.dto'; diff --git a/src/course/dtos/update-course.dto.ts b/src/course/dtos/update-course.dto.ts new file mode 100644 index 0000000..8fa8ad0 --- /dev/null +++ b/src/course/dtos/update-course.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty, PartialType } from '@nestjs/swagger'; +import { CreateCourseDto } from './index'; +import { IsOptional, IsString } from 'class-validator'; + +export class UpdateCourseDto extends PartialType(CreateCourseDto) { + + @IsOptional() + @IsString() + @ApiProperty({ + description: 'Course thumbnail Key', + type: String, + example: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + }) + thumbnailKey: string; + +} diff --git a/src/database/database.module.ts b/src/database/database.module.ts new file mode 100644 index 0000000..857bc80 --- /dev/null +++ b/src/database/database.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { databaseProviders } from './database.providers'; + +@Module({ + providers: [...databaseProviders], + exports: [...databaseProviders], +}) +export class DatabaseModule {} diff --git a/src/database/database.providers.ts b/src/database/database.providers.ts new file mode 100644 index 0000000..3e390e4 --- /dev/null +++ b/src/database/database.providers.ts @@ -0,0 +1,11 @@ +import { DataSource } from 'typeorm'; +import { databaseConfig } from '../shared/configs/database.config'; + +export const databaseProviders = [ + { + provide: 'DataSource', + useFactory: async () => { + return await new DataSource(databaseConfig).initialize(); + }, + }, +]; diff --git a/src/enrollment/dtos/create-enrollment.dto.ts b/src/enrollment/dtos/create-enrollment.dto.ts new file mode 100644 index 0000000..d1ca343 --- /dev/null +++ b/src/enrollment/dtos/create-enrollment.dto.ts @@ -0,0 +1,73 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsBoolean, + IsEnum, + IsNotEmpty, + IsNumber, + IsOptional, + IsUUID, +} from 'class-validator'; +import { EnrollmentStatus } from '../enums/enrollment-status.enum'; + +export class CreateEnrollmentDto { + @IsNotEmpty() + @IsUUID() + @ApiProperty({ + description: 'User ID', + type: String, + format: 'uuid', + }) + userId: string; + + @IsNotEmpty() + @IsUUID() + @ApiProperty({ + description: 'Course ID', + type: String, + format: 'uuid', + }) + courseId: string; + + @IsOptional() + @IsBoolean() + @ApiProperty({ + description: 'Certificate issued status', + type: Boolean, + default: false, + }) + certificateIssued?: boolean; + + @IsOptional() + @IsNumber() + @ApiProperty({ + description: 'Completion rate', + type: Number, + default: 0, + }) + completionRate?: number; + + @IsOptional() + @IsEnum(EnrollmentStatus) + @ApiProperty({ + description: 'Enrollment status', + enum: EnrollmentStatus, + default: EnrollmentStatus.ACTIVE, + }) + status?: EnrollmentStatus; + + @IsOptional() + @ApiProperty({ + description: 'Enrollment date', + type: Date, + default: new Date(), + }) + enrolledAt: Date; + + @IsOptional() + @ApiProperty({ + description: 'Completion date', + type: Date, + required: false, + }) + completedAt?: Date; +} diff --git a/src/enrollment/dtos/enrollment-response.dto.ts b/src/enrollment/dtos/enrollment-response.dto.ts new file mode 100644 index 0000000..76979d4 --- /dev/null +++ b/src/enrollment/dtos/enrollment-response.dto.ts @@ -0,0 +1,84 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { CourseResponseDto } from 'src/course/dtos'; +import { PaginatedResponse } from 'src/shared/pagination/dtos/paginate-response.dto'; +import { UserResponseDto } from 'src/user/dtos/user-response.dto'; +import { Enrollment } from '../enrollment.entity'; +import { EnrollmentStatus } from '../enums/enrollment-status.enum'; + +export class EnrollmentResponseDto { + @ApiProperty({ + description: 'Enrollment ID', + type: String, + }) + id: string; + + @ApiProperty({ + description: 'User data', + type: UserResponseDto, + }) + user: UserResponseDto; + + @ApiProperty({ + description: 'Course data', + type: CourseResponseDto, + }) + course: CourseResponseDto; + + @ApiProperty({ + description: 'Enrollment status', + enum: EnrollmentStatus, + }) + status: EnrollmentStatus; + + @ApiProperty({ + description: 'Completion rate', + type: Number, + }) + completionRate: number; + + @ApiProperty({ + description: 'Certificate issued status', + type: Boolean, + }) + certificateIssued: boolean; + + @ApiProperty({ + description: 'Enrollment date', + type: Date, + }) + enrolledAt: Date; + + @ApiProperty({ + description: 'Completion date', + type: Date, + nullable: true, + }) + completedAt: Date; + + constructor(enrollment: Enrollment) { + this.id = enrollment.id; + this.user = new UserResponseDto(enrollment.user); + this.course = new CourseResponseDto(enrollment.course); + this.status = enrollment.status; + this.completionRate = enrollment.completionRate; + this.certificateIssued = enrollment.certificateIssued; + this.enrolledAt = enrollment.enrolledAt; + this.completedAt = enrollment.completedAt; + } +} + +export class PaginatedEnrollmentResponseDto extends PaginatedResponse( + EnrollmentResponseDto, +) { + constructor( + enrollments: Enrollment[], + total: number, + pageSize: number, + currentPage: number, + ) { + const enrollmentDtos = enrollments.map( + (enrollment) => new EnrollmentResponseDto(enrollment), + ); + super(enrollmentDtos, total, pageSize, currentPage); + } +} diff --git a/src/enrollment/dtos/update-enrollment.dto.ts b/src/enrollment/dtos/update-enrollment.dto.ts new file mode 100644 index 0000000..44f2c35 --- /dev/null +++ b/src/enrollment/dtos/update-enrollment.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateEnrollmentDto } from './create-enrollment.dto'; + +export class UpdateEnrollmentDto extends PartialType(CreateEnrollmentDto) {} diff --git a/src/enrollment/enrollment.controller.ts b/src/enrollment/enrollment.controller.ts new file mode 100644 index 0000000..6ba16a2 --- /dev/null +++ b/src/enrollment/enrollment.controller.ts @@ -0,0 +1,136 @@ +import { + BadRequestException, + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Injectable, + Param, + ParseUUIDPipe, + Patch, + Post, + Query, + Req, +} from '@nestjs/common'; +import { ApiBearerAuth, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { AuthenticatedRequest } from 'src/auth/interfaces/authenticated-request.interface'; +import { Roles } from 'src/shared/decorators/role.decorator'; +import { Role } from 'src/shared/enums'; +import { PaginateQueryDto } from 'src/shared/pagination/dtos/paginate-query.dto'; +import { CreateEnrollmentDto } from './dtos/create-enrollment.dto'; +import { + EnrollmentResponseDto, + PaginatedEnrollmentResponseDto, +} from './dtos/enrollment-response.dto'; +import { UpdateEnrollmentDto } from './dtos/update-enrollment.dto'; +import { EnrollmentService } from './enrollment.service'; + +@Controller('enrollment') +@ApiTags('Enrollment') +@ApiBearerAuth() +@Injectable() +export class EnrollmentController { + constructor(private readonly enrollmentService: EnrollmentService) {} + + @Get() + @ApiResponse({ + status: HttpStatus.OK, + type: EnrollmentResponseDto, + description: 'Get all enrollments', + isArray: true, + }) + async findAll( + @Query() query: PaginateQueryDto, + ): Promise { + return this.enrollmentService.findAll(query); + } + + @Get('/user') + @ApiResponse({ + status: HttpStatus.OK, + type: EnrollmentResponseDto, + description: 'Get all enrollments by user', + isArray: true, + }) + @Roles(Role.STUDENT) + async findAllByUser( + @Req() request: AuthenticatedRequest, + @Query() query: PaginateQueryDto, + ): Promise { + return await this.enrollmentService.findAllByUser(request.user.id, query); + } + + @Get(':id') + @ApiParam({ + name: 'id', + type: String, + description: 'Enrollment ID', + }) + @ApiResponse({ + status: HttpStatus.OK, + type: EnrollmentResponseDto, + description: 'Get enrollment by ID', + }) + async findOne( + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + return await this.enrollmentService.findOne({ id }); + } + + @Post() + @Roles(Role.STUDENT) + @ApiResponse({ + status: HttpStatus.CREATED, + type: EnrollmentResponseDto, + description: 'Create enrollment', + }) + async create( + @Body() createEnrollmentDto: CreateEnrollmentDto, + ): Promise { + return await this.enrollmentService.create(createEnrollmentDto); + } + + @Patch(':id') + @Roles(Role.STUDENT) + @ApiParam({ + name: 'id', + type: String, + description: 'Enrollment ID', + }) + @ApiResponse({ + status: HttpStatus.OK, + type: EnrollmentResponseDto, + description: 'Update enrollment by ID', + }) + async update( + @Param('id', ParseUUIDPipe) id: string, + @Body() updateEnrollmentDto: UpdateEnrollmentDto, + ): Promise { + return await this.enrollmentService.update(id, updateEnrollmentDto); + } + + @Delete(':id') + @Roles(Role.STUDENT) + @ApiParam({ + name: 'id', + type: String, + description: 'Enrollment ID', + }) + @ApiResponse({ + status: HttpStatus.NO_CONTENT, + description: 'Delete enrollment by ID', + }) + @HttpCode(HttpStatus.NO_CONTENT) + async remove(@Param('id', ParseUUIDPipe) id: string): Promise { + return await this.enrollmentService.remove(id); + } +} diff --git a/src/enrollment/enrollment.entity.ts b/src/enrollment/enrollment.entity.ts new file mode 100644 index 0000000..31eb44a --- /dev/null +++ b/src/enrollment/enrollment.entity.ts @@ -0,0 +1,77 @@ +import { Course } from 'src/course/course.entity'; +import { Progress } from 'src/progress/progress.entity'; +import { User } from 'src/user/user.entity'; +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; +import { EnrollmentStatus } from './enums/enrollment-status.enum'; + +@Entity() +export class Enrollment { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => User, { onDelete: 'CASCADE', eager: true }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @ManyToOne(() => Course, { onDelete: 'CASCADE', eager: true }) + @JoinColumn({ name: 'course_id' }) + course: Course; + + @OneToMany(() => Progress, (progress) => progress.enrollment) + progresses: Progress[]; + + @Column({ + type: 'enum', + enum: EnrollmentStatus, + default: EnrollmentStatus.ACTIVE, + }) + status: EnrollmentStatus; + + @Column({ + type: 'decimal', + precision: 5, + scale: 2, + default: 0, + }) + completionRate: number; + + @Column({ + type: 'boolean', + default: false, + }) + certificateIssued: boolean; + + @Column({ + type: 'timestamp', + name: 'enrolled_at', + }) + enrolledAt: Date; + + @Column({ + type: 'timestamp', + name: 'completed_at', + nullable: true, + }) + completedAt: Date; + + @CreateDateColumn({ + type: 'timestamp', + name: 'created_at', + }) + createdAt: Date; + + @UpdateDateColumn({ + type: 'timestamp', + name: 'updated_at', + }) + updatedAt: Date; +} diff --git a/src/enrollment/enrollment.module.ts b/src/enrollment/enrollment.module.ts new file mode 100644 index 0000000..1e7579b --- /dev/null +++ b/src/enrollment/enrollment.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { EnrollmentController } from './enrollment.controller'; +import { Enrollment } from './enrollment.entity'; +import { EnrollmentService } from './enrollment.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([Enrollment])], + controllers: [EnrollmentController], + providers: [EnrollmentService], + exports: [EnrollmentService], +}) +export class EnrollmentModule {} diff --git a/src/enrollment/enrollment.service.ts b/src/enrollment/enrollment.service.ts new file mode 100644 index 0000000..289ccb7 --- /dev/null +++ b/src/enrollment/enrollment.service.ts @@ -0,0 +1,131 @@ +import { + ConflictException, + Injectable, + InternalServerErrorException, + NotFoundException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { createPagination } from 'src/shared/pagination'; +import { FindOneOptions, FindOptionsWhere, ILike, Repository } from 'typeorm'; +import { CreateEnrollmentDto } from './dtos/create-enrollment.dto'; +import { PaginatedEnrollmentResponseDto } from './dtos/enrollment-response.dto'; +import { UpdateEnrollmentDto } from './dtos/update-enrollment.dto'; +import { Enrollment } from './enrollment.entity'; +import { EnrollmentStatus } from './enums/enrollment-status.enum'; + +@Injectable() +export class EnrollmentService { + constructor( + @InjectRepository(Enrollment) + private readonly enrollmentRepository: Repository, + ) {} + + async findAll({ + page = 1, + limit = 20, + }: { + page?: number; + limit?: number; + }): Promise { + const { find } = await createPagination(this.enrollmentRepository, { + page, + limit, + }); + + const enrollments = await find({ + relations: { + user: true, + course: { + teacher: true, + category: true, + }, + }, + }).run(); + return enrollments; + } + + async findOne(where: FindOptionsWhere): Promise { + const options: FindOneOptions = { + where, + relations: { + user: true, + course: true, + }, + }; + + const enrollment = await this.enrollmentRepository.findOne(options); + + if (!enrollment) throw new NotFoundException('Enrollment not found'); + + return enrollment; + } + + async findAllByUser( + userId: string, + { + page = 1, + limit = 20, + }: { + page?: number; + limit?: number; + }, + ): Promise { + const { find } = await createPagination(this.enrollmentRepository, { + page, + limit, + }); + + const enrollments = await find({ + where: { + user: { id: userId }, + }, + relations: { + user: true, + course: true, + }, + }).run(); + + return enrollments; + } + + async create(createEnrollmentDto: CreateEnrollmentDto): Promise { + const enrollment = await this.enrollmentRepository.findOne({ + where: { + user: { id: createEnrollmentDto.userId }, + course: { id: createEnrollmentDto.courseId }, + }, + }); + if (enrollment) throw new ConflictException('Enrollment already exists'); + try { + const createdEnrollment = + this.enrollmentRepository.create(createEnrollmentDto); + return await this.enrollmentRepository.save({ + ...createdEnrollment, + user: { id: createEnrollmentDto.userId }, + course: { id: createEnrollmentDto.courseId }, + }); + } catch (error) { + throw new InternalServerErrorException(error.message); + } + } + + async update( + id: string, + updateEnrollmentDto: UpdateEnrollmentDto, + ): Promise { + const enrollment = await this.findOne({ + status: EnrollmentStatus.ACTIVE, + id, + }); + this.enrollmentRepository.merge(enrollment, updateEnrollmentDto); + await this.enrollmentRepository.save(enrollment); + return enrollment; + } + + async remove(id: string): Promise { + const enrollment = await this.findOne({ + id, + }); + await this.enrollmentRepository.remove(enrollment); + } +} diff --git a/src/enrollment/enums/enrollment-status.enum.ts b/src/enrollment/enums/enrollment-status.enum.ts new file mode 100644 index 0000000..07420c8 --- /dev/null +++ b/src/enrollment/enums/enrollment-status.enum.ts @@ -0,0 +1,5 @@ +export enum EnrollmentStatus { + ACTIVE = 'active', + COMPLETED = 'completed', + DROPPED = 'dropped', +} diff --git a/src/exam-answer/dtos/create-exam-answer.dto.ts b/src/exam-answer/dtos/create-exam-answer.dto.ts new file mode 100644 index 0000000..d484c51 --- /dev/null +++ b/src/exam-answer/dtos/create-exam-answer.dto.ts @@ -0,0 +1,46 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsBoolean, IsInt, IsNotEmpty, IsOptional } from 'class-validator'; + +export class CreateExamAnswerDto { + @IsOptional() + @ApiProperty({ + description: 'Exam Attempt ID', + type: String, + example: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + }) + examAttemptId?: string; + + @IsNotEmpty() + @ApiProperty({ + description: 'Select Question ID', + type: String, + example: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + }) + selectedOptionId: string; + + @IsNotEmpty() + @ApiProperty({ + description: 'Answer text', + type: String, + example: 'biology', + }) + answerText: string; + + @IsNotEmpty() + @IsBoolean() + @ApiProperty({ + description: 'Is answer correct?', + type: Boolean, + example: false, + }) + isCorrect: boolean; + + @IsOptional() + @IsInt() + @ApiProperty({ + description: 'Points in this answer', + type: Number, + example: 0, + }) + points?: number; +} diff --git a/src/exam-answer/dtos/exam-answer-pretest-response.dto.ts b/src/exam-answer/dtos/exam-answer-pretest-response.dto.ts new file mode 100644 index 0000000..f72fc85 --- /dev/null +++ b/src/exam-answer/dtos/exam-answer-pretest-response.dto.ts @@ -0,0 +1,146 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { ExamAnswer } from '../exam-answer.entity'; +import { PaginatedResponse } from 'src/shared/pagination/dtos/paginate-response.dto'; +import { ExamAttempt } from 'src/exam-attempt/exam-attempt.entity'; +import { Question } from 'src/question/question.entity'; +import { QuestionOption } from 'src/question-option/question-option.entity'; +import { ExamAttemptResponseDto } from 'src/exam-attempt/dtos/exam-attempt-response.dto'; + +export class ExamAnswerPretestResponseDto { + @ApiProperty({ + description: 'Exam Answer ID', + type: String, + example: '123e4567-e89b-12d3-a456-426614174000', + }) + id: string; + + @ApiProperty({ + description: 'Exam Attempt Data', + type: ExamAttemptResponseDto, + example: { + score: '0', + id: '195abd69-0c8f-4fd3-88cb-e02e08917b1f', + status: 'in_progress', + startedAt: '2024-11-27T11:11:10.292Z', + submittedAt: null, + user: { + id: 'a12e1e37-3504-4711-a389-09a1734d7b1c', + }, + }, + }) + examAttempt: ExamAttemptResponseDto; + + @ApiProperty({ + description: 'Question Data', + type: Question, + example: { + points: 1, + orderIndex: 1, + id: 'b43d7571-f371-48a9-bee0-542f6fe154bb', + question: + 'What is the primary function of a microprocessor in a computer system?', + type: 'pretest', + pretest: { + timeLimit: 20, + id: 'b269a451-87a9-41f8-86be-d5c3ecdfa934', + title: 'Biology', + description: 'This course is an introduction to biology', + passingScore: 3, + maxAttempts: 1, + user: { + id: 'b269a451-87a9-41f8-86be-d5c3ecdfa934', + }, + }, + }, + }) + question: Question; + + @ApiProperty({ + description: 'Select Question Data', + type: QuestionOption, + example: { + id: '9d77c1d9-b0d1-4025-880c-48073e9dc7d5', + questionId: '1e251a62-6339-4a59-bb56-e338f1dae55b', + isCorrect: false, + explanation: 'Rock is not biology.', + optionText: 'ROM', + }, + }) + selectedOption: QuestionOption; + + @ApiProperty({ + description: 'Select Question Data', + type: QuestionOption, + example: { + id: '9d77c1d9-b0d1-4025-880c-48073e9dc7d5', + questionId: '1e251a62-6339-4a59-bb56-e338f1dae55b', + isCorrect: true, + explanation: 'Rock is not biology.', + optionText: 'ROM', + }, + }) + correctAnswer: QuestionOption; + + @ApiProperty({ + description: 'Answer text', + type: String, + example: 'biology', + }) + answerText: string; + + @ApiProperty({ + description: 'Is answer correct?', + type: Boolean, + example: false, + }) + isCorrect: boolean; + + @ApiProperty({ + description: 'Points in this answer', + type: Number, + example: 0, + }) + points: number = 0; + + @ApiProperty({ + description: 'Exam created date', + type: Date, + example: new Date(), + }) + createdAt: Date; + + @ApiProperty({ + description: 'Exam updated date', + type: Date, + example: new Date(), + }) + updatedAt: Date; + + constructor(examAnswer: ExamAnswer) { + this.id = examAnswer.id; + this.examAttempt = examAnswer.examAttempt; + this.question = examAnswer.question; + this.selectedOption = examAnswer.selectedOption; + this.correctAnswer = examAnswer.correctAnswer; + this.isCorrect = examAnswer.isCorrect; + this.points = examAnswer.points; + this.createdAt = examAnswer.createdAt; + this.updatedAt = examAnswer.updatedAt; + } +} + +export class PaginatedExamAnswerPretestResponseDto extends PaginatedResponse( + ExamAnswerPretestResponseDto, +) { + constructor( + examAnswer: ExamAnswer[], + total: number, + pageSize: number, + currentPage: number, + ) { + const examAnswerDtos = examAnswer.map( + (examAnswer) => new ExamAnswerPretestResponseDto(examAnswer), + ); + super(examAnswerDtos, total, pageSize, currentPage); + } +} diff --git a/src/exam-answer/dtos/exam-answer-response.dto.ts b/src/exam-answer/dtos/exam-answer-response.dto.ts new file mode 100644 index 0000000..53e0a6d --- /dev/null +++ b/src/exam-answer/dtos/exam-answer-response.dto.ts @@ -0,0 +1,133 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { ExamAnswer } from '../exam-answer.entity'; +import { PaginatedResponse } from 'src/shared/pagination/dtos/paginate-response.dto'; +import { ExamAttempt } from 'src/exam-attempt/exam-attempt.entity'; +import { Question } from 'src/question/question.entity'; +import { QuestionOption } from 'src/question-option/question-option.entity'; +import { ExamAttemptResponseDto } from 'src/exam-attempt/dtos/exam-attempt-response.dto'; + +export class ExamAnswerResponseDto { + @ApiProperty({ + description: 'Exam Answer ID', + type: String, + example: '123e4567-e89b-12d3-a456-426614174000', + }) + id: string; + + @ApiProperty({ + description: 'Exam Attempt Data', + type: ExamAttemptResponseDto, + example: { + score: '0', + id: '195abd69-0c8f-4fd3-88cb-e02e08917b1f', + status: 'in_progress', + startedAt: '2024-11-27T11:11:10.292Z', + submittedAt: null, + user: { + id: 'a12e1e37-3504-4711-a389-09a1734d7b1c', + }, + }, + }) + examAttempt: ExamAttemptResponseDto; + + @ApiProperty({ + description: 'Question Data', + type: Question, + example: { + id: '1e251a62-6339-4a59-bb56-e338f1dae55b', + question: 'What is this?', + type: 'true_false', + points: 1, + orderIndex: 1, + }, + }) + question: Question; + + @ApiProperty({ + description: 'Select Question Data', + type: QuestionOption, + example: { + id: '9d77c1d9-b0d1-4025-880c-48073e9dc7d5', + questionId: '1e251a62-6339-4a59-bb56-e338f1dae55b', + isCorrect: false, + explanation: 'Rock is not biology.', + }, + }) + selectedOption: QuestionOption; + + @ApiProperty({ + description: 'Select Question Data', + type: QuestionOption, + example: { + id: '9d77c1d9-b0d1-4025-880c-48073e9dc7d5', + questionId: '1e251a62-6339-4a59-bb56-e338f1dae55b', + isCorrect: true, + explanation: 'Rock is not biology.', + optionText: 'ROM', + }, + }) + correctAnswer: QuestionOption; + + @ApiProperty({ + description: 'Answer text', + type: String, + example: 'biology', + }) + answerText: string; + + @ApiProperty({ + description: 'Is answer correct?', + type: Boolean, + example: false, + }) + isCorrect: boolean; + + @ApiProperty({ + description: 'Points in this answer', + type: Number, + example: 0, + }) + points: number = 0; + + @ApiProperty({ + description: 'Exam created date', + type: Date, + example: new Date(), + }) + createdAt: Date; + + @ApiProperty({ + description: 'Exam updated date', + type: Date, + example: new Date(), + }) + updatedAt: Date; + + constructor(examAnswer: ExamAnswer) { + this.id = examAnswer.id; + this.examAttempt = examAnswer.examAttempt; + this.question = examAnswer.question; + this.selectedOption = examAnswer.selectedOption; + this.correctAnswer = examAnswer.correctAnswer; + this.isCorrect = examAnswer.isCorrect; + this.points = examAnswer.points; + this.createdAt = examAnswer.createdAt; + this.updatedAt = examAnswer.updatedAt; + } +} + +export class PaginatedExamAnswerResponseDto extends PaginatedResponse( + ExamAnswerResponseDto, +) { + constructor( + examAnswer: ExamAnswer[], + total: number, + pageSize: number, + currentPage: number, + ) { + const examAnswerDtos = examAnswer.map( + (examAnswer) => new ExamAnswerResponseDto(examAnswer), + ); + super(examAnswerDtos, total, pageSize, currentPage); + } +} diff --git a/src/exam-answer/dtos/update-exam-answer.dto.ts b/src/exam-answer/dtos/update-exam-answer.dto.ts new file mode 100644 index 0000000..c4d4b52 --- /dev/null +++ b/src/exam-answer/dtos/update-exam-answer.dto.ts @@ -0,0 +1,6 @@ +import { OmitType, PartialType } from '@nestjs/swagger'; +import { CreateExamAnswerDto } from './create-exam-answer.dto'; + +export class UpdateExamAnswerDto extends PartialType( + OmitType(CreateExamAnswerDto, ['selectedOptionId'] as const), +) {} diff --git a/src/exam-answer/exam-answer.controller.ts b/src/exam-answer/exam-answer.controller.ts new file mode 100644 index 0000000..b7e6e76 --- /dev/null +++ b/src/exam-answer/exam-answer.controller.ts @@ -0,0 +1,447 @@ +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Injectable, + Param, + ParseUUIDPipe, + Patch, + Post, + Query, + Req, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth, ApiResponse, ApiQuery } from '@nestjs/swagger'; +import { ExamAnswerService } from './exam-answer.service'; +import { + ExamAnswerResponseDto, + PaginatedExamAnswerResponseDto, +} from './dtos/exam-answer-response.dto'; +import { AuthenticatedRequest } from 'src/auth/interfaces/authenticated-request.interface'; +import { PaginateQueryDto } from 'src/shared/pagination/dtos/paginate-query.dto'; +import { Roles } from 'src/shared/decorators/role.decorator'; +import { Role } from 'src/shared/enums'; +import { CreateExamAnswerDto } from './dtos/create-exam-answer.dto'; +import { UpdateExamAnswerDto } from './dtos/update-exam-answer.dto'; +import { + ExamAnswerPretestResponseDto, + PaginatedExamAnswerPretestResponseDto, +} from './dtos/exam-answer-pretest-response.dto'; + +@Controller('exam-answer') +@ApiTags('ExamAnswer') +@ApiBearerAuth() +@Injectable() +export class ExamAnswerController { + constructor(private readonly examAnswerService: ExamAnswerService) {} + + @Get() + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns all exam answers', + type: PaginatedExamAnswerResponseDto, + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by title', + }) + async findAll( + @Req() request: AuthenticatedRequest, + @Query() query: PaginateQueryDto, + ): Promise { + return await this.examAnswerService.findAll( + request.user.id, + request.user.role, + { + page: query.page, + limit: query.limit, + search: query.search, + }, + ); + } + + @Get('/pretest') + @Roles(Role.STUDENT, Role.ADMIN) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns all exam answers with pretest', + type: PaginatedExamAnswerPretestResponseDto, + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by title', + }) + async findAllExamAnswerPretest( + @Req() request: AuthenticatedRequest, + @Query() query: PaginateQueryDto, + ): Promise { + return await this.examAnswerService.findAllExamAnswerPretest( + request.user.id, + request.user.role, + { + page: query.page, + limit: query.limit, + search: query.search, + }, + ); + } + + @Get('/user') + @Roles(Role.STUDENT, Role.ADMIN) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns all exam answers by user id', + type: PaginatedExamAnswerResponseDto, + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by title', + }) + async findExamAnswerByUserId( + @Req() request: AuthenticatedRequest, + @Query() query: PaginateQueryDto, + ): Promise { + return await this.examAnswerService.findExamAnswerByUserId( + request.user.id, + request.user.role, + { + page: query.page, + limit: query.limit, + search: query.search, + }, + ); + } + + @Get(':id') + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns an exam answer', + type: ExamAnswerResponseDto, + }) + async findOne( + @Req() request: AuthenticatedRequest, + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + const examAnswer = await this.examAnswerService.findOne( + request.user.id, + request.user.role, + { + where: { id }, + }, + ); + return new ExamAnswerResponseDto(examAnswer); + } + + @Get('/pretest/user') + @Roles(Role.STUDENT, Role.ADMIN) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns all exam answers with pretest by user id', + type: PaginatedExamAnswerPretestResponseDto, + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by title', + }) + async findExamAnswerPretestByUserId( + @Req() request: AuthenticatedRequest, + @Query() query: PaginateQueryDto, + ): Promise { + return await this.examAnswerService.findExamAnswerPretestByUserId( + request.user.id, + request.user.role, + { + page: query.page, + limit: query.limit, + search: query.search, + }, + ); + } + + @Get('/pretest/:pretestId') + @Roles(Role.STUDENT, Role.ADMIN) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns all exam answers with pretest by pretest id', + type: PaginatedExamAnswerPretestResponseDto, + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by title', + }) + async findExamAnswerPretest( + @Req() request: AuthenticatedRequest, + @Query() query: PaginateQueryDto, + @Param( + 'pretestId', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + pretestId: string, + ): Promise { + return await this.examAnswerService.findExamAnswerPretestByPretestId( + request.user.id, + request.user.role, + pretestId, + { + page: query.page, + limit: query.limit, + search: query.search, + }, + ); + } + + @Get('question/:questionId') + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns all exam answer in question', + type: PaginatedExamAnswerResponseDto, + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by title', + }) + async findQuestionByExamId( + @Req() request: AuthenticatedRequest, + @Query() query: PaginateQueryDto, + @Param( + 'questionId', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + questionId: string, + ): Promise { + return await this.examAnswerService.findExamAnswerByQuestionId( + request.user.id, + request.user.role, + questionId, + { + page: query.page, + limit: query.limit, + search: query.search, + }, + ); + } + + @Get('selected-option/:selectedOptionId') + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns all exam answer in selectedOption', + type: PaginatedExamAnswerResponseDto, + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by title', + }) + async findQuestionByselectedOptionId( + @Req() request: AuthenticatedRequest, + @Query() query: PaginateQueryDto, + @Param( + 'selectedOptionId', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + selectedOptionId: string, + ): Promise { + return await this.examAnswerService.findExamAnswerBySelectedOptionId( + request.user.id, + request.user.role, + selectedOptionId, + { + page: query.page, + limit: query.limit, + search: query.search, + }, + ); + } + + @Post() + @Roles(Role.STUDENT) + @ApiResponse({ + status: HttpStatus.CREATED, + description: 'Create an exam answer', + type: ExamAnswerResponseDto, + }) + @HttpCode(HttpStatus.CREATED) + async createExamAnswer( + @Body() createExamAnswerDto: CreateExamAnswerDto, + ): Promise { + const examAnswer = await this.examAnswerService.createExamAnswer( + createExamAnswerDto, + ); + return new ExamAnswerResponseDto(examAnswer); + } + + @Patch(':id') + @Roles(Role.STUDENT) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Update an exam answer', + type: ExamAnswerResponseDto, + }) + async updateExamAnswer( + @Req() request: AuthenticatedRequest, + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + @Body() updateExamAnswerDto: UpdateExamAnswerDto, + ): Promise { + const examAnswer = await this.examAnswerService.updateExamAnswer( + request.user.id, + request.user.role, + id, + updateExamAnswerDto, + ); + return new ExamAnswerResponseDto(examAnswer); + } + + @Delete(':id') + @ApiResponse({ + status: HttpStatus.OK, + description: 'Delete an exam', + type: ExamAnswerResponseDto, + }) + async deleteExam( + @Req() request: AuthenticatedRequest, + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + const examAnswer = await this.examAnswerService.deleteExamAnswer( + request.user.id, + request.user.role, + id, + ); + return new ExamAnswerResponseDto(examAnswer); + } +} diff --git a/src/exam-answer/exam-answer.entity.ts b/src/exam-answer/exam-answer.entity.ts new file mode 100644 index 0000000..30b5dc6 --- /dev/null +++ b/src/exam-answer/exam-answer.entity.ts @@ -0,0 +1,95 @@ +import { ExamAttempt } from 'src/exam-attempt/exam-attempt.entity'; +import { QuestionOption } from 'src/question-option/question-option.entity'; +import { Question } from 'src/question/question.entity'; +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + Unique, + UpdateDateColumn, +} from 'typeorm'; + +@Entity() +@Unique(['selectedOptionId', 'questionId']) +export class ExamAnswer { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => ExamAttempt, (examAttempt) => examAttempt.examAnswer, { + onDelete: 'CASCADE', + nullable: true, + }) + @JoinColumn({ name: 'exam_attempt_id' }) + examAttempt: ExamAttempt; + + @Column({ name: 'exam_attempt_id', nullable: true }) + examAttemptId: string; + + @ManyToOne(() => Question, (question) => question.examAnswer, { + onDelete: 'CASCADE', + nullable: false, + }) + @JoinColumn({ name: 'question_id' }) + question: Question; + + @Column({ name: 'question_id' }) + questionId: string; + + @ManyToOne( + () => QuestionOption, + (questionOption) => questionOption.examAnswer, + { + onDelete: 'CASCADE', + nullable: false, + }, + ) + @JoinColumn({ name: 'question_option_id' }) + selectedOption: QuestionOption; + + @Column({ name: 'question_option_id' }) + selectedOptionId: string; + + @ManyToOne( + () => QuestionOption, + (questionOption) => questionOption.examAnswerCorrect, + { + onDelete: 'CASCADE', + nullable: false, + }, + ) + @JoinColumn({ name: 'correct_answer_id' }) + correctAnswer: QuestionOption; + + @Column({ name: 'correct_answer_id' }) + correctAnswerId: string; + + @Column({ + nullable: false, + }) + answerText: string; + + @Column({ + nullable: true, + type: Boolean, + }) + isCorrect: boolean; + + @Column({ + nullable: false, + default: 0, + }) + points: number = 0; + + @CreateDateColumn({ + type: 'timestamp with time zone', + }) + createdAt: Date; + + @UpdateDateColumn({ + type: 'timestamp with time zone', + }) + updatedAt: Date; +} diff --git a/src/exam-answer/exam-answer.module.ts b/src/exam-answer/exam-answer.module.ts new file mode 100644 index 0000000..d38a90d --- /dev/null +++ b/src/exam-answer/exam-answer.module.ts @@ -0,0 +1,31 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { DatabaseModule } from 'src/database/database.module'; +import { ExamAnswer } from './exam-answer.entity'; +import { ExamAttemptController } from 'src/exam-attempt/exam-attempt.controller'; +import { examAnswerProviders } from './exam-answer.providers'; +import { ExamAnswerService } from './exam-answer.service'; +import { ExamAnswerController } from './exam-answer.controller'; +import { ExamAttempt } from 'src/exam-attempt/exam-attempt.entity'; +import { Question } from 'src/question/question.entity'; +import { QuestionOption } from 'src/question-option/question-option.entity'; +import { User } from 'src/user/user.entity'; +import { Pretest } from 'src/pretest/pretest.entity'; + +@Module({ + imports: [ + DatabaseModule, + TypeOrmModule.forFeature([ + ExamAnswer, + ExamAttempt, + Question, + QuestionOption, + User, + Pretest, + ]), + ], + controllers: [ExamAnswerController], + providers: [...examAnswerProviders, ExamAnswerService], + exports: [ExamAnswerService], +}) +export class ExamAnswerModule {} diff --git a/src/exam-answer/exam-answer.providers.ts b/src/exam-answer/exam-answer.providers.ts new file mode 100644 index 0000000..a786078 --- /dev/null +++ b/src/exam-answer/exam-answer.providers.ts @@ -0,0 +1,11 @@ +import { DataSource } from 'typeorm'; +import { ExamAnswer } from './exam-answer.entity'; + +export const examAnswerProviders = [ + { + provide: 'ExamAnswerRepository', + useFactory: (dataSource: DataSource) => + dataSource.getRepository(ExamAnswer), + inject: ['DataSource'], + }, +]; diff --git a/src/exam-answer/exam-answer.service.ts b/src/exam-answer/exam-answer.service.ts new file mode 100644 index 0000000..e8087d3 --- /dev/null +++ b/src/exam-answer/exam-answer.service.ts @@ -0,0 +1,725 @@ +import { + Injectable, + Inject, + NotFoundException, + BadRequestException, + ConflictException, +} from '@nestjs/common'; +import { + FindOneOptions, + FindOptionsSelect, + FindOptionsWhere, + ILike, + Repository, +} from 'typeorm'; +import { ExamAnswer } from './exam-answer.entity'; +import { PaginatedExamAnswerResponseDto } from './dtos/exam-answer-response.dto'; +import { createPagination } from 'src/shared/pagination'; +import { CourseStatus, ExamStatus, Role } from 'src/shared/enums'; +import { CreateExamAnswerDto } from './dtos/create-exam-answer.dto'; +import { Question } from 'src/question/question.entity'; +import { ExamAttempt } from 'src/exam-attempt/exam-attempt.entity'; +import { QuestionOption } from 'src/question-option/question-option.entity'; +import { UpdateExamAnswerDto } from './dtos/update-exam-answer.dto'; +import { PaginatedExamAnswerPretestResponseDto } from './dtos/exam-answer-pretest-response.dto'; +import { User } from 'src/user/user.entity'; +import { Pretest } from 'src/pretest/pretest.entity'; + +@Injectable() +export class ExamAnswerService { + constructor( + @Inject('ExamAnswerRepository') + private readonly examAnswerRepository: Repository, + @Inject('ExamAttemptRepository') + private readonly examAttemptRepository: Repository, + @Inject('QuestionRepository') + private readonly questionRepository: Repository, + @Inject('QuestionOptionRepository') + private readonly questionOptionRepository: Repository, + @Inject('UserRepository') + private readonly userRepository: Repository, + @Inject('PretestRepository') + private readonly pretestRepository: Repository, + ) {} + + async findAll( + userId: string, + role: Role, + { + page = 1, + limit = 20, + search = '', + }: { + page?: number; + limit?: number; + search?: string; + }, + ): Promise { + const { find } = await createPagination(this.examAnswerRepository, { + page, + limit, + }); + + const whereCondition = this.validateAndCreateCondition( + userId, + role, + search, + ); + const exam = await find({ + where: whereCondition, + relations: [ + 'examAttempt', + 'question', + 'selectedOption', + 'examAttempt.user', + 'correctAnswer', + ], + select: { + examAttempt: this.selectPopulateExamAttempt(), + question: this.selectPopulateQuestion(), + selectedOption: this.selectPopulateSelectedOption(), + correctAnswer: this.selectPopulateSelectedOption(), + }, + }).run(); + + return exam; + } + + private validateAndCreateCondition( + userId: string, + role: Role, + search: string, + ): FindOptionsWhere | FindOptionsWhere[] { + const baseSearch = search ? { answerText: ILike(`%${search}%`) } : {}; + + if (role === Role.STUDENT) { + return [ + { + ...baseSearch, + examAttempt: { + userId, + }, + question: { + exam: { + status: ExamStatus.PUBLISHED, + courseModule: { + course: { + status: CourseStatus.PUBLISHED, + enrollments: { + user: { + id: userId, + }, + }, + }, + }, + }, + }, + }, + ]; + } + + if (role === Role.TEACHER) { + return [ + { + ...baseSearch, + question: { + exam: { + courseModule: { + course: { + teacher: { + id: userId, + }, + }, + }, + }, + }, + }, + ]; + } + + if (role === Role.ADMIN) { + return { ...baseSearch }; + } + + return [ + { + ...baseSearch, + examAttempt: { + userId, + }, + question: { + exam: { + status: ExamStatus.PUBLISHED, + courseModule: { + course: { + status: CourseStatus.PUBLISHED, + enrollments: { + user: { + id: userId, + }, + }, + }, + }, + }, + }, + }, + ]; + } + + async findOne( + userId: string, + role: Role, + options: FindOneOptions = {}, + ): Promise { + const whereCondition = this.validateAndCreateCondition(userId, role, ''); + + const where = Array.isArray(whereCondition) + ? [ + { ...whereCondition[0], ...options.where }, + { ...whereCondition[1], ...options.where }, + ] + : { ...whereCondition, ...options.where }; + + const examAnswer = await this.examAnswerRepository.findOne({ + ...options, + where, + relations: [ + 'examAttempt', + 'question', + 'selectedOption', + 'examAttempt.user', + 'correctAnswer', + ], + select: { + examAttempt: this.selectPopulateExamAttempt(), + question: this.selectPopulateQuestion(), + selectedOption: this.selectPopulateSelectedOption(), + correctAnswer: this.selectPopulateSelectedOption(), + }, + }); + + if (!examAnswer) { + throw new NotFoundException('Exam answer not found'); + } + + return examAnswer; + } + + async findExamAnswerByQuestionId( + userId: string, + role: Role, + questionId: string, + { + page = 1, + limit = 20, + search = '', + }: { + page?: number; + limit?: number; + search?: string; + }, + ): Promise { + const { find } = await createPagination(this.examAnswerRepository, { + page, + limit, + }); + + const whereCondition = this.validateAndCreateCondition( + userId, + role, + search, + ); + whereCondition['question'] = { id: questionId }; + + const question = await this.questionRepository.findOne({ + where: { id: questionId }, + }); + + if (!question) { + throw new NotFoundException('Question not found.'); + } + + return await find({ + where: whereCondition, + relations: [ + 'examAttempt', + 'question', + 'selectedOption', + 'examAttempt.user', + 'correctAnswer', + ], + select: { + examAttempt: this.selectPopulateExamAttempt(), + question: this.selectPopulateQuestion(), + selectedOption: this.selectPopulateSelectedOption(), + correctAnswer: this.selectPopulateSelectedOption(), + }, + }).run(); + } + + async findExamAnswerBySelectedOptionId( + userId: string, + role: Role, + selectedOptionId: string, + { + page = 1, + limit = 20, + search = '', + }: { + page?: number; + limit?: number; + search?: string; + }, + ): Promise { + const { find } = await createPagination(this.examAnswerRepository, { + page, + limit, + }); + + const whereCondition = this.validateAndCreateCondition( + userId, + role, + search, + ); + whereCondition['selectedOption'] = { id: selectedOptionId }; + + const selectedOption = await this.questionOptionRepository.findOne({ + where: { id: selectedOptionId }, + }); + + if (!selectedOption) { + throw new NotFoundException('Selected option not found.'); + } + + return await find({ + where: whereCondition, + relations: [ + 'examAttempt', + 'question', + 'selectedOption', + 'examAttempt.user', + 'correctAnswer', + ], + select: { + examAttempt: this.selectPopulateExamAttempt(), + question: this.selectPopulateQuestion(), + selectedOption: this.selectPopulateSelectedOption(), + correctAnswer: this.selectPopulateSelectedOption(), + }, + }).run(); + } + + async findExamAnswerByUserId( + userId: string, + role: Role, + { + page = 1, + limit = 20, + search = '', + }: { + page?: number; + limit?: number; + search?: string; + }, + ): Promise { + const { find } = await createPagination(this.examAnswerRepository, { + page, + limit, + }); + + const whereCondition = this.validateAndCreateConditionForePretest( + userId, + role, + search, + ); + whereCondition['examAttempt.user'] = { id: userId }; + + const user = await this.userRepository.findOne({ + where: { id: userId }, + }); + + if (!user) { + throw new NotFoundException('User not found.'); + } + + return await find({ + where: whereCondition, + relations: [ + 'examAttempt', + 'question', + 'selectedOption', + 'examAttempt.user', + 'correctAnswer', + ], + select: { + examAttempt: this.selectPopulateExamAttempt(), + question: this.selectPopulateQuestionPretest(), + selectedOption: this.selectPopulateSelectedOption(), + correctAnswer: this.selectPopulateSelectedOption(), + }, + }).run(); + } + + async createExamAnswer( + createExamAnswerDto: CreateExamAnswerDto, + ): Promise { + const selectedOption = await this.questionOptionRepository.findOne({ + where: { id: createExamAnswerDto.selectedOptionId }, + select: this.selectPopulateSelectedOption(), + }); + + if (!selectedOption) + throw new NotFoundException('Not Found SelectedOption'); + + const question = await this.questionRepository.findOne({ + where: { id: selectedOption.questionId }, + select: this.selectPopulateQuestion(), + }); + + if (!question) throw new NotFoundException('Not Found Question'); + + const correctAnswer = await this.questionOptionRepository.findOne({ + where: { questionId: selectedOption.questionId, isCorrect: true }, + select: this.selectPopulateSelectedOption(), + }); + + if (!correctAnswer) + throw new NotFoundException('Not found correct answer in this question'); + + const examAttempt = createExamAnswerDto.examAttemptId + ? await this.examAttemptRepository.findOne({ + where: { id: createExamAnswerDto.examAttemptId }, + select: this.selectPopulateExamAttempt(), + }) + : null; + + const examAnswer = await this.examAnswerRepository.create({ + ...createExamAnswerDto, + selectedOption, + question, + examAttempt, + correctAnswer, + correctAnswerId: correctAnswer.id, + }); + + if (!examAnswer) throw new NotFoundException("Can't create exam"); + try { + await this.examAnswerRepository.save(examAnswer); + return examAnswer; + } catch (error) { + if (error.code === '23505') { + throw new ConflictException( + 'An exam answer for this question and selected option already exists.', + ); + } + throw error; + } + } + + async updateExamAnswer( + userId: string, + role: Role, + id: string, + updateExamAnswerDto: UpdateExamAnswerDto, + ): Promise { + const examAnswerInData = await this.findOne(userId, role, { + where: { id }, + }); + if (!examAnswerInData) throw new NotFoundException('Exam answer not found'); + + let examAttempt = null; + if (updateExamAnswerDto.examAttemptId) { + examAttempt = await this.examAttemptRepository.findOne({ + where: { id: updateExamAnswerDto.examAttemptId }, + select: this.selectPopulateExamAttempt(), + }); + if (!examAttempt) { + throw new NotFoundException('Exam attempt not found'); + } + } + + const updateExamAnswer = { + ...updateExamAnswerDto, + ...(examAttempt ? { examAttemptId: examAttempt.id } : {}), + }; + + const examAnswer = await this.examAnswerRepository.update( + id, + updateExamAnswer, + ); + if (!examAnswer) throw new BadRequestException("Can't update exam answer"); + return await this.examAnswerRepository.findOne({ + where: { id }, + relations: [ + 'examAttempt', + 'question', + 'selectedOption', + 'examAttempt.user', + 'correctAnswer', + ], + select: { + examAttempt: this.selectPopulateExamAttempt(), + question: this.selectPopulateQuestion(), + selectedOption: this.selectPopulateSelectedOption(), + correctAnswer: this.selectPopulateSelectedOption(), + }, + }); + } + + async deleteExamAnswer( + userId: string, + role: Role, + id: string, + ): Promise { + try { + const examAnswer = await this.findOne(userId, role, { where: { id } }); + return await this.examAnswerRepository.remove(examAnswer); + } catch (error) { + if (error instanceof Error) + throw new NotFoundException('Exam answer not found'); + } + } + + private selectPopulateExamAttempt(): FindOptionsSelect { + return { + id: true, + score: true, + status: true, + startedAt: true, + submittedAt: true, + user: { + id: true, + }, + }; + } + + private selectPopulateQuestion(): FindOptionsSelect { + return { + id: true, + question: true, + type: true, + points: true, + orderIndex: true, + }; + } + + private selectPopulateSelectedOption(): FindOptionsSelect { + return { + id: true, + isCorrect: true, + explanation: true, + questionId: true, + optionText: true, + }; + } + + async findAllExamAnswerPretest( + userId: string, + role: Role, + { + page = 1, + limit = 20, + search = '', + }: { + page?: number; + limit?: number; + search?: string; + }, + ): Promise { + const { find } = await createPagination(this.examAnswerRepository, { + page, + limit, + }); + + const whereCondition = this.validateAndCreateConditionForePretest( + userId, + role, + search, + ); + const exam = await find({ + where: whereCondition, + relations: [ + 'examAttempt', + 'question', + 'selectedOption', + 'question.pretest', + 'question.pretest.user', + 'correctAnswer', + ], + select: { + examAttempt: this.selectPopulateExamAttempt(), + question: this.selectPopulateQuestionPretest(), + selectedOption: this.selectPopulateSelectedOption(), + correctAnswer: this.selectPopulateSelectedOption(), + }, + }).run(); + + return exam; + } + + private validateAndCreateConditionForePretest( + userId: string, + role: Role, + search: string, + ): FindOptionsWhere | FindOptionsWhere[] { + const baseSearch = search ? { answerText: ILike(`%${search}%`) } : {}; + + if (role === Role.STUDENT) { + return [ + { + ...baseSearch, + question: { + pretest: { + user: { + id: userId, + }, + }, + }, + }, + ]; + } + + if (role === Role.ADMIN) { + return { ...baseSearch }; + } + + return [ + { + ...baseSearch, + question: { + pretest: { + user: { + id: userId, + }, + }, + }, + }, + ]; + } + + async findExamAnswerPretestByPretestId( + userId: string, + role: Role, + pretestId: string, + { + page = 1, + limit = 20, + search = '', + }: { + page?: number; + limit?: number; + search?: string; + }, + ): Promise { + const { find } = await createPagination(this.examAnswerRepository, { + page, + limit, + }); + + const whereCondition = this.validateAndCreateConditionForePretest( + userId, + role, + search, + ); + whereCondition['question.pretest'] = { id: pretestId }; + + const pretest = await this.pretestRepository.findOne({ + where: { id: pretestId }, + }); + + if (!pretest) { + throw new NotFoundException('Pretest not found.'); + } + + return await find({ + where: whereCondition, + relations: [ + 'examAttempt', + 'question', + 'selectedOption', + 'question.pretest', + 'question.pretest.user', + 'correctAnswer', + ], + select: { + examAttempt: this.selectPopulateExamAttempt(), + question: this.selectPopulateQuestionPretest(), + selectedOption: this.selectPopulateSelectedOption(), + correctAnswer: this.selectPopulateSelectedOption(), + }, + }).run(); + } + + async findExamAnswerPretestByUserId( + userId: string, + role: Role, + { + page = 1, + limit = 20, + search = '', + }: { + page?: number; + limit?: number; + search?: string; + }, + ): Promise { + const { find } = await createPagination(this.examAnswerRepository, { + page, + limit, + }); + + const whereCondition = this.validateAndCreateConditionForePretest( + userId, + role, + search, + ); + whereCondition['question.pretest.user'] = { id: userId }; + + const user = await this.userRepository.findOne({ + where: { id: userId }, + }); + + if (!user) { + throw new NotFoundException('User not found.'); + } + + return await find({ + where: whereCondition, + relations: [ + 'examAttempt', + 'question', + 'selectedOption', + 'question.pretest', + 'question.pretest.user', + 'correctAnswer', + ], + select: { + examAttempt: this.selectPopulateExamAttempt(), + question: this.selectPopulateQuestionPretest(), + selectedOption: this.selectPopulateSelectedOption(), + correctAnswer: this.selectPopulateSelectedOption(), + }, + }).run(); + } + + private selectPopulateQuestionPretest(): FindOptionsSelect { + return { + id: true, + question: true, + type: true, + points: true, + orderIndex: true, + pretest: { + id: true, + title: true, + description: true, + timeLimit: true, + passingScore: true, + maxAttempts: true, + user: { + id: true, + }, + }, + }; + } +} diff --git a/src/exam-attempt/dtos/create-exam-attempt.dto.ts b/src/exam-attempt/dtos/create-exam-attempt.dto.ts new file mode 100644 index 0000000..1851e5c --- /dev/null +++ b/src/exam-attempt/dtos/create-exam-attempt.dto.ts @@ -0,0 +1,63 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsInt, IsNotEmpty, IsOptional, Min } from 'class-validator'; +import { ExamAttemptStatus } from 'src/shared/enums'; + +export class CreateExamAttemptDto { + @IsNotEmpty() + @ApiProperty({ + description: 'Exam ID', + type: String, + example: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + }) + examId: string; + + @IsOptional() + @Min(0) + @IsInt() + @ApiProperty({ + description: 'Score', + type: Number, + example: 0, + }) + score?: number; + + @IsNotEmpty() + @IsEnum(ExamAttemptStatus) + @ApiProperty({ + description: 'Exam attempt status', + type: String, + example: ExamAttemptStatus.IN_PROGRESS, + enum: ExamAttemptStatus, + }) + status: ExamAttemptStatus; +} + +export class CreateExamAttemptPretestDto { + @IsNotEmpty() + @ApiProperty({ + description: 'Pretest ID', + type: String, + example: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + }) + pretestId: string; + + @IsOptional() + @Min(0) + @IsInt() + @ApiProperty({ + description: 'Score', + type: Number, + example: 0, + }) + score?: number; + + @IsNotEmpty() + @IsEnum(ExamAttemptStatus) + @ApiProperty({ + description: 'Exam attempt status', + type: String, + example: ExamAttemptStatus.IN_PROGRESS, + enum: ExamAttemptStatus, + }) + status: ExamAttemptStatus; +} diff --git a/src/exam-attempt/dtos/exam-attempt-pretest.dto.ts b/src/exam-attempt/dtos/exam-attempt-pretest.dto.ts new file mode 100644 index 0000000..a44661c --- /dev/null +++ b/src/exam-attempt/dtos/exam-attempt-pretest.dto.ts @@ -0,0 +1,100 @@ +import { ExamAttemptStatus } from 'src/shared/enums'; +import { ApiProperty } from '@nestjs/swagger'; +import { PaginatedResponse } from 'src/shared/pagination/dtos/paginate-response.dto'; +import { UserResponseDto } from 'src/user/dtos/user-response.dto'; +import { ExamAttempt } from '../exam-attempt.entity'; +import { PretestResponseDto } from 'src/pretest/dtos/pretest-response.dto'; + +export class ExamAttemptPretestResponseDto { + @ApiProperty({ + description: 'Exam Attempy ID', + type: String, + example: '123e4567-e89b-12d3-a456-426614174000', + }) + id: string; + + @ApiProperty({ + description: 'Pretest Data', + type: PretestResponseDto, + example: { + id: '123e4567-e89b-12d3-a456-426614174000', + title: 'Exam title', + description: 'Exam description', + timeLimit: 20, + passingScore: 3, + maxAttempts: 1, + createdAt: '2024-11-26T11:10:00.257Z', + updatedAt: '2024-11-26T11:10:00.257Z', + }, + }) + pretest: PretestResponseDto; + + @ApiProperty({ + description: 'User Data', + type: String, + example: { + id: '389d1065-b898-4249-9d3d-e17e100336a7', + username: 'johndoe', + fullname: 'John Doe', + role: 'student', + email: 'johndoe@gmail.com', + profileKey: null, + }, + }) + user: UserResponseDto; + + @ApiProperty({ + description: 'Score', + type: String, + example: 0, + }) + score: number; + + @ApiProperty({ + description: 'Exam attempt status', + type: String, + example: ExamAttemptStatus.IN_PROGRESS, + enum: ExamAttemptStatus, + }) + status: ExamAttemptStatus; + + @ApiProperty({ + description: 'Exam attempt start at', + type: Date, + example: new Date(), + }) + startedAt: Date; + + @ApiProperty({ + description: 'Exam attempt submit at', + type: Date, + example: new Date(), + }) + submittedAt: Date; + + constructor(examAttempt: ExamAttempt) { + this.id = examAttempt.id; + this.pretest = examAttempt.pretest; + this.user = examAttempt.user; + this.score = examAttempt.score; + this.status = examAttempt.status; + this.startedAt = examAttempt.startedAt; + this.submittedAt = examAttempt.submittedAt; + } +} + +export class PaginatedExamAttemptPretestResponseDto extends PaginatedResponse( + ExamAttemptPretestResponseDto, +) { + constructor( + examAttempt: ExamAttempt[], + total: number, + pageSize: number, + currentPage: number, + ) { + const examAttemptDtos = examAttempt.map( + (examAttempt) => new ExamAttemptPretestResponseDto(examAttempt), + ); + super(examAttemptDtos, total, pageSize, currentPage); + } +} diff --git a/src/exam-attempt/dtos/exam-attempt-response.dto.ts b/src/exam-attempt/dtos/exam-attempt-response.dto.ts new file mode 100644 index 0000000..3f6a4f9 --- /dev/null +++ b/src/exam-attempt/dtos/exam-attempt-response.dto.ts @@ -0,0 +1,116 @@ +import { ExamAttemptStatus } from 'src/shared/enums'; +import { ApiProperty } from '@nestjs/swagger'; +import { PaginatedResponse } from 'src/shared/pagination/dtos/paginate-response.dto'; +import { ExamResponseDto } from 'src/exam/dtos/exam-response.dto'; +import { UserResponseDto } from 'src/user/dtos/user-response.dto'; +import { ExamAttempt } from '../exam-attempt.entity'; + +export class ExamAttemptResponseDto { + @ApiProperty({ + description: 'Exam Attempy ID', + type: String, + example: '123e4567-e89b-12d3-a456-426614174000', + }) + id: string; + + @ApiProperty({ + description: 'Exam Data', + type: String, + example: { + id: 'ce2fd59a-28ea-4192-bfc6-c2347450ab7e', + title: 'Biology', + description: 'This course is an introduction to biology', + timeLimit: 20, + passingScore: 50, + maxAttempts: 1, + shuffleQuestions: false, + status: 'archived', + }, + }) + exam: ExamResponseDto; + + @ApiProperty({ + description: 'User Data', + type: String, + example: { + id: '389d1065-b898-4249-9d3d-e17e100336a7', + username: 'johndoe', + fullname: 'John Doe', + role: 'student', + email: 'johndoe@gmail.com', + profileKey: null, + }, + }) + user: UserResponseDto; + + @ApiProperty({ + description: 'Score', + type: String, + example: 0, + }) + score: number; + + @ApiProperty({ + description: 'Exam attempt status', + type: String, + example: ExamAttemptStatus.IN_PROGRESS, + enum: ExamAttemptStatus, + }) + status: ExamAttemptStatus; + + @ApiProperty({ + description: 'Exam attempt start at', + type: Date, + example: new Date(), + }) + startedAt: Date; + + @ApiProperty({ + description: 'Exam attempt submit at', + type: Date, + example: new Date(), + }) + submittedAt: Date; + + @ApiProperty({ + description: 'Exam created date', + type: Date, + example: new Date(), + }) + createdAt: Date; + + @ApiProperty({ + description: 'Exam updated date', + type: Date, + example: new Date(), + }) + updatedAt: Date; + + constructor(examAttempt: ExamAttempt) { + this.id = examAttempt.id; + this.exam = examAttempt.exam; + this.user = examAttempt.user; + this.score = examAttempt.score; + this.status = examAttempt.status; + this.startedAt = examAttempt.startedAt; + this.submittedAt = examAttempt.submittedAt; + this.createdAt = examAttempt.createdAt; + this.updatedAt = examAttempt.updatedAt; + } +} + +export class PaginatedExamAttemptResponseDto extends PaginatedResponse( + ExamAttemptResponseDto, +) { + constructor( + examAttempt: ExamAttempt[], + total: number, + pageSize: number, + currentPage: number, + ) { + const examAttemptDtos = examAttempt.map( + (examAttempt) => new ExamAttemptResponseDto(examAttempt), + ); + super(examAttemptDtos, total, pageSize, currentPage); + } +} diff --git a/src/exam-attempt/dtos/update-exam-attempt.dto.ts b/src/exam-attempt/dtos/update-exam-attempt.dto.ts new file mode 100644 index 0000000..424bbdb --- /dev/null +++ b/src/exam-attempt/dtos/update-exam-attempt.dto.ts @@ -0,0 +1,13 @@ +import { OmitType, PartialType } from '@nestjs/swagger'; +import { + CreateExamAttemptDto, + CreateExamAttemptPretestDto, +} from './create-exam-attempt.dto'; + +export class UpdateExamAttemptDto extends PartialType( + OmitType(CreateExamAttemptDto, ['examId'] as const), +) {} + +export class UpdateExamAttemptPretestDto extends PartialType( + OmitType(CreateExamAttemptPretestDto, ['pretestId'] as const), +) {} diff --git a/src/exam-attempt/exam-attempt.controller.ts b/src/exam-attempt/exam-attempt.controller.ts new file mode 100644 index 0000000..ce9487d --- /dev/null +++ b/src/exam-attempt/exam-attempt.controller.ts @@ -0,0 +1,378 @@ +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Injectable, + Param, + ParseUUIDPipe, + Patch, + Post, + Query, + Req, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth, ApiResponse, ApiQuery } from '@nestjs/swagger'; +import { + ExamAttemptResponseDto, + PaginatedExamAttemptResponseDto, +} from './dtos/exam-attempt-response.dto'; +import { AuthenticatedRequest } from 'src/auth/interfaces/authenticated-request.interface'; +import { PaginateQueryDto } from 'src/shared/pagination/dtos/paginate-query.dto'; +import { ExamAttemptService } from './exam-attempt.service.dto'; +import { Roles } from 'src/shared/decorators/role.decorator'; +import { Role } from 'src/shared/enums'; +import { + CreateExamAttemptDto, + CreateExamAttemptPretestDto, +} from './dtos/create-exam-attempt.dto'; +import { + UpdateExamAttemptDto, + UpdateExamAttemptPretestDto, +} from './dtos/update-exam-attempt.dto'; +import { Exam } from 'src/exam/exam.entity'; +import { + ExamAttemptPretestResponseDto, + PaginatedExamAttemptPretestResponseDto, +} from './dtos/exam-attempt-pretest.dto'; + +@Controller('exam-attempt') +@ApiTags('ExamAttempt') +@ApiBearerAuth() +@Injectable() +export class ExamAttemptController { + constructor(private readonly examAttemptService: ExamAttemptService) {} + + @Get() + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns all exam-attempt', + type: PaginatedExamAttemptResponseDto, + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by title', + }) + async findAll( + @Req() request: AuthenticatedRequest, + @Query() query: PaginateQueryDto, + ): Promise { + return await this.examAttemptService.findAll( + request.user.id, + request.user.role, + { + page: query.page, + limit: query.limit, + search: query.search, + }, + ); + } + + @Get('/pretest') + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns all exam-attempt pretest', + type: PaginatedExamAttemptPretestResponseDto, + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by title', + }) + async findAllExamAttemptPretest( + @Req() request: AuthenticatedRequest, + @Query() query: PaginateQueryDto, + ): Promise { + return await this.examAttemptService.findAllExamAttemptPretest( + request.user.id, + request.user.role, + { + page: query.page, + limit: query.limit, + search: query.search, + }, + ); + } + + @Get(':id') + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns an exam-attempt', + type: ExamAttemptResponseDto, + }) + async findOne( + @Req() request: AuthenticatedRequest, + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + const exam = await this.examAttemptService.findOne( + request.user.id, + request.user.role, + { + where: { id }, + }, + ); + return new ExamAttemptResponseDto(exam); + } + + @Get('/pretest/:id') + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns an exam-attempt pretest', + type: ExamAttemptPretestResponseDto, + }) + async findOneExamAttemptPretest( + @Req() request: AuthenticatedRequest, + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + const exam = await this.examAttemptService.findOneExamAttemptPrestest( + request.user.id, + request.user.role, + { + where: { id }, + }, + ); + return new ExamAttemptPretestResponseDto(exam); + } + + @Post() + @Roles(Role.STUDENT) + @ApiResponse({ + status: HttpStatus.CREATED, + description: 'Create an exam-attempt', + type: ExamAttemptResponseDto, + }) + @HttpCode(HttpStatus.CREATED) + async createExamAttempt( + @Req() request: AuthenticatedRequest, + @Body() createExamAttemptDto: CreateExamAttemptDto, + ): Promise { + const exam = await this.examAttemptService.createExamAttempt( + request.user.id, + createExamAttemptDto, + ); + return new ExamAttemptResponseDto(exam); + } + + @Post('/pretest') + @Roles(Role.STUDENT) + @ApiResponse({ + status: HttpStatus.CREATED, + description: 'Create an exam-attempt pretest', + type: ExamAttemptPretestResponseDto, + }) + @HttpCode(HttpStatus.CREATED) + async createExamAttemptPretest( + @Req() request: AuthenticatedRequest, + @Body() createExamAttemptPretestDto: CreateExamAttemptPretestDto, + ): Promise { + const exam = await this.examAttemptService.createExamAttemptPretest( + request.user.id, + createExamAttemptPretestDto, + ); + return new ExamAttemptPretestResponseDto(exam); + } + + @Patch(':id') + @Roles(Role.STUDENT) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Update an exam-attempt', + type: ExamAttemptPretestResponseDto, + }) + async updateExamAttemptPretest( + @Req() request: AuthenticatedRequest, + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + @Body() updateExamAttemptPressDto: UpdateExamAttemptPretestDto, + ): Promise { + const exam = await this.examAttemptService.updateExamAttempt( + request.user.id, + request.user.role, + id, + updateExamAttemptPressDto, + ); + return new ExamAttemptPretestResponseDto(exam); + } + + @Patch('/pretest/:id') + @Roles(Role.STUDENT) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Update an exam-attempt ptetest', + type: ExamAttemptResponseDto, + }) + async updateExamAttempt( + @Req() request: AuthenticatedRequest, + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + @Body() updateExamAttemptDto: UpdateExamAttemptDto, + ): Promise { + const exam = await this.examAttemptService.updateExamAttemptPretest( + request.user.id, + request.user.role, + id, + updateExamAttemptDto, + ); + return new ExamAttemptPretestResponseDto(exam); + } + + @Patch('/submit/:id') + @Roles(Role.STUDENT) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Update an exam-attempt', + type: ExamAttemptResponseDto, + }) + async submitExamAttempt( + @Req() request: AuthenticatedRequest, + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + const exam = await this.examAttemptService.submittedExam( + request.user.id, + request.user.role, + id, + ); + return new ExamAttemptResponseDto(exam); + } + + @Patch('/pretest/submit/:id') + @Roles(Role.STUDENT) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Update an exam-attempt pretest', + type: ExamAttemptPretestResponseDto, + }) + async submitExamAttemptPretest( + @Req() request: AuthenticatedRequest, + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + const exam = await this.examAttemptService.submittedExamPretest( + request.user.id, + request.user.role, + id, + ); + return new ExamAttemptPretestResponseDto(exam); + } + + @Delete(':id') + @Roles(Role.TEACHER, Role.ADMIN) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Delete an exam', + type: ExamAttemptResponseDto, + }) + async deleteExamAttempt( + @Req() request: AuthenticatedRequest, + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + const examAttempt = await this.examAttemptService.deleteExamAttempt( + request.user.id, + request.user.role, + id, + ); + return new ExamAttemptResponseDto(examAttempt); + } + + @Delete('/pretest/:id') + @Roles(Role.ADMIN) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Delete an exam', + type: ExamAttemptPretestResponseDto, + }) + async deleteExamAttemptPretest( + @Req() request: AuthenticatedRequest, + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + const examAttempt = await this.examAttemptService.deleteExamAttemptPretest( + request.user.id, + request.user.role, + id, + ); + return new ExamAttemptPretestResponseDto(examAttempt); + } +} diff --git a/src/exam-attempt/exam-attempt.entity.ts b/src/exam-attempt/exam-attempt.entity.ts new file mode 100644 index 0000000..4fa8bcf --- /dev/null +++ b/src/exam-attempt/exam-attempt.entity.ts @@ -0,0 +1,92 @@ +import { ExamAnswer } from 'src/exam-answer/exam-answer.entity'; +import { Exam } from 'src/exam/exam.entity'; +import { Pretest } from 'src/pretest/pretest.entity'; +import { ExamAttemptStatus } from 'src/shared/enums'; +import { User } from 'src/user/user.entity'; +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, + Unique, + UpdateDateColumn, +} from 'typeorm'; + +@Entity() +export class ExamAttempt { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => Exam, (exam) => exam.examAttempt, { + onDelete: 'CASCADE', + nullable: true, + }) + @JoinColumn({ name: 'exam_id' }) + exam: Exam; + + @Column({ name: 'exam_id', nullable: true }) + examId: String; + + @ManyToOne(() => Pretest, (pretest) => pretest.examAttempt, { + onDelete: 'CASCADE', + nullable: true, + }) + @JoinColumn({ name: 'pretest_id' }) + pretest: Pretest; + + @Column({ name: 'pretest_id', nullable: true }) + pretestId: String; + + @ManyToOne(() => User, (user) => user.examAttempt, { + onDelete: 'CASCADE', + nullable: false, + }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @Column({ name: 'user_id' }) + userId: String; + + @OneToMany(() => ExamAnswer, (examAnswer) => examAnswer.examAttempt, { + cascade: true, + }) + examAnswer: ExamAnswer[]; + + @Column({ + nullable: false, + default: 0, + type: 'decimal', + }) + score: number = 0; + + @Column({ + enum: ExamAttemptStatus, + nullable: false, + }) + status: ExamAttemptStatus; + + @CreateDateColumn({ + type: 'timestamp with time zone', + nullable: false, + }) + startedAt: Date; + + @Column({ + type: 'timestamp with time zone', + nullable: true, + }) + submittedAt: Date; + + @CreateDateColumn({ + type: 'timestamp with time zone', + }) + createdAt: Date; + + @UpdateDateColumn({ + type: 'timestamp with time zone', + }) + updatedAt: Date; +} diff --git a/src/exam-attempt/exam-attempt.module.ts b/src/exam-attempt/exam-attempt.module.ts new file mode 100644 index 0000000..2c33fb3 --- /dev/null +++ b/src/exam-attempt/exam-attempt.module.ts @@ -0,0 +1,21 @@ +import { Module } from '@nestjs/common'; +import { DatabaseModule } from 'src/database/database.module'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ExamAttempt } from './exam-attempt.entity'; +import { ExamAttemptController } from './exam-attempt.controller'; +import { examAttemptProviders } from './exam-attempt.providers'; +import { ExamAttemptService } from './exam-attempt.service.dto'; +import { Exam } from 'src/exam/exam.entity'; +import { User } from 'src/user/user.entity'; +import { Pretest } from 'src/pretest/pretest.entity'; + +@Module({ + imports: [ + DatabaseModule, + TypeOrmModule.forFeature([ExamAttempt, Exam, User, Pretest]), + ], + controllers: [ExamAttemptController], + providers: [...examAttemptProviders, ExamAttemptService], + exports: [ExamAttemptService], +}) +export class ExamAttemptModule {} diff --git a/src/exam-attempt/exam-attempt.providers.ts b/src/exam-attempt/exam-attempt.providers.ts new file mode 100644 index 0000000..4567fc9 --- /dev/null +++ b/src/exam-attempt/exam-attempt.providers.ts @@ -0,0 +1,11 @@ +import { DataSource } from 'typeorm'; +import { ExamAttempt } from './exam-attempt.entity'; + +export const examAttemptProviders = [ + { + provide: 'ExamAttemptRepository', + useFactory: (dataSource: DataSource) => + dataSource.getRepository(ExamAttempt), + inject: ['DataSource'], + }, +]; diff --git a/src/exam-attempt/exam-attempt.service.dto.ts b/src/exam-attempt/exam-attempt.service.dto.ts new file mode 100644 index 0000000..20e362e --- /dev/null +++ b/src/exam-attempt/exam-attempt.service.dto.ts @@ -0,0 +1,578 @@ +import { + Injectable, + Inject, + NotFoundException, + ForbiddenException, + BadRequestException, +} from '@nestjs/common'; +import { + FindOneOptions, + FindOptionsSelect, + FindOptionsWhere, + ILike, + IsNull, + Not, + Repository, +} from 'typeorm'; +import { ExamAttempt } from './exam-attempt.entity'; +import { PaginatedExamAttemptResponseDto } from './dtos/exam-attempt-response.dto'; +import { createPagination } from 'src/shared/pagination'; +import { + CourseStatus, + ExamAttemptStatus, + ExamStatus, + Role, +} from 'src/shared/enums'; +import { + CreateExamAttemptDto, + CreateExamAttemptPretestDto, +} from './dtos/create-exam-attempt.dto'; +import { Exam } from 'src/exam/exam.entity'; +import { + UpdateExamAttemptDto, + UpdateExamAttemptPretestDto, +} from './dtos/update-exam-attempt.dto'; +import { User } from 'src/user/user.entity'; +import { Pretest } from 'src/pretest/pretest.entity'; +import { PaginatedExamAttemptPretestResponseDto } from './dtos/exam-attempt-pretest.dto'; + +@Injectable() +export class ExamAttemptService { + constructor( + @Inject('ExamAttemptRepository') + private readonly examAttemptRepository: Repository, + @Inject('ExamRepository') + private readonly examRepository: Repository, + @Inject('UserRepository') + private readonly userRepository: Repository, + @Inject('PretestRepository') + private readonly pretestRepository: Repository, + ) {} + + async findAll( + userId: string, + role: Role, + { + page = 1, + limit = 20, + search = '', + }: { + page?: number; + limit?: number; + search?: string; + }, + ): Promise { + const { find } = await createPagination(this.examAttemptRepository, { + page, + limit, + }); + + const whereCondition = this.validateAndCreateCondition( + userId, + role, + search, + ); + const exam = await find({ + where: whereCondition, + relations: ['exam', 'user'], + select: { + user: this.selectPopulateUser(), + exam: this.selectPopulateExam(), + }, + }).run(); + + return exam; + } + + private validateAndCreateCondition( + userId: string, + role: Role, + search: string, + ): FindOptionsWhere | FindOptionsWhere[] { + const baseSearch = search ? { id: ILike(`%${search}%`) } : {}; + + if (role === Role.ADMIN) { + return { ...baseSearch }; + } + + if (role === Role.STUDENT) { + return { + ...baseSearch, + userId: userId, + exam: { + status: ExamStatus.PUBLISHED, + courseModule: { + course: { + status: CourseStatus.PUBLISHED, + enrollments: { + user: { + id: userId, + }, + }, + }, + }, + }, + }; + } + + if (role === Role.TEACHER) { + return { + ...baseSearch, + exam: { + courseModule: { + course: { + teacher: { + id: userId, + }, + }, + }, + }, + }; + } + + return { + ...baseSearch, + userId: userId, + exam: { + status: ExamStatus.PUBLISHED, + courseModule: { + course: { + status: CourseStatus.PUBLISHED, + enrollments: { + user: { + id: userId, + }, + }, + }, + }, + }, + }; + } + + async findOne( + userId: string, + role: Role, + options: FindOneOptions = {}, + ): Promise { + const whereCondition = this.validateAndCreateCondition(userId, role, ''); + + const where = Array.isArray(whereCondition) + ? [ + { ...whereCondition[0], ...options.where }, + { ...whereCondition[1], ...options.where }, + ] + : { ...whereCondition, ...options.where }; + + const exam = await this.examAttemptRepository.findOne({ + ...options, + where, + relations: ['exam', 'user'], + select: { + user: this.selectPopulateUser(), + exam: this.selectPopulateExam(), + }, + }); + + if (!exam) { + throw new NotFoundException('Exam not found'); + } + + return exam; + } + + async createExamAttempt( + userId: string, + createExamAttemptDto: CreateExamAttemptDto, + ): Promise { + const exam = await this.examRepository.findOne({ + where: { id: createExamAttemptDto.examId }, + select: this.selectPopulateExam(), + }); + if (!exam) { + throw new NotFoundException('Exam not found.'); + } + const user = await this.userRepository.findOne({ + where: { id: userId }, + select: this.selectPopulateUser(), + }); + if (!user) { + throw new NotFoundException('User not found.'); + } + if (user.role != Role.STUDENT) + throw new ForbiddenException('User is not student.'); + if ( + (await this.countExamAttemptsWithExamId( + createExamAttemptDto.examId, + userId, + )) >= exam.maxAttempts + ) + throw new ForbiddenException( + "Can't create exam-attempt more than max attempt", + ); + const examAttempt = await this.examAttemptRepository.create({ + ...createExamAttemptDto, + exam, + user, + }); + if (!examAttempt) throw new NotFoundException("Can't create exam-attempt"); + await this.examAttemptRepository.save(examAttempt); + return examAttempt; + } + + async updateExamAttempt( + userId: string, + role: Role, + id: string, + updateExamAttemptDto: UpdateExamAttemptDto, + ): Promise { + const examAttemptInData = await this.findOne(userId, role, { + where: { id }, + }); + if (examAttemptInData.submittedAt) + throw new ForbiddenException('Already submitted'); + if ( + examAttemptInData.status != ExamAttemptStatus.IN_PROGRESS && + updateExamAttemptDto.status == ExamAttemptStatus.IN_PROGRESS + ) { + throw new ForbiddenException("Can't change status to in progress"); + } + const examAttempt = await this.examAttemptRepository.update( + id, + updateExamAttemptDto, + ); + if (!examAttempt) + throw new BadRequestException("Can't update exam-attempt"); + return await this.examAttemptRepository.findOne({ + where: { id }, + relations: ['exam', 'user'], + select: { + user: this.selectPopulateUser(), + exam: this.selectPopulateExam(), + }, + }); + } + + async deleteExamAttempt( + userId: string, + role: Role, + id: string, + ): Promise { + try { + const examAttempt = await this.findOne(userId, role, { where: { id } }); + return await this.examAttemptRepository.remove(examAttempt); + } catch (error) { + if (error instanceof Error) + throw new NotFoundException('Exam-attempt not found'); + } + } + + async submittedExam( + userId: string, + role: Role, + id: string, + ): Promise { + const examAttemptInData = await this.findOne(userId, role, { + where: { id }, + }); + + examAttemptInData.submittedAt = new Date(); + + await this.examAttemptRepository.update(id, examAttemptInData); + + const updatedExamAttempt = await this.findOne(userId, role, { + where: { id }, + }); + + return updatedExamAttempt; + } + + async countExamAttemptsWithExamId( + examId: string, + userId: string, + ): Promise { + const count = await this.examAttemptRepository + .createQueryBuilder('examAttempt') + .where('examAttempt.examId = :examId AND examAttempt.userId = :userId', { + examId, + userId, + }) + .getCount(); + + return count; + } + + private selectPopulateExam(): FindOptionsSelect { + return { + id: true, + title: true, + description: true, + timeLimit: true, + passingScore: true, + maxAttempts: true, + shuffleQuestions: true, + status: true, + }; + } + + private selectPopulateUser(): FindOptionsSelect { + return { + id: true, + username: true, + fullname: true, + role: true, + email: true, + profileKey: true, + }; + } + + async findAllExamAttemptPretest( + userId: string, + role: Role, + { + page = 1, + limit = 20, + search = '', + }: { + page?: number; + limit?: number; + search?: string; + }, + ): Promise { + const { find } = await createPagination(this.examAttemptRepository, { + page, + limit, + }); + + const whereCondition = this.validateAndCreateConditionForPretest( + userId, + role, + search, + ); + const exam = await find({ + where: whereCondition, + relations: ['pretest', 'user'], + select: { + user: this.selectPopulateUser(), + pretest: this.selectPopulatePretest(), + }, + }).run(); + + return exam; + } + + async findOneExamAttemptPrestest( + userId: string, + role: Role, + options: FindOneOptions = {}, + ): Promise { + const whereCondition = this.validateAndCreateConditionForPretest( + userId, + role, + '', + ); + + const where = Array.isArray(whereCondition) + ? [ + { ...whereCondition[0], ...options.where }, + { ...whereCondition[1], ...options.where }, + ] + : { ...whereCondition, ...options.where }; + + const exam = await this.examAttemptRepository.findOne({ + ...options, + where, + relations: ['pretest', 'user'], + select: { + user: this.selectPopulateUser(), + pretest: this.selectPopulatePretest(), + }, + }); + + if (!exam) { + throw new NotFoundException('Exam not found'); + } + + return exam; + } + + private validateAndCreateConditionForPretest( + userId: string, + role: Role, + search: string, + ): FindOptionsWhere | FindOptionsWhere[] { + const baseSearch = search ? { id: ILike(`%${search}%`) } : {}; + + if (role === Role.ADMIN) { + return { ...baseSearch }; + } + + if (role === Role.STUDENT) { + return { + ...baseSearch, + pretest: { + user: { + id: userId, + }, + }, + }; + } + + return { + ...baseSearch, + pretest: { + user: { + id: userId, + }, + }, + }; + } + + async createExamAttemptPretest( + userId: string, + createExamAttemptPretestDto: CreateExamAttemptPretestDto, + ): Promise { + const pretest = await this.pretestRepository.findOne({ + where: { id: createExamAttemptPretestDto.pretestId }, + select: this.selectPopulatePretest(), + }); + if (!pretest) { + throw new NotFoundException('Pretest not found.'); + } + const user = await this.userRepository.findOne({ + where: { id: userId }, + select: this.selectPopulateUser(), + }); + if (!user) { + throw new NotFoundException('User not found.'); + } + if (user.role != Role.STUDENT) + throw new ForbiddenException('User is not student.'); + if ( + (await this.countExamAttemptsWithPretestId( + createExamAttemptPretestDto.pretestId, + userId, + )) >= pretest.maxAttempts + ) + throw new ForbiddenException( + "Can't create exam-attempt more than max attempt", + ); + const examAttempt = await this.examAttemptRepository.create({ + ...createExamAttemptPretestDto, + pretest, + user, + }); + if (!examAttempt) throw new NotFoundException("Can't create exam-attempt"); + await this.examAttemptRepository.save(examAttempt); + return examAttempt; + } + + async updateExamAttemptPretest( + userId: string, + role: Role, + id: string, + updateExamAttemptPretestDto: UpdateExamAttemptPretestDto, + ): Promise { + const examAttemptInData = await this.findOneExamAttemptPrestest( + userId, + role, + { + where: { id }, + }, + ); + if (examAttemptInData.submittedAt) + throw new ForbiddenException('Already submitted'); + if ( + examAttemptInData.status != ExamAttemptStatus.IN_PROGRESS && + updateExamAttemptPretestDto.status == ExamAttemptStatus.IN_PROGRESS + ) { + throw new ForbiddenException("Can't change status to in progress"); + } + const examAttempt = await this.examAttemptRepository.update( + id, + updateExamAttemptPretestDto, + ); + if (!examAttempt) + throw new BadRequestException("Can't update exam-attempt"); + return await this.examAttemptRepository.findOne({ + where: { id }, + relations: ['pretest', 'user'], + select: { + user: this.selectPopulateUser(), + pretest: this.selectPopulatePretest(), + }, + }); + } + + async deleteExamAttemptPretest( + userId: string, + role: Role, + id: string, + ): Promise { + try { + const examAttempt = await this.findOneExamAttemptPrestest(userId, role, { + where: { id }, + }); + return await this.examAttemptRepository.remove(examAttempt); + } catch (error) { + if (error instanceof Error) + throw new NotFoundException('Exam-attempt not found'); + } + } + + async submittedExamPretest( + userId: string, + role: Role, + id: string, + ): Promise { + const examAttemptInData = await this.findOneExamAttemptPrestest( + userId, + role, + { + where: { id }, + }, + ); + + examAttemptInData.submittedAt = new Date(); + + await this.examAttemptRepository.update(id, examAttemptInData); + + const updatedExamAttempt = await this.findOneExamAttemptPrestest( + userId, + role, + { + where: { id }, + }, + ); + + return updatedExamAttempt; + } + + async countExamAttemptsWithPretestId( + pretestId: string, + userId: string, + ): Promise { + const count = await this.examAttemptRepository + .createQueryBuilder('examAttempt') + .where( + 'examAttempt.pretestId = :pretestId AND examAttempt.userId = :userId', + { + pretestId, + userId, + }, + ) + .getCount(); + + return count; + } + + private selectPopulatePretest(): FindOptionsSelect { + return { + id: true, + title: true, + description: true, + timeLimit: true, + passingScore: true, + maxAttempts: true, + }; + } +} diff --git a/src/exam/dtos/create-exam.dto.ts b/src/exam/dtos/create-exam.dto.ts new file mode 100644 index 0000000..08cf1af --- /dev/null +++ b/src/exam/dtos/create-exam.dto.ts @@ -0,0 +1,80 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsBoolean, + IsEnum, + IsInt, + IsNotEmpty, + IsOptional, +} from 'class-validator'; +import { ExamStatus } from 'src/shared/enums'; +export class CreateExamDto { + @IsNotEmpty() + @ApiProperty({ + description: 'Course Module ID', + type: String, + example: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + }) + courseModuleId: string; + + @IsNotEmpty() + @ApiProperty({ + description: 'Exam title', + type: String, + example: 'Biology', + }) + title: string; + + @IsOptional() + @ApiProperty({ + description: 'Exam description', + type: String, + example: 'This course is an introduction to biology', + }) + description?: string; + + @IsNotEmpty() + @IsInt() + @ApiProperty({ + description: 'time limit to do exam.', + type: Number, + example: 20, + }) + timeLimit: number; + + @IsNotEmpty() + @IsInt() + @ApiProperty({ + description: 'Score to pass exam.', + type: Number, + example: 50, + }) + passingScore: number; + + @IsNotEmpty() + @IsInt() + @ApiProperty({ + description: 'Max attempts to do exam.', + type: Number, + example: 1, + }) + maxAttempts: number; + + @IsOptional() + @IsBoolean() + @ApiProperty({ + description: 'Shuffle question.', + type: Boolean, + example: false, + }) + shuffleQuestions?: boolean; + + @IsOptional() + @IsEnum(ExamStatus) + @ApiProperty({ + description: 'Exam status', + type: String, + example: ExamStatus.DRAFT, + enum: ExamStatus, + }) + status?: ExamStatus; +} diff --git a/src/exam/dtos/exam-response.dto.ts b/src/exam/dtos/exam-response.dto.ts new file mode 100644 index 0000000..12cadc2 --- /dev/null +++ b/src/exam/dtos/exam-response.dto.ts @@ -0,0 +1,124 @@ +import { ExamStatus } from 'src/shared/enums'; +import { Exam } from '../exam.entity'; +import { ApiProperty } from '@nestjs/swagger'; +import { PaginatedResponse } from 'src/shared/pagination/dtos/paginate-response.dto'; +import { CourseModuleResponseDto } from 'src/course-module/dtos/course-module-response.dto'; + +export class ExamResponseDto { + @ApiProperty({ + description: 'Exam ID', + type: String, + example: '123e4567-e89b-12d3-a456-426614174000', + }) + id: string; + + @ApiProperty({ + description: 'Course Module Data', + type: String, + example: { + id: '7093a5ae-cc1d-4017-8445-cba7ea978b22', + title: 'Introduction to Biology', + description: 'This module is an introduction to biology', + orderIndex: 1, + course: { + id: 'b7634715-9536-46be-ae06-650dc0d719fb', + teacher: { + id: '75af7b82-d765-40a3-82aa-bc4f572c492c', + }, + }, + }, + }) + courseModule: CourseModuleResponseDto; + + @ApiProperty({ + description: 'Exam title', + type: String, + example: 'Exam title', + }) + title: string; + + @ApiProperty({ + description: 'Exam description', + type: String, + example: 'Exam description', + }) + description: string; + + @ApiProperty({ + description: 'Timelimit to do exam.', + type: Number, + example: 20, + }) + timeLimit: Number; + + @ApiProperty({ + description: 'Score to pass exam.', + type: Number, + example: 20, + }) + passingScore: Number; + + @ApiProperty({ + description: 'Max attempts to do exam.', + type: Number, + example: 1, + }) + maxAttempts: Number; + + @ApiProperty({ + description: 'Shuffle question', + type: Boolean, + example: false, + }) + shuffleQuestions: Boolean; + + @ApiProperty({ + description: 'Exam status', + type: String, + enum: ExamStatus, + example: ExamStatus.DRAFT, + }) + status: ExamStatus; + + @ApiProperty({ + description: 'Exam created date', + type: Date, + example: new Date(), + }) + createdAt: Date; + + @ApiProperty({ + description: 'Exam updated date', + type: Date, + example: new Date(), + }) + updatedAt: Date; + + constructor(exam: Exam) { + this.id = exam.id; + this.courseModule = exam.courseModule; + this.title = exam.title; + this.description = exam.description; + this.timeLimit = exam.timeLimit; + this.passingScore = exam.passingScore; + this.maxAttempts = exam.maxAttempts; + this.shuffleQuestions = exam.shuffleQuestions; + this.status = exam.status; + this.createdAt = exam.createdAt; + this.updatedAt = exam.updatedAt; + } +} + +export class PaginatedExamResponseDto extends PaginatedResponse( + ExamResponseDto, +) { + constructor( + exam: Exam[], + total: number, + pageSize: number, + currentPage: number, + ) { + const examDtos = exam.map((exam) => new ExamResponseDto(exam)); + super(examDtos, total, pageSize, currentPage); + } +} diff --git a/src/exam/dtos/update-exam.dto.ts b/src/exam/dtos/update-exam.dto.ts new file mode 100644 index 0000000..cd18912 --- /dev/null +++ b/src/exam/dtos/update-exam.dto.ts @@ -0,0 +1,6 @@ +import { OmitType, PartialType } from '@nestjs/swagger'; +import { CreateExamDto } from './create-exam.dto'; + +export class UpdateExamDto extends PartialType( + OmitType(CreateExamDto, ['courseModuleId'] as const), +) {} diff --git a/src/exam/exam.controller.ts b/src/exam/exam.controller.ts new file mode 100644 index 0000000..64f2d29 --- /dev/null +++ b/src/exam/exam.controller.ts @@ -0,0 +1,220 @@ +import { + Controller, + Injectable, + Get, + Param, + ParseUUIDPipe, + HttpStatus, + Post, + Body, + Patch, + Delete, + HttpCode, + Query, + Req, +} from '@nestjs/common'; +import { + ApiTags, + ApiBearerAuth, + ApiResponse, + ApiQuery, + ApiParam, +} from '@nestjs/swagger'; +import { ExamService } from './exam.service'; +import { + ExamResponseDto, + PaginatedExamResponseDto, +} from './dtos/exam-response.dto'; +import { Roles } from 'src/shared/decorators/role.decorator'; +import { Role } from 'src/shared/enums'; +import { CreateExamDto } from './dtos/create-exam.dto'; +import { PaginateQueryDto } from 'src/shared/pagination/dtos/paginate-query.dto'; +import { AuthenticatedRequest } from 'src/auth/interfaces/authenticated-request.interface'; +import { UpdateExamDto } from './dtos/update-exam.dto'; + +@Controller('exam') +@ApiTags('Exam') +@ApiBearerAuth() +@Injectable() +export class ExamController { + constructor(private readonly examService: ExamService) {} + + @Get() + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns all exams', + type: PaginatedExamResponseDto, + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by title', + }) + async findAll( + @Req() request: AuthenticatedRequest, + @Query() query: PaginateQueryDto, + ): Promise { + return await this.examService.findAll(request.user.id, request.user.role, { + page: query.page, + limit: query.limit, + search: query.search, + }); + } + + @Get(':id') + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns an exam', + type: ExamResponseDto, + }) + async findOne( + @Req() request: AuthenticatedRequest, + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + const exam = await this.examService.findOne( + request.user.id, + request.user.role, + { where: { id } }, + ); + return new ExamResponseDto(exam); + } + + @Get('course-module/:courseModuleId') + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns all exams by courseModule id', + type: PaginatedExamResponseDto, + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by title', + }) + async findExamByCourseModuleId( + @Req() request: AuthenticatedRequest, + @Query() query: PaginateQueryDto, + @Param( + 'courseModuleId', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + courseModuleId: string, + ): Promise { + return await this.examService.findExamByCourseModuleId( + request.user.id, + request.user.role, + courseModuleId, + { + page: query.page, + limit: query.limit, + search: query.search, + }, + ); + } + + @Post() + @Roles(Role.TEACHER) + @ApiResponse({ + status: HttpStatus.CREATED, + description: 'Create an exam', + type: ExamResponseDto, + }) + @HttpCode(HttpStatus.CREATED) + async createExam( + @Body() createExamDto: CreateExamDto, + ): Promise { + const exam = await this.examService.createExam(createExamDto); + return new ExamResponseDto(exam); + } + + @Patch(':id') + @Roles(Role.TEACHER) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Update an exam', + type: ExamResponseDto, + }) + async updateExam( + @Req() request: AuthenticatedRequest, + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + @Body() updateExamDto: UpdateExamDto, + ): Promise { + const exam = await this.examService.updateExam( + request.user.id, + request.user.role, + id, + updateExamDto, + ); + return new ExamResponseDto(exam); + } + + @Delete(':id') + @Roles(Role.TEACHER, Role.ADMIN) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Delete an exam', + type: ExamResponseDto, + }) + async deleteExam( + @Req() request: AuthenticatedRequest, + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + const exam = await this.examService.deleteExam( + request.user.id, + request.user.role, + id, + ); + return new ExamResponseDto(exam); + } +} diff --git a/src/exam/exam.entity.ts b/src/exam/exam.entity.ts new file mode 100644 index 0000000..c173b42 --- /dev/null +++ b/src/exam/exam.entity.ts @@ -0,0 +1,90 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, + OneToMany, + OneToOne, + JoinColumn, +} from 'typeorm'; +import { ExamStatus } from 'src/shared/enums'; +import { CourseModule } from 'src/course-module/course-module.entity'; +import { ExamAttempt } from 'src/exam-attempt/exam-attempt.entity'; +import { Question } from 'src/question/question.entity'; + +@Entity() +export class Exam { + @PrimaryGeneratedColumn('uuid') + id: string; + + @OneToOne(() => CourseModule, (courseModule) => courseModule.exam, { + onDelete: 'CASCADE', + nullable: false, + }) + @JoinColumn({ name: 'course_module_id' }) + courseModule: CourseModule; + + @Column({ name: 'course_module_id' }) + courseModuleId: string; + + @OneToMany(() => ExamAttempt, (examAttempt) => examAttempt.exam, { + cascade: true, + }) + examAttempt: ExamAttempt[]; + + @OneToMany(() => Question, (question) => question.exam, { + cascade: true, + }) + question: Question[]; + + @Column({ + nullable: false, + }) + title: string; + + @Column({ + nullable: true, + }) + description: string; + + @Column({ + nullable: false, + default: 20, + }) + timeLimit: number = 20; + + @Column({ + nullable: false, + }) + passingScore: number; + + @Column({ + nullable: false, + }) + maxAttempts: number; + + @Column({ + nullable: false, + default: false, + }) + shuffleQuestions: boolean = false; + + @Column({ + type: 'enum', + enum: ExamStatus, + nullable: false, + default: ExamStatus.DRAFT, + }) + status: ExamStatus = ExamStatus.DRAFT; + + @CreateDateColumn({ + type: 'timestamp with time zone', + }) + createdAt: Date; + + @UpdateDateColumn({ + type: 'timestamp with time zone', + }) + updatedAt: Date; +} diff --git a/src/exam/exam.module.ts b/src/exam/exam.module.ts new file mode 100644 index 0000000..39b0384 --- /dev/null +++ b/src/exam/exam.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { DatabaseModule } from 'src/database/database.module'; +import { ExamController } from './exam.controller'; +import { ExamService } from './exam.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Exam } from './exam.entity'; +import { examProviders } from './exam.providers'; +import { CourseModule } from 'src/course-module/course-module.entity'; + +@Module({ + imports: [DatabaseModule, TypeOrmModule.forFeature([Exam, CourseModule])], + controllers: [ExamController], + providers: [...examProviders, ExamService], + exports: [ExamService], +}) +export class ExamModule {} diff --git a/src/exam/exam.providers.ts b/src/exam/exam.providers.ts new file mode 100644 index 0000000..29b0433 --- /dev/null +++ b/src/exam/exam.providers.ts @@ -0,0 +1,10 @@ +import { DataSource } from 'typeorm'; +import { Exam } from './exam.entity'; + +export const examProviders = [ + { + provide: 'ExamRepository', + useFactory: (dataSource: DataSource) => dataSource.getRepository(Exam), + inject: ['DataSource'], + }, +]; diff --git a/src/exam/exam.service.ts b/src/exam/exam.service.ts new file mode 100644 index 0000000..f0dc13c --- /dev/null +++ b/src/exam/exam.service.ts @@ -0,0 +1,301 @@ +import { + Injectable, + Inject, + NotFoundException, + BadRequestException, + ForbiddenException, +} from '@nestjs/common'; +import { + Repository, + FindOneOptions, + ILike, + FindOptionsWhere, + FindOptionsSelect, +} from 'typeorm'; +import { Exam } from './exam.entity'; +import { CreateExamDto } from './dtos/create-exam.dto'; +import { PaginatedExamResponseDto } from './dtos/exam-response.dto'; +import { createPagination } from 'src/shared/pagination'; +import { CourseStatus, ExamStatus, Role } from 'src/shared/enums'; +import { UpdateExamDto } from './dtos/update-exam.dto'; +import { CourseModule } from 'src/course-module/course-module.entity'; + +@Injectable() +export class ExamService { + constructor( + @Inject('ExamRepository') + private readonly examRepository: Repository, + @Inject('CourseModuleRepository') + private readonly courseModuleRepository: Repository, + ) {} + + async findAll( + userId: string, + role: Role, + { + page = 1, + limit = 20, + search = '', + }: { + page?: number; + limit?: number; + search?: string; + }, + ): Promise { + const { find } = await createPagination(this.examRepository, { + page, + limit, + }); + + const whereCondition = this.validateAndCreateCondition( + userId, + role, + search, + ); + const exam = await find({ + where: whereCondition, + relations: [ + 'courseModule', + 'courseModule.course', + 'courseModule.course.teacher', + ], + select: { + courseModule: this.selectPopulateCourseModule(), + }, + }).run(); + + return new PaginatedExamResponseDto( + exam.data, + exam.meta.total, + exam.meta.pageSize, + exam.meta.currentPage, + ); + } + + private validateAndCreateCondition( + userId: string, + role: Role, + search: string, + ): FindOptionsWhere | FindOptionsWhere[] { + const baseSearch = search ? { title: ILike(`%${search}%`) } : {}; + + if (role === Role.STUDENT) { + return { + ...baseSearch, + status: ExamStatus.PUBLISHED, + courseModule: { + course: { + status: CourseStatus.PUBLISHED, + enrollments: { + user: { + id: userId, + }, + }, + }, + }, + }; + } + + if (role === Role.TEACHER) { + return [ + { + ...baseSearch, + courseModule: { + course: { + teacher: { + id: userId, + }, + }, + }, + }, + ]; + } + + if (role === Role.ADMIN) { + return { ...baseSearch }; + } + + return { + ...baseSearch, + status: ExamStatus.PUBLISHED, + courseModule: { + course: { + status: CourseStatus.PUBLISHED, + enrollments: { + user: { + id: userId, + }, + }, + }, + }, + }; + } + + async findOne( + userId: string, + role: Role, + options: FindOneOptions = {}, + ): Promise { + const whereCondition = this.validateAndCreateCondition(userId, role, ''); + + const where = Array.isArray(whereCondition) + ? [ + { ...whereCondition[0], ...options.where }, + { ...whereCondition[1], ...options.where }, + ] + : { ...whereCondition, ...options.where }; + + const exam = await this.examRepository.findOne({ + ...options, + where, + relations: [ + 'courseModule', + 'courseModule.course', + 'courseModule.course.teacher', + ], + select: { + courseModule: this.selectPopulateCourseModule(), + }, + }); + + if (!exam) { + throw new NotFoundException('Exam not found'); + } + + return exam; + } + + async findExamByCourseModuleId( + userId: string, + role: Role, + courseModuleId: string, + { + page = 1, + limit = 20, + search = '', + }: { + page?: number; + limit?: number; + search?: string; + }, + ): Promise { + const { find } = await createPagination(this.examRepository, { + page, + limit, + }); + + const whereCondition = this.validateAndCreateCondition( + userId, + role, + search, + ); + whereCondition['courseModule'] = { id: courseModuleId }; + + const courseModule = await this.courseModuleRepository.findOne({ + where: { id: courseModuleId }, + }); + + if (!courseModule) { + throw new NotFoundException('courseModule not found.'); + } + + return await find({ + where: whereCondition, + relations: [ + 'courseModule', + 'courseModule.course', + 'courseModule.course.teacher', + ], + select: { + courseModule: this.selectPopulateCourseModule(), + }, + }).run(); + } + + async createExam(createExamDto: CreateExamDto): Promise { + const courseModule = await this.courseModuleRepository.findOne({ + where: { id: createExamDto.courseModuleId }, + select: this.selectPopulateCourseModule(), + relations: ['course', 'course.teacher'], + }); + + if (!courseModule) throw new NotFoundException('Course Module not found'); + + const exam = await this.examRepository.create({ + ...createExamDto, + courseModule, + }); + + if (!exam) throw new BadRequestException("Can't create exam"); + await this.examRepository.save(exam); + return exam; + } + + async updateExam( + userId: string, + role: Role, + id: string, + updateExamDto: UpdateExamDto, + ): Promise { + const examInData = await this.findOne(userId, role, { where: { id } }); + if (this.checkPermission(userId, role, examInData) === false) + throw new ForbiddenException('Can not change this exam'); + if ( + examInData.status != ExamStatus.DRAFT && + updateExamDto.status == ExamStatus.DRAFT + ) { + throw new ForbiddenException("Can't change status to draft"); + } + + const exam = await this.examRepository.update(id, updateExamDto); + if (!exam) throw new BadRequestException("Can't update exam"); + return await this.examRepository.findOne({ + where: { id }, + relations: [ + 'courseModule', + 'courseModule.course', + 'courseModule.course.teacher', + ], + select: { + courseModule: this.selectPopulateCourseModule(), + }, + }); + } + + async deleteExam(userId: string, role: Role, id: string): Promise { + try { + const exam = await this.findOne(userId, role, { where: { id } }); + if (this.checkPermission(userId, role, exam) === false) + throw new ForbiddenException('Can not change this exam'); + return await this.examRepository.remove(exam); + } catch (error) { + if (error instanceof Error) throw new NotFoundException('Exam not found'); + } + } + + private selectPopulateCourseModule(): FindOptionsSelect { + return { + id: true, + title: true, + description: true, + orderIndex: true, + course: { + id: true, + teacher: { + id: true, + }, + }, + }; + } + + private checkPermission(userId: string, role: Role, exam: Exam): boolean { + switch (role) { + case Role.ADMIN: + return true; + case Role.TEACHER: + return exam.courseModule?.course?.teacher?.id === userId; + case Role.STUDENT: + return false; + } + } +} diff --git a/src/file/enums/folder.enum.ts b/src/file/enums/folder.enum.ts new file mode 100644 index 0000000..3299080 --- /dev/null +++ b/src/file/enums/folder.enum.ts @@ -0,0 +1,6 @@ +export enum Folder { + CHAPTER_VIDEOS = 'chapter-videos', + COURSE_THUMBNAILS = 'course-thumbnails', + PROFILES = 'profiles', + REWARD_THUMBNAILS = 'reward-thumbnails', +} diff --git a/src/file/file.module.ts b/src/file/file.module.ts new file mode 100644 index 0000000..0ec74bb --- /dev/null +++ b/src/file/file.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { FileService } from './file.service'; + +@Module({ + providers: [FileService], + exports: [FileService], +}) +export class FileModule {} diff --git a/src/file/file.service.ts b/src/file/file.service.ts new file mode 100644 index 0000000..714aafc --- /dev/null +++ b/src/file/file.service.ts @@ -0,0 +1,86 @@ +import { + Injectable, + InternalServerErrorException, + NotFoundException, +} from '@nestjs/common'; +import { + S3Client, + PutObjectCommand, + GetObjectCommand, + DeleteObjectCommand, + GetObjectCommandOutput, +} from '@aws-sdk/client-s3'; +import { ConfigService } from '@nestjs/config'; +import { GLOBAL_CONFIG } from 'src/shared/constants/global-config.constant'; +import { Folder } from './enums/folder.enum'; +import { NodeJsClient } from '@smithy/types'; + +@Injectable() +export class FileService { + constructor(private readonly configService: ConfigService) {} + + private readonly s3Client = new S3Client({ + region: this.configService.getOrThrow(GLOBAL_CONFIG.AWS_REGION), + }) as NodeJsClient; + + async upload( + folder: Folder, + key: string, + file: Express.Multer.File, + ): Promise { + try { + await this.s3Client.send( + new PutObjectCommand({ + Bucket: this.configService.getOrThrow( + GLOBAL_CONFIG.AWS_BUCKET_NAME, + ), + Key: `${folder}/${key}.${file.originalname.split('.').pop()}`, + Body: file.buffer, + }), + ); + } catch (error) { + if (error instanceof Error) + throw new InternalServerErrorException(error.message); + } + } + + async get(folder: Folder, key: string): Promise { + try { + const result: GetObjectCommandOutput = await this.s3Client.send( + new GetObjectCommand({ + Bucket: this.configService.getOrThrow( + GLOBAL_CONFIG.AWS_BUCKET_NAME, + ), + Key: `${folder}/${key}`, + }), + ); + return await result.Body.transformToByteArray(); + } catch (error) { + if (error instanceof Error) throw new NotFoundException(error.message); + } + } + + async delete(folder: Folder, key: string): Promise { + try { + await this.s3Client.send( + new DeleteObjectCommand({ + Bucket: this.configService.getOrThrow( + GLOBAL_CONFIG.AWS_BUCKET_NAME, + ), + Key: `${folder}/${key}`, + }), + ); + } catch (error) { + if (error instanceof Error) throw new NotFoundException(error.message); + } + } + + async update( + folder: Folder, + key: string, + file: Express.Multer.File, + ): Promise { + await this.delete(folder, key); + await this.upload(folder, key.split('.')[0], file); + } +} diff --git a/src/main.ts b/src/main.ts index f76bc8d..f3e748b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,49 @@ +import { ValidationPipe } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { NestFactory } from '@nestjs/core'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { createDatabase } from 'typeorm-extension'; import { AppModule } from './app.module'; +import { databaseConfig } from './shared/configs/database.config'; +import { GLOBAL_CONFIG } from './shared/constants/global-config.constant'; async function bootstrap() { + const configService = new ConfigService(); + + await createDatabase({ + options: databaseConfig, + }); + const app = await NestFactory.create(AppModule); - await app.listen(process.env.PORT ?? 3000); + + app.useGlobalPipes( + new ValidationPipe({ + transform: true, + transformOptions: { + enableImplicitConversion: true, + }, + whitelist: true, + forbidNonWhitelisted: true, + enableDebugMessages: true, + }), + ); + app.enableCors({ + origin: configService.get(GLOBAL_CONFIG.CORS_ALLOW_ORIGIN), + methods: ['GET', 'POST', 'PATCH', 'DELETE'], + optionsSuccessStatus: 200, + exposedHeaders: 'Authorization', + }); + + const config = new DocumentBuilder() + .addBearerAuth() + .setTitle('Edusaig API') + .setDescription('This is the Edusaig API documentation') + .setVersion('1.0') + .build(); + const documentFactory = () => SwaggerModule.createDocument(app, config); + SwaggerModule.setup('docs', app, documentFactory); + + await app.listen(configService.get(GLOBAL_CONFIG.PORT) ?? 3000); } + bootstrap(); diff --git a/src/pretest/dtos/create-pretest.dto.ts b/src/pretest/dtos/create-pretest.dto.ts new file mode 100644 index 0000000..daecd40 --- /dev/null +++ b/src/pretest/dtos/create-pretest.dto.ts @@ -0,0 +1,46 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsInt, IsNotEmpty, IsOptional } from 'class-validator'; +export class CreatePretestDto { + @IsNotEmpty() + @ApiProperty({ + description: 'Exam title', + type: String, + example: 'Biology', + }) + title: string; + + @IsOptional() + @ApiProperty({ + description: 'Exam description', + type: String, + example: 'This course is an introduction to biology', + }) + description?: string; + + @IsNotEmpty() + @IsInt() + @ApiProperty({ + description: 'time limit to do exam.', + type: Number, + example: 20, + }) + timeLimit: number; + + @IsNotEmpty() + @IsInt() + @ApiProperty({ + description: 'Score to pass exam.', + type: Number, + example: 3, + }) + passingScore: number; + + @IsNotEmpty() + @IsInt() + @ApiProperty({ + description: 'Max attempts to do exam.', + type: Number, + example: 1, + }) + maxAttempts: number; +} diff --git a/src/pretest/dtos/evaluate.dto.ts b/src/pretest/dtos/evaluate.dto.ts new file mode 100644 index 0000000..95f68da --- /dev/null +++ b/src/pretest/dtos/evaluate.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class EvaluateResponseDto { + @ApiProperty({ + description: 'The result of the evaluation', + type: String, + example: 'Passed', + }) + result: string; + + constructor(result: string) { + this.result = result; + } +} diff --git a/src/pretest/dtos/pretest-response.dto.ts b/src/pretest/dtos/pretest-response.dto.ts new file mode 100644 index 0000000..e8c620d --- /dev/null +++ b/src/pretest/dtos/pretest-response.dto.ts @@ -0,0 +1,104 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { UserResponseDto } from 'src/user/dtos/user-response.dto'; +import { Pretest } from '../pretest.entity'; +import { PaginatedResponse } from 'src/shared/pagination/dtos/paginate-response.dto'; + +export class PretestResponseDto { + @ApiProperty({ + description: 'Pretest ID', + type: String, + example: '123e4567-e89b-12d3-a456-426614174000', + }) + id: string; + + @ApiProperty({ + description: 'User Data', + type: String, + example: { + id: '389d1065-b898-4249-9d3d-e17e100336a7', + username: 'johndoe', + fullname: 'John Doe', + role: 'student', + email: 'johndoe@gmail.com', + profileKey: null, + }, + }) + user: UserResponseDto; + + @ApiProperty({ + description: 'Exam title', + type: String, + example: 'Exam title', + }) + title: string; + + @ApiProperty({ + description: 'Exam description', + type: String, + example: 'Exam description', + }) + description: string; + + @ApiProperty({ + description: 'Timelimit to do exam.', + type: Number, + example: 20, + }) + timeLimit: Number; + + @ApiProperty({ + description: 'Score to pass exam.', + type: Number, + example: 3, + }) + passingScore: Number; + + @ApiProperty({ + description: 'Max attempts to do exam.', + type: Number, + example: 1, + }) + maxAttempts: Number; + + @ApiProperty({ + description: 'Exam created date', + type: Date, + example: new Date(), + }) + createdAt: Date; + + @ApiProperty({ + description: 'Exam updated date', + type: Date, + example: new Date(), + }) + updatedAt: Date; + + constructor(pretest: Pretest) { + this.id = pretest.id; + this.user = pretest.user; + this.title = pretest.title; + this.description = pretest.description; + this.timeLimit = pretest.timeLimit; + this.passingScore = pretest.passingScore; + this.maxAttempts = pretest.maxAttempts; + this.createdAt = pretest.createdAt; + this.updatedAt = pretest.updatedAt; + } +} + +export class PaginatedPretestResponseDto extends PaginatedResponse( + PretestResponseDto, +) { + constructor( + pretest: Pretest[], + total: number, + pageSize: number, + currentPage: number, + ) { + const pretestDtos = pretest.map( + (pretest) => new PretestResponseDto(pretest), + ); + super(pretestDtos, total, pageSize, currentPage); + } +} diff --git a/src/pretest/dtos/pretest.dto.ts b/src/pretest/dtos/pretest.dto.ts new file mode 100644 index 0000000..265e8f0 --- /dev/null +++ b/src/pretest/dtos/pretest.dto.ts @@ -0,0 +1,79 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class AnswerAiDto { + @ApiProperty({ + description: 'A answer', + type: String, + example: 'import', + }) + a: string; + + @ApiProperty({ + description: 'B answer', + type: String, + example: 'hi', + }) + b: string; + + @ApiProperty({ + description: 'C answer', + type: String, + example: 'hello', + }) + c: string; + + @ApiProperty({ + description: 'D answer', + type: String, + example: 'delete', + }) + d: string; +} + +export class QuestionAiDto { + @ApiProperty({ + description: 'The question text', + type: String, + example: 'What is this?', + }) + question: string; + + @ApiProperty({ + description: 'Answer options for the question', + type: AnswerAiDto, + example: { + a: 'import', + b: 'hi', + c: 'hello', + d: 'delete', + }, + }) + choices: AnswerAiDto; + + @ApiProperty({ + description: 'The correct answer', + type: String, + example: 'c', + }) + answer: string; +} + +export class PretestDto { + @ApiProperty({ + description: 'All data', + type: [QuestionAiDto], + example: [ + { + question: 'What is this?', + choices: { + a: 'import', + b: 'hi', + c: 'hello', + d: 'delete', + }, + answer: 'c', + }, + ], + }) + data: QuestionAiDto[]; +} \ No newline at end of file diff --git a/src/pretest/dtos/update-pretest.dto.ts b/src/pretest/dtos/update-pretest.dto.ts new file mode 100644 index 0000000..3a72746 --- /dev/null +++ b/src/pretest/dtos/update-pretest.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreatePretestDto } from './create-pretest.dto'; + +export class UpdatePretestDto extends PartialType(CreatePretestDto) {} diff --git a/src/pretest/interfaces/create-evaluate.d.ts b/src/pretest/interfaces/create-evaluate.d.ts new file mode 100644 index 0000000..04bb241 --- /dev/null +++ b/src/pretest/interfaces/create-evaluate.d.ts @@ -0,0 +1,5 @@ +export interface CreateEvaluate { + question: string[]; + correct_answer: string[]; + user_answer: string[]; +} diff --git a/src/pretest/pretest.controller.ts b/src/pretest/pretest.controller.ts new file mode 100644 index 0000000..1aca4d0 --- /dev/null +++ b/src/pretest/pretest.controller.ts @@ -0,0 +1,196 @@ +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Injectable, + Param, + ParseUUIDPipe, + Patch, + Post, + Query, + Req, +} from '@nestjs/common'; +import { ApiBearerAuth, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { AuthenticatedRequest } from 'src/auth/interfaces/authenticated-request.interface'; +import { Roles } from 'src/shared/decorators/role.decorator'; +import { Role } from 'src/shared/enums'; +import { PaginateQueryDto } from 'src/shared/pagination/dtos/paginate-query.dto'; +import { CreatePretestDto } from './dtos/create-pretest.dto'; +import { EvaluateResponseDto } from './dtos/evaluate.dto'; +import { + PaginatedPretestResponseDto, + PretestResponseDto, +} from './dtos/pretest-response.dto'; +import { UpdatePretestDto } from './dtos/update-pretest.dto'; +import { PretestService } from './pretest.service'; +@Controller('pretest') +@ApiTags('Pretest') +@ApiBearerAuth() +@Injectable() +export class PretestController { + constructor(private readonly pretestService: PretestService) {} + @Get() + @Roles(Role.STUDENT, Role.ADMIN) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns all pretests', + type: PaginatedPretestResponseDto, + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by title', + }) + async findAll( + @Req() request: AuthenticatedRequest, + @Query() query: PaginateQueryDto, + ): Promise { + return await this.pretestService.findAll( + request.user.id, + request.user.role, + { + page: query.page, + limit: query.limit, + search: query.search, + }, + ); + } + + @Get(':id') + @Roles(Role.STUDENT, Role.ADMIN) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns a pretest', + type: PretestResponseDto, + }) + async findOne( + @Req() request: AuthenticatedRequest, + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + const pretest = await this.pretestService.findOne( + request.user.id, + request.user.role, + { where: { id } }, + ); + return new PretestResponseDto(pretest); + } + + @Post('/evaluate/:id') + @Roles(Role.STUDENT) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Evaluate a pretest', + type: EvaluateResponseDto, + }) + async evaluatePretest( + @Req() request: AuthenticatedRequest, + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ) { + const pretest = await this.pretestService.evaluatePretest(request.user, id); + return new EvaluateResponseDto(pretest); + } + + @Post() + @Roles(Role.STUDENT) + @ApiResponse({ + status: HttpStatus.CREATED, + description: 'Create a pretest', + type: PretestResponseDto, + }) + @HttpCode(HttpStatus.CREATED) + async createPretest( + @Req() request: AuthenticatedRequest, + @Body() createPretestDto: CreatePretestDto, + ): Promise { + const pretest = await this.pretestService.createPretest( + request.user.id, + createPretestDto, + ); + return new PretestResponseDto(pretest); + } + + @Patch(':id') + @Roles(Role.STUDENT) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Update a pretest', + type: PretestResponseDto, + }) + async updatePretest( + @Req() request: AuthenticatedRequest, + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + @Body() updatePretestDto: UpdatePretestDto, + ): Promise { + const pretest = await this.pretestService.updatePretest( + request.user.id, + request.user.role, + id, + updatePretestDto, + ); + return new PretestResponseDto(pretest); + } + + @Delete(':id') + @Roles(Role.STUDENT, Role.ADMIN) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Delete a pretest', + type: PretestResponseDto, + }) + async deleteExam( + @Req() request: AuthenticatedRequest, + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + const pretest = await this.pretestService.deletePretest( + request.user.id, + request.user.role, + id, + ); + return new PretestResponseDto(pretest); + } +} diff --git a/src/pretest/pretest.entity.ts b/src/pretest/pretest.entity.ts new file mode 100644 index 0000000..192e926 --- /dev/null +++ b/src/pretest/pretest.entity.ts @@ -0,0 +1,75 @@ +import { ExamAttempt } from 'src/exam-attempt/exam-attempt.entity'; +import { Question } from 'src/question/question.entity'; +import { User } from 'src/user/user.entity'; +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity() +export class Pretest { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => User, (user) => user.pretest, { + onDelete: 'CASCADE', + nullable: false, + }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @Column({ name: 'user_id' }) + userId: String; + + @OneToMany(() => ExamAttempt, (examAttempt) => examAttempt.pretest, { + cascade: true, + }) + examAttempt: ExamAttempt[]; + + @OneToMany(() => Question, (question) => question.pretest, { + cascade: true, + }) + question: Question[]; + + @Column({ + nullable: false, + }) + title: string; + + @Column({ + nullable: true, + }) + description: string; + + @Column({ + nullable: false, + default: 20, + }) + timeLimit: number = 20; + + @Column({ + nullable: false, + }) + passingScore: number; + + @Column({ + nullable: false, + }) + maxAttempts: number; + + @CreateDateColumn({ + type: 'timestamp with time zone', + }) + createdAt: Date; + + @UpdateDateColumn({ + type: 'timestamp with time zone', + }) + updatedAt: Date; +} diff --git a/src/pretest/pretest.module.ts b/src/pretest/pretest.module.ts new file mode 100644 index 0000000..cdd581c --- /dev/null +++ b/src/pretest/pretest.module.ts @@ -0,0 +1,32 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { DatabaseModule } from 'src/database/database.module'; +import { Pretest } from './pretest.entity'; +import { PretestController } from './pretest.controller'; +import { pretestProviders } from './pretest.providers'; +import { PretestService } from './pretest.service'; +import { User } from 'src/user/user.entity'; +import { UserModule } from 'src/user/user.module'; +import { UserBackgroundModule } from 'src/user-background/user-background.module'; +import { HttpModule } from '@nestjs/axios'; +import { ConfigModule } from '@nestjs/config'; +import { QuestionModule } from 'src/question/question.module'; +import { QuestionOptionModule } from 'src/question-option/question-option.module'; +import { ExamAnswerModule } from 'src/exam-answer/exam-answer.module'; + +@Module({ + imports: [ + DatabaseModule, + TypeOrmModule.forFeature([Pretest, User]), + UserBackgroundModule, + HttpModule, + ConfigModule, + QuestionModule, + QuestionOptionModule, + ExamAnswerModule, + ], + controllers: [PretestController], + providers: [...pretestProviders, PretestService], + exports: [PretestService], +}) +export class PretestModule {} diff --git a/src/pretest/pretest.providers.ts b/src/pretest/pretest.providers.ts new file mode 100644 index 0000000..9eb7021 --- /dev/null +++ b/src/pretest/pretest.providers.ts @@ -0,0 +1,10 @@ +import { DataSource } from 'typeorm'; +import { Pretest } from './pretest.entity'; + +export const pretestProviders = [ + { + provide: 'PretestRepository', + useFactory: (dataSource: DataSource) => dataSource.getRepository(Pretest), + inject: ['DataSource'], + }, +]; diff --git a/src/pretest/pretest.service.ts b/src/pretest/pretest.service.ts new file mode 100644 index 0000000..24c8682 --- /dev/null +++ b/src/pretest/pretest.service.ts @@ -0,0 +1,330 @@ +import { HttpService } from '@nestjs/axios'; +import { + BadRequestException, + Inject, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { JwtPayloadDto } from 'src/auth/dtos/jwt-payload.dto'; +import { ExamAnswerService } from 'src/exam-answer/exam-answer.service'; +import { QuestionOptionService } from 'src/question-option/question-option.service'; +import { QuestionService } from 'src/question/question.service'; +import { GLOBAL_CONFIG } from 'src/shared/constants/global-config.constant'; +import { QuestionType, Role } from 'src/shared/enums'; +import { createPagination } from 'src/shared/pagination'; +import { UserBackgroundService } from 'src/user-background/user-background.service'; +import { User } from 'src/user/user.entity'; +import { + FindOneOptions, + FindOptionsSelect, + FindOptionsWhere, + ILike, + Repository, +} from 'typeorm'; +import { CreatePretestDto } from './dtos/create-pretest.dto'; +import { PaginatedPretestResponseDto } from './dtos/pretest-response.dto'; +import { PretestDto } from './dtos/pretest.dto'; +import { UpdatePretestDto } from './dtos/update-pretest.dto'; +import { CreateEvaluate } from './interfaces/create-evaluate'; +import { Pretest } from './pretest.entity'; + +@Injectable() +export class PretestService { + constructor( + @Inject('PretestRepository') + private readonly pretestRepository: Repository, + @Inject('UserRepository') + private readonly userRepository: Repository, + private readonly userBackgroundService: UserBackgroundService, + private readonly httpService: HttpService, + private readonly configService: ConfigService, + private readonly questionService: QuestionService, + private readonly questionOptionService: QuestionOptionService, + private readonly examAnswerService: ExamAnswerService, + ) {} + async findAll( + userId: string, + role: Role, + { + page = 1, + limit = 20, + search = '', + }: { + page?: number; + limit?: number; + search?: string; + }, + ): Promise { + const { find } = await createPagination(this.pretestRepository, { + page, + limit, + }); + + const whereCondition = this.validateAndCreateCondition( + userId, + role, + search, + ); + const pretest = await find({ + where: whereCondition, + relations: ['user'], + select: { + user: this.selectPopulateUser(), + }, + }).run(); + + return new PaginatedPretestResponseDto( + pretest.data, + pretest.meta.total, + pretest.meta.pageSize, + pretest.meta.currentPage, + ); + } + + async findOne( + userId: string, + role: Role, + options: FindOneOptions = {}, + ): Promise { + const whereCondition = this.validateAndCreateCondition(userId, role, ''); + + const where = Array.isArray(whereCondition) + ? [ + { ...whereCondition[0], ...options.where }, + { ...whereCondition[1], ...options.where }, + ] + : { ...whereCondition, ...options.where }; + + const pretest = await this.pretestRepository.findOne({ + ...options, + where, + relations: ['user'], + select: { + user: this.selectPopulateUser(), + }, + }); + + if (!pretest) { + throw new NotFoundException('Pretest not found'); + } + + return pretest; + } + + private validateAndCreateCondition( + userId: string, + role: Role, + search: string, + ): FindOptionsWhere | FindOptionsWhere[] { + const baseSearch = search ? { title: ILike(`%${search}%`) } : {}; + + if (role === Role.ADMIN) { + return { + ...baseSearch, + }; + } + if (role === Role.STUDENT) { + return { + ...baseSearch, + user: { + id: userId, + }, + }; + } + return { + ...baseSearch, + user: { + id: userId, + }, + }; + } + + async createPretest( + userId: string, + createPretestDto: CreatePretestDto, + ): Promise { + const user = await this.userRepository.findOne({ + where: { id: userId }, + select: this.selectPopulateUser(), + }); + + if (!user) throw new NotFoundException('User not found'); + + const pretest = await this.pretestRepository.create({ + ...createPretestDto, + user, + }); + + if (!pretest) throw new BadRequestException("Can't create pretest"); + await this.pretestRepository.save(pretest); + await this.createQuestionAndChoice(pretest.id, userId); + return pretest; + } + + async updatePretest( + userId: string, + role: Role, + id: string, + updatePretestDto: UpdatePretestDto, + ): Promise { + const pretestInData = await this.findOne(userId, role, { where: { id } }); + + const pretest = await this.pretestRepository.update(id, updatePretestDto); + if (!pretest) throw new BadRequestException("Can't update pretest"); + return await this.pretestRepository.findOne({ + where: { id }, + relations: ['user'], + select: { + user: this.selectPopulateUser(), + }, + }); + } + + async deletePretest( + userId: string, + role: Role, + id: string, + ): Promise { + try { + const pretest = await this.findOne(userId, role, { where: { id } }); + return await this.pretestRepository.remove(pretest); + } catch (error) { + if (error instanceof Error) + throw new NotFoundException('Pretest not found'); + } + } + + private selectPopulateUser(): FindOptionsSelect { + return { + id: true, + username: true, + fullname: true, + role: true, + email: true, + profileKey: true, + }; + } + + async fetchEvaluation(requestBody: CreateEvaluate) { + const response = await this.httpService.axiosRef.post( + `https://ai.edusaig.com/ai/evaluate`, + requestBody, + ); + return { data: response.data }; + } + + async fetchData(userId: string, pretestId: string): Promise { + const userBackground = await this.userBackgroundService.findOneByUserId( + userId, + ); + try { + const requestBody = { + id: pretestId, + user: { + id: userBackground.user.id, + email: userBackground.user.email, + points: userBackground.user.points, + role: userBackground.user.role, + createdAt: userBackground.user.createdAt, + updatedAt: userBackground.user.updatedAt, + fullname: userBackground.user.fullname, + }, + occupation: { + id: userBackground.occupation.id, + title: userBackground.occupation.title, + description: userBackground.occupation.description, + createdAt: userBackground.occupation.createdAt, + updatedAt: userBackground.occupation.updatedAt, + }, + createdAt: new Date(), + updatedAt: new Date(), + topics: + userBackground.topics.length > 0 + ? userBackground.topics.map((topic) => ({ + id: topic.id, + title: topic.title, + description: topic.description, + level: topic.level, + createdAt: topic.createdAt, + updatedAt: topic.updatedAt, + })) + : [], + }; + const response = await this.httpService.axiosRef.post( + `${this.configService.get( + GLOBAL_CONFIG.AI_URL, + )}/ai/generate-pretest/`, + requestBody, + ); + return { data: response.data }; + } catch (error) { + throw new Error('Failed to fetch data or process request'); + } + } + + async evaluatePretest( + user: JwtPayloadDto, + pretestId: string, + ): Promise { + const preTestExam = + await this.examAnswerService.findExamAnswerPretestByPretestId( + user.id, + user.role, + pretestId, + { + page: 1, + limit: 10, + search: '', + }, + ); + const requestBody: CreateEvaluate = { + question: preTestExam.data.map((data) => data.question.question), + correct_answer: preTestExam.data.map( + (data) => data.correctAnswer.optionText, + ), + user_answer: preTestExam.data.map( + (data) => data.selectedOption.optionText, + ), + }; + + const response = await this.fetchEvaluation(requestBody); + + return response.data; + } + + async createQuestionAndChoice( + pretestId: string, + userId: string, + ): Promise { + const fetchData = await this.fetchData(userId, pretestId); + let orderIndex = 1; + await Promise.all( + fetchData.data.map(async (data) => { + const createQuestionDto = { + pretestId, + question: data.question, + type: QuestionType.PRETEST, + points: 1, + orderIndex: orderIndex++, + }; + const question = await this.questionService.createQuestionForPretest( + createQuestionDto, + ); + await Promise.all( + Object.entries(data.choices).map(([key, value]) => { + const createQuestionOptionDto = { + questionId: question.id, + optionText: `${value}`, + isCorrect: key === data.answer, + explanation: '', + }; + return this.questionOptionService.createQuestionOptionPretest( + createQuestionOptionDto, + ); + }), + ); + }), + ); + } +} diff --git a/src/progress/dtos/create-progress.dto.ts b/src/progress/dtos/create-progress.dto.ts new file mode 100644 index 0000000..35d67db --- /dev/null +++ b/src/progress/dtos/create-progress.dto.ts @@ -0,0 +1,60 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsEnum, + IsNotEmpty, + IsNumber, + IsOptional, + IsUUID, +} from 'class-validator'; +import { ProgressStatus } from '../enums/progress-status.enum'; + +export class CreateProgressDto { + @IsNotEmpty() + @IsUUID() + @ApiProperty({ + description: 'Enrollment ID', + type: String, + }) + enrollmentId: string; + + @IsNotEmpty() + @IsUUID() + @ApiProperty({ + description: 'Chapter ID', + type: String, + }) + chapterId: string; + + @IsOptional() + @IsEnum(ProgressStatus) + @ApiProperty({ + description: 'Progress status', + enum: ProgressStatus, + default: ProgressStatus.ACTIVE, + }) + status?: ProgressStatus; + + @IsOptional() + @IsNumber() + @ApiProperty({ + description: 'Watch time', + type: Number, + default: 0, + }) + watchTime?: number; + + @IsOptional() + @ApiProperty({ + description: 'Last accessed date', + type: Date, + default: new Date(), + }) + lastAccessedAt?: Date; + + @IsOptional() + @ApiProperty({ + description: 'Completion date', + type: Date, + }) + completedAt?: Date; +} diff --git a/src/progress/dtos/progress-response.dto.ts b/src/progress/dtos/progress-response.dto.ts new file mode 100644 index 0000000..2f9181a --- /dev/null +++ b/src/progress/dtos/progress-response.dto.ts @@ -0,0 +1,92 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { ChapterResponseDto } from 'src/chapter/dtos/chapter-response.dto'; +import { EnrollmentResponseDto } from 'src/enrollment/dtos/enrollment-response.dto'; +import { PaginatedResponse } from 'src/shared/pagination/dtos/paginate-response.dto'; +import { ProgressStatus } from '../enums/progress-status.enum'; +import { Progress } from '../progress.entity'; + +export class ProgressResponseDto { + @ApiProperty({ + description: 'Progress ID', + type: String, + }) + id: string; + + @ApiProperty({ + description: 'Enrollment data', + type: EnrollmentResponseDto, + }) + enrollment: EnrollmentResponseDto; + + @ApiProperty({ + description: 'Chapter data', + type: ChapterResponseDto, + }) + chapter: ChapterResponseDto; + + @ApiProperty({ + description: 'Progress status', + enum: ProgressStatus, + }) + status: ProgressStatus; + + @ApiProperty({ + description: 'Watch time', + type: Number, + }) + watchTime: number; + + @ApiProperty({ + description: 'Last accessed date', + type: Date, + }) + lastAccessedAt: Date; + + @ApiProperty({ + description: 'Completion date', + type: Date, + nullable: true, + }) + completedAt: Date; + + @ApiProperty({ + description: 'Created date', + type: Date, + }) + createdAt: Date; + + @ApiProperty({ + description: 'Updated date', + type: Date, + }) + updatedAt: Date; + + constructor(progress: Progress) { + this.id = progress.id; + this.enrollment = new EnrollmentResponseDto(progress.enrollment); + this.chapter = new ChapterResponseDto(progress.chapter); + this.status = progress.status; + this.watchTime = progress.watchTime; + this.lastAccessedAt = progress.lastAccessedAt; + this.completedAt = progress.completedAt; + this.createdAt = progress.createdAt; + } +} + +export class PaginatedProgressResponseDto extends PaginatedResponse( + ProgressResponseDto, +) { + constructor( + progress: Progress[], + total: number, + page: number, + limit: number, + ) { + super( + progress.map((progress) => new ProgressResponseDto(progress)), + total, + page, + limit, + ); + } +} diff --git a/src/progress/dtos/update-progress.dto.ts b/src/progress/dtos/update-progress.dto.ts new file mode 100644 index 0000000..88ee423 --- /dev/null +++ b/src/progress/dtos/update-progress.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateProgressDto } from './create-progress.dto'; + +export class UpdateProgressDto extends PartialType(CreateProgressDto) {} diff --git a/src/progress/enums/progress-status.enum.ts b/src/progress/enums/progress-status.enum.ts new file mode 100644 index 0000000..818e3cb --- /dev/null +++ b/src/progress/enums/progress-status.enum.ts @@ -0,0 +1,5 @@ +export enum ProgressStatus { + ACTIVE = 'active', + COMPLETED = 'completed', + DROPPED = 'dropped', +} diff --git a/src/progress/progress.controller.ts b/src/progress/progress.controller.ts new file mode 100644 index 0000000..4e5f35c --- /dev/null +++ b/src/progress/progress.controller.ts @@ -0,0 +1,151 @@ +import { + Body, + Controller, + Delete, + Get, + HttpStatus, + Injectable, + Param, + ParseUUIDPipe, + Patch, + Post, + Query, + Req, +} from '@nestjs/common'; +import { + ApiBearerAuth, + ApiParam, + ApiQuery, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; +import { AuthenticatedRequest } from 'src/auth/interfaces/authenticated-request.interface'; +import { Roles } from 'src/shared/decorators/role.decorator'; +import { Role } from 'src/shared/enums'; +import { PaginateQueryDto } from 'src/shared/pagination/dtos/paginate-query.dto'; +import { CreateProgressDto } from './dtos/create-progress.dto'; +import { + PaginatedProgressResponseDto, + ProgressResponseDto, +} from './dtos/progress-response.dto'; +import { UpdateProgressDto } from './dtos/update-progress.dto'; +import { ProgressService } from './progress.service'; + +@Controller('progress') +@ApiTags('Progress') +@ApiBearerAuth() +@Injectable() +export class ProgressController { + constructor(private readonly progressService: ProgressService) {} + + @Get() + @ApiResponse({ + status: HttpStatus.OK, + type: ProgressResponseDto, + description: 'Get all progress', + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by title', + }) + async findAll( + @Query() query: PaginateQueryDto, + ): Promise { + return this.progressService.findAll(query); + } + + @Get('/user') + @ApiResponse({ + status: HttpStatus.OK, + type: ProgressResponseDto, + description: 'Get all progress by user', + isArray: true, + }) + @Roles(Role.STUDENT) + async findAllByUser( + @Req() request: AuthenticatedRequest, + @Query() query: PaginateQueryDto, + ): Promise { + return this.progressService.findAllByUser(request.user.id, query); + } + + @Get(':id') + @ApiParam({ + name: 'id', + type: String, + description: 'Progress ID', + }) + async findOne( + @Param('id', ParseUUIDPipe) id: string, + ): Promise { + return this.progressService.findOne(id, { where: { id } }); + } + + @Post() + @Roles(Role.STUDENT) + @ApiResponse({ + status: HttpStatus.CREATED, + type: ProgressResponseDto, + description: 'Create a progress', + }) + async create( + @Body() createProgressDto: CreateProgressDto, + @Req() req: AuthenticatedRequest, + ): Promise { + return this.progressService.create(createProgressDto); + } + + @Patch(':id') + @Roles(Role.STUDENT) + @ApiParam({ + name: 'id', + type: String, + description: 'Progress ID', + }) + @ApiResponse({ + status: HttpStatus.OK, + type: ProgressResponseDto, + description: 'Update a progress', + }) + async update( + @Param('id', ParseUUIDPipe) id: string, + @Body() updateProgressDto: UpdateProgressDto, + @Req() req: AuthenticatedRequest, + ): Promise { + return this.progressService.update(id, updateProgressDto); + } + + @Delete(':id') + @Roles(Role.STUDENT) + @ApiParam({ + name: 'id', + type: String, + description: 'Progress ID', + }) + @ApiResponse({ + status: HttpStatus.OK, + type: ProgressResponseDto, + description: 'Delete a progress', + }) + async delete( + @Param('id', ParseUUIDPipe) id: string, + @Req() req: AuthenticatedRequest, + ): Promise { + return this.progressService.remove(id); + } +} diff --git a/src/progress/progress.entity.ts b/src/progress/progress.entity.ts new file mode 100644 index 0000000..ca1639e --- /dev/null +++ b/src/progress/progress.entity.ts @@ -0,0 +1,70 @@ +import { Chapter } from 'src/chapter/chapter.entity'; +import { Enrollment } from 'src/enrollment/enrollment.entity'; +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; +import { ProgressStatus } from './enums/progress-status.enum'; + +@Entity() +export class Progress { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => Enrollment, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'enrollment_id' }) + enrollment: Enrollment; + @Column({ name: 'enrollment_id' }) + enrollmentId: string; + + @ManyToOne(() => Chapter, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'chapter_id' }) + chapter: Chapter; + @Column({ name: 'chapter_id' }) + chapterId: string; + + @Column({ + type: 'enum', + enum: ProgressStatus, + default: ProgressStatus.ACTIVE, + }) + status: ProgressStatus; + + @Column({ + type: 'decimal', + precision: 5, + scale: 2, + default: 0, + }) + watchTime: number; + + @Column({ + type: 'timestamp', + name: 'last_accessed_at', + }) + lastAccessedAt: Date; + + @Column({ + type: 'timestamp', + name: 'completed_at', + nullable: true, + }) + completedAt: Date; + + @CreateDateColumn({ + type: 'timestamp', + name: 'created_at', + }) + createdAt: Date; + + @UpdateDateColumn({ + type: 'timestamp', + name: 'updated_at', + }) + updatedAt: Date; +} diff --git a/src/progress/progress.module.ts b/src/progress/progress.module.ts new file mode 100644 index 0000000..7dff459 --- /dev/null +++ b/src/progress/progress.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ProgressController } from './progress.controller'; +import { Progress } from './progress.entity'; +import { ProgressService } from './progress.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([Progress])], + controllers: [ProgressController], + providers: [ProgressService], +}) +export class ProgressModule {} diff --git a/src/progress/progress.service.ts b/src/progress/progress.service.ts new file mode 100644 index 0000000..a1fba3a --- /dev/null +++ b/src/progress/progress.service.ts @@ -0,0 +1,106 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { createPagination } from 'src/shared/pagination'; +import { PaginateQueryDto } from 'src/shared/pagination/dtos/paginate-query.dto'; +import { FindOneOptions, FindOptionsWhere, Repository } from 'typeorm'; +import { CreateProgressDto } from './dtos/create-progress.dto'; +import { PaginatedProgressResponseDto } from './dtos/progress-response.dto'; +import { UpdateProgressDto } from './dtos/update-progress.dto'; +import { Progress } from './progress.entity'; + +@Injectable() +export class ProgressService { + constructor( + @InjectRepository(Progress) + private readonly progressRepository: Repository, + ) {} + + async findAll({ + page = 1, + limit = 20, + }: { + page?: number; + limit?: number; + }): Promise { + const { find } = await createPagination(this.progressRepository, { + page, + limit, + }); + + const progress = await find({ + relations: { + enrollment: true, + chapter: true, + }, + }).run(); + + return progress; + } + + async findAllByUser( + userId: string, + query: PaginateQueryDto, + ): Promise { + const { find } = await createPagination(this.progressRepository, query); + + const progress = await find({ + where: { + enrollment: { + user: { + id: userId, + }, + }, + }, + relations: { + enrollment: true, + chapter: true, + }, + }).run(); + + return progress; + } + + async findOne( + id: string, + options: FindOneOptions, + ): Promise { + const baseWhere = options.where as FindOptionsWhere; + const whereCondition = { ...baseWhere, id }; + + const progress = await this.progressRepository.findOne({ + where: whereCondition, + relations: { + enrollment: true, + chapter: true, + }, + }); + + if (!progress) { + throw new NotFoundException('Progress not found'); + } + + return progress; + } + + async create(data: CreateProgressDto): Promise { + const progress = this.progressRepository.create(data); + await this.progressRepository.save(progress); + + return progress; + } + + async update(id: string, data: UpdateProgressDto): Promise { + const progress = await this.findOne(id, { where: { id } }); + Object.assign(progress, data); + await this.progressRepository.save(progress); + + return progress; + } + + async remove(id: string): Promise { + const progress = await this.findOne(id, { where: { id } }); + await this.progressRepository.remove(progress); + + return progress; + } +} diff --git a/src/question-option/dtos/create-question-option.dto.ts b/src/question-option/dtos/create-question-option.dto.ts new file mode 100644 index 0000000..81a958c --- /dev/null +++ b/src/question-option/dtos/create-question-option.dto.ts @@ -0,0 +1,36 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsBoolean, IsNotEmpty } from 'class-validator'; + +export class CreateQuestionOptionDto { + @IsNotEmpty() + @ApiProperty({ + description: 'Question ID', + type: String, + example: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + }) + questionId: string; + @IsNotEmpty() + @ApiProperty({ + description: 'Question option text', + type: String, + example: 'A. Rock', + }) + optionText: string; + + @IsNotEmpty() + @IsBoolean() + @ApiProperty({ + description: 'Is this question option correct?', + type: Boolean, + example: false, + }) + isCorrect: boolean; + + @IsNotEmpty() + @ApiProperty({ + description: 'ehy this question option correct or incorrect?', + type: String, + example: 'Rock is not biology.', + }) + explanation: string; +} diff --git a/src/question-option/dtos/question-option-pretest-response.dto.ts b/src/question-option/dtos/question-option-pretest-response.dto.ts new file mode 100644 index 0000000..15b31e4 --- /dev/null +++ b/src/question-option/dtos/question-option-pretest-response.dto.ts @@ -0,0 +1,106 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { QuestionOption } from '../question-option.entity'; +import { PaginatedResponse } from 'src/shared/pagination/dtos/paginate-response.dto'; +import { QuestionPretestResponseDto } from 'src/question/dtos/question-pretest-response.dto'; + +export class QuestionOptionPretestResponseDto { + @ApiProperty({ + description: 'Question option ID', + type: String, + example: '123e4567-e89b-12d3-a456-426614174000', + }) + id: string; + + @ApiProperty({ + description: 'Question Data', + type: QuestionPretestResponseDto, + example: { + points: 1, + orderIndex: 10, + id: 'e99e39a0-edf0-470c-b8f1-9fe8fddd0ef3', + examId: null, + pretestId: '168eb5b6-9ba9-4e6d-9880-551d4232129e', + question: + "What is the difference between 'int()', 'float()', and 'str()' functions in Python?", + type: 'pretest', + pretest: { + timeLimit: 20, + id: '168eb5b6-9ba9-4e6d-9880-551d4232129e', + title: 'Biology', + description: 'This course is an introduction to biology', + passingScore: 3, + maxAttempts: 1, + user: { + id: 'a12e1e37-3504-4711-a389-09a1734d7b1c', + username: 'johndoe', + fullname: 'John Doe', + role: 'student', + email: 'johndoe@gmail.com', + profileKey: null, + }, + }, + }, + }) + question: QuestionPretestResponseDto; + + @ApiProperty({ + description: 'Question option text', + type: String, + example: 'A. Rock', + }) + optionText: string; + + @ApiProperty({ + description: 'Is this question option correct?', + type: Boolean, + example: false, + }) + isCorrect: boolean; + + @ApiProperty({ + description: 'ehy this question option correct or incorrect?', + type: String, + example: 'Rock is not biology.', + }) + explanation: string; + + @ApiProperty({ + description: 'Exam created date', + type: Date, + example: new Date(), + }) + createdAt: Date; + + @ApiProperty({ + description: 'Exam updated date', + type: Date, + example: new Date(), + }) + updatedAt: Date; + + constructor(questionOption: QuestionOption) { + this.id = questionOption.id; + this.question = questionOption.question; + this.optionText = questionOption.optionText; + this.isCorrect = questionOption.isCorrect; + this.explanation = questionOption.explanation; + this.createdAt = questionOption.createdAt; + this.updatedAt = questionOption.updatedAt; + } +} + +export class PaginatedQuestionOptionPretestResponseDto extends PaginatedResponse( + QuestionOptionPretestResponseDto, +) { + constructor( + questionOption: QuestionOption[], + total: number, + pageSize: number, + currentPage: number, + ) { + const questionOptionDtos = questionOption.map( + (questionOption) => new QuestionOptionPretestResponseDto(questionOption), + ); + super(questionOptionDtos, total, pageSize, currentPage); + } +} diff --git a/src/question-option/dtos/question-option-response.dto.ts b/src/question-option/dtos/question-option-response.dto.ts new file mode 100644 index 0000000..3e742b9 --- /dev/null +++ b/src/question-option/dtos/question-option-response.dto.ts @@ -0,0 +1,94 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { QuestionOption } from '../question-option.entity'; +import { PaginatedResponse } from 'src/shared/pagination/dtos/paginate-response.dto'; +import { QuestionResponseDto } from 'src/question/dtos/question-response.dto'; + +export class QuestionOptionResponseDto { + @ApiProperty({ + description: 'Question option ID', + type: String, + example: '123e4567-e89b-12d3-a456-426614174000', + }) + id: string; + + @ApiProperty({ + description: 'Question Data', + type: QuestionResponseDto, + example: { + id: 'e20ffe51-2514-4f14-9bea-4bb28bb97fdd', + courseModuleId: '7093a5ae-cc1d-4017-8445-cba7ea978b22', + courseModule: { + id: '7093a5ae-cc1d-4017-8445-cba7ea978b22', + courseId: 'b7634715-9536-46be-ae06-650dc0d719fb', + course: { + id: 'b7634715-9536-46be-ae06-650dc0d719fb', + teacher: { + id: '75af7b82-d765-40a3-82aa-bc4f572c492c', + }, + }, + }, + }, + }) + question: QuestionResponseDto; + + @ApiProperty({ + description: 'Question option text', + type: String, + example: 'A. Rock', + }) + optionText: string; + + @ApiProperty({ + description: 'Is this question option correct?', + type: Boolean, + example: false, + }) + isCorrect: boolean; + + @ApiProperty({ + description: 'ehy this question option correct or incorrect?', + type: String, + example: 'Rock is not biology.', + }) + explanation: string; + + @ApiProperty({ + description: 'Exam created date', + type: Date, + example: new Date(), + }) + createdAt: Date; + + @ApiProperty({ + description: 'Exam updated date', + type: Date, + example: new Date(), + }) + updatedAt: Date; + + constructor(questionOption: QuestionOption) { + this.id = questionOption.id; + this.question = questionOption.question; + this.optionText = questionOption.optionText; + this.isCorrect = questionOption.isCorrect; + this.explanation = questionOption.explanation; + this.createdAt = questionOption.createdAt; + this.updatedAt = questionOption.updatedAt; + } +} + +export class PaginatedQuestionOptionResponseDto extends PaginatedResponse( + QuestionOptionResponseDto, +) { + constructor( + questionOption: QuestionOption[], + total: number, + pageSize: number, + currentPage: number, + ) { + const questionOptionDtos = questionOption.map( + (questionOption) => new QuestionOptionResponseDto(questionOption), + ); + super(questionOptionDtos, total, pageSize, currentPage); + } +} diff --git a/src/question-option/dtos/update-question-option.dto.ts b/src/question-option/dtos/update-question-option.dto.ts new file mode 100644 index 0000000..97d64a5 --- /dev/null +++ b/src/question-option/dtos/update-question-option.dto.ts @@ -0,0 +1,6 @@ +import { OmitType, PartialType } from '@nestjs/swagger'; +import { CreateQuestionOptionDto } from './create-question-option.dto'; + +export class UpdateQuestionOptionDto extends PartialType( + OmitType(CreateQuestionOptionDto, ['questionId'] as const), +) {} diff --git a/src/question-option/question-option.controller.ts b/src/question-option/question-option.controller.ts new file mode 100644 index 0000000..d04e3fe --- /dev/null +++ b/src/question-option/question-option.controller.ts @@ -0,0 +1,318 @@ +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Injectable, + Param, + ParseUUIDPipe, + Patch, + Post, + Query, + Req, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth, ApiResponse, ApiQuery } from '@nestjs/swagger'; +import { QuestionOptionService } from './question-option.service'; +import { + PaginatedQuestionOptionResponseDto, + QuestionOptionResponseDto, +} from './dtos/question-option-response.dto'; +import { AuthenticatedRequest } from 'src/auth/interfaces/authenticated-request.interface'; +import { PaginateQueryDto } from 'src/shared/pagination/dtos/paginate-query.dto'; +import { Roles } from 'src/shared/decorators/role.decorator'; +import { Role } from 'src/shared/enums'; +import { CreateQuestionOptionDto } from './dtos/create-question-option.dto'; +import { UpdateQuestionOptionDto } from './dtos/update-question-option.dto'; +import { PaginatedQuestionOptionPretestResponseDto } from './dtos/question-option-pretest-response.dto'; + +@Controller('question-option') +@Injectable() +@ApiTags('QuestionOption') +@ApiBearerAuth() +export class QuestionOptionController { + constructor(private readonly questionOptionService: QuestionOptionService) {} + + @Get() + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns all question options', + type: PaginatedQuestionOptionResponseDto, + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by title', + }) + async findAll( + @Req() request: AuthenticatedRequest, + @Query() query: PaginateQueryDto, + ): Promise { + return await this.questionOptionService.findAll( + request.user.id, + request.user.role, + { + page: query.page, + limit: query.limit, + search: query.search, + }, + ); + } + + @Get('/pretest') + @Roles(Role.STUDENT, Role.ADMIN) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns all question option pretest', + type: PaginatedQuestionOptionPretestResponseDto, + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by title', + }) + async findAllQuestionPretest( + @Req() request: AuthenticatedRequest, + @Query() query: PaginateQueryDto, + ): Promise { + return await this.questionOptionService.findAllQuestionOptionPretest( + request.user.id, + request.user.role, + { + page: query.page, + limit: query.limit, + search: query.search, + }, + ); + } + + @Get(':id') + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns a question option', + type: QuestionOptionResponseDto, + }) + async findOne( + @Req() request: AuthenticatedRequest, + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + const questionOption = await this.questionOptionService.findOne( + request.user.id, + request.user.role, + { + where: { id }, + }, + ); + return new QuestionOptionResponseDto(questionOption); + } + + @Get('pretest/:questionId') + @Roles(Role.STUDENT, Role.ADMIN) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns all question options in pretest id', + type: PaginatedQuestionOptionPretestResponseDto, + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by title', + }) + async findQuestionOptionPretestByQuestionId( + @Req() request: AuthenticatedRequest, + @Query() query: PaginateQueryDto, + @Param( + 'questionId', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + questionId: string, + ): Promise { + return await this.questionOptionService.findQuestionOptionPretestByQuestionId( + request.user.id, + request.user.role, + questionId, + { + page: query.page, + limit: query.limit, + search: query.search, + }, + ); + } + + @Get('question/:questionId') + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns all question options in question', + type: PaginatedQuestionOptionResponseDto, + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by title', + }) + async findQuestionByExamId( + @Req() request: AuthenticatedRequest, + @Query() query: PaginateQueryDto, + @Param( + 'questionId', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + questionId: string, + ): Promise { + return await this.questionOptionService.findQuestionOptionByQuestionId( + request.user.id, + request.user.role, + questionId, + { + page: query.page, + limit: query.limit, + search: query.search, + }, + ); + } + + @Post() + @Roles(Role.TEACHER) + @ApiResponse({ + status: HttpStatus.CREATED, + description: 'Create an question option', + type: QuestionOptionResponseDto, + }) + @HttpCode(HttpStatus.CREATED) + async createQuestionOption( + @Body() createQuestionOptionDto: CreateQuestionOptionDto, + ): Promise { + const questionOption = + await this.questionOptionService.createQuestionOption( + createQuestionOptionDto, + ); + return new QuestionOptionResponseDto(questionOption); + } + + @Patch(':id') + @Roles(Role.TEACHER) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Update a question option', + type: QuestionOptionResponseDto, + }) + async updateQuestionOption( + @Req() request: AuthenticatedRequest, + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + @Body() updateQuestionOptionDto: UpdateQuestionOptionDto, + ): Promise { + const questionOption = + await this.questionOptionService.updateQuestionOption( + request.user.id, + request.user.role, + id, + updateQuestionOptionDto, + ); + return new QuestionOptionResponseDto(questionOption); + } + + @Delete(':id') + @Roles(Role.TEACHER, Role.ADMIN) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Delete a question option', + type: QuestionOptionResponseDto, + }) + async deleteExam( + @Req() request: AuthenticatedRequest, + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + const questionOption = + await this.questionOptionService.deleteQuestionOption( + request.user.id, + request.user.role, + id, + ); + + return new QuestionOptionResponseDto(questionOption); + } +} diff --git a/src/question-option/question-option.entity.ts b/src/question-option/question-option.entity.ts new file mode 100644 index 0000000..971c13d --- /dev/null +++ b/src/question-option/question-option.entity.ts @@ -0,0 +1,63 @@ +import { ExamAnswer } from 'src/exam-answer/exam-answer.entity'; +import { Question } from 'src/question/question.entity'; +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity() +export class QuestionOption { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => Question, (question) => question.questionOption, { + onDelete: 'CASCADE', + nullable: false, + }) + @JoinColumn({ name: 'question_id' }) + question: Question; + + @Column({ name: 'question_id' }) + questionId: string; + + @OneToMany(() => ExamAnswer, (examAnswer) => examAnswer.selectedOption, { + cascade: true, + }) + examAnswer: ExamAnswer[]; + + @OneToMany(() => ExamAnswer, (examAnswer) => examAnswer.correctAnswer, { + cascade: true, + }) + examAnswerCorrect: ExamAnswer[]; + + @Column({ + nullable: false, + }) + optionText: string; + + @Column({ + nullable: false, + }) + isCorrect: boolean; + + @Column({ + nullable: false, + }) + explanation: string; + + @CreateDateColumn({ + type: 'timestamp with time zone', + }) + createdAt: Date; + + @UpdateDateColumn({ + type: 'timestamp with time zone', + }) + updatedAt: Date; +} diff --git a/src/question-option/question-option.module.ts b/src/question-option/question-option.module.ts new file mode 100644 index 0000000..e2b0422 --- /dev/null +++ b/src/question-option/question-option.module.ts @@ -0,0 +1,19 @@ +import { TypeOrmModule } from '@nestjs/typeorm'; +import { DatabaseModule } from 'src/database/database.module'; +import { QuestionOption } from './question-option.entity'; +import { Module } from '@nestjs/common'; +import { QuestionOptionController } from './question-option.controller'; +import { questionOptionProviders } from './question-option.providers'; +import { QuestionOptionService } from './question-option.service'; +import { Question } from 'src/question/question.entity'; + +@Module({ + imports: [ + DatabaseModule, + TypeOrmModule.forFeature([QuestionOption, Question]), + ], + controllers: [QuestionOptionController], + providers: [...questionOptionProviders, QuestionOptionService], + exports: [QuestionOptionService], +}) +export class QuestionOptionModule {} diff --git a/src/question-option/question-option.providers.ts b/src/question-option/question-option.providers.ts new file mode 100644 index 0000000..6c445b8 --- /dev/null +++ b/src/question-option/question-option.providers.ts @@ -0,0 +1,11 @@ +import { DataSource } from 'typeorm'; +import { QuestionOption } from './question-option.entity'; + +export const questionOptionProviders = [ + { + provide: 'QuestionOptionRepository', + useFactory: (dataSource: DataSource) => + dataSource.getRepository(QuestionOption), + inject: ['DataSource'], + }, +]; diff --git a/src/question-option/question-option.service.ts b/src/question-option/question-option.service.ts new file mode 100644 index 0000000..9573f92 --- /dev/null +++ b/src/question-option/question-option.service.ts @@ -0,0 +1,510 @@ +import { + Injectable, + Inject, + NotFoundException, + BadRequestException, + ForbiddenException, +} from '@nestjs/common'; +import { + FindOneOptions, + FindOptionsSelect, + FindOptionsWhere, + ILike, + Not, + Repository, +} from 'typeorm'; +import { QuestionOption } from './question-option.entity'; +import { AuthenticatedRequest } from 'src/auth/interfaces/authenticated-request.interface'; +import { PaginatedQuestionOptionResponseDto } from './dtos/question-option-response.dto'; +import { createPagination } from 'src/shared/pagination'; +import { CourseStatus, ExamStatus, QuestionType, Role } from 'src/shared/enums'; +import { CreateQuestionOptionDto } from './dtos/create-question-option.dto'; +import { Question } from 'src/question/question.entity'; +import { UpdateQuestionOptionDto } from './dtos/update-question-option.dto'; +import { PaginatedQuestionOptionPretestResponseDto } from './dtos/question-option-pretest-response.dto'; +@Injectable() +export class QuestionOptionService { + constructor( + @Inject('QuestionOptionRepository') + private readonly questionOptionRepository: Repository, + @Inject('QuestionRepository') + private readonly questionRepository: Repository, + ) {} + + async findAll( + userId: string, + role: Role, + { + page = 1, + limit = 20, + search = '', + }: { + page?: number; + limit?: number; + search?: string; + }, + ): Promise { + const { find } = await createPagination(this.questionOptionRepository, { + page, + limit, + }); + + const whereCondition = this.validateAndCreateCondition( + userId, + role, + search, + ); + const question = await find({ + where: whereCondition, + relations: [ + 'question', + 'question.exam', + 'question.exam.courseModule', + 'question.exam.courseModule.course', + 'question.exam.courseModule.course.teacher', + ], + select: { + question: this.selectPopulateQuestion(), + }, + }).run(); + + return question; + } + + async findOne( + userId: string, + role: Role, + options: FindOneOptions = {}, + ): Promise { + const whereCondition = this.validateAndCreateCondition(userId, role, ''); + + const where = Array.isArray(whereCondition) + ? [ + { ...whereCondition[0], ...options.where }, + { ...whereCondition[1], ...options.where }, + ] + : { ...whereCondition, ...options.where }; + + const question = await this.questionOptionRepository.findOne({ + ...options, + where, + relations: [ + 'question', + 'question.exam', + 'question.exam.courseModule', + 'question.exam.courseModule.course', + 'question.exam.courseModule.course.teacher', + ], + select: { + question: this.selectPopulateQuestion(), + }, + }); + + if (!question) { + throw new NotFoundException('Question not found'); + } + + return question; + } + + async findQuestionOptionByQuestionId( + userId: string, + role: Role, + questionId: string, + { + page = 1, + limit = 20, + search = '', + }: { + page?: number; + limit?: number; + search?: string; + }, + ): Promise { + const { find } = await createPagination(this.questionOptionRepository, { + page, + limit, + }); + + const whereCondition = this.validateAndCreateCondition( + userId, + role, + search, + ); + whereCondition['question'] = { + id: questionId, + type: Not(QuestionType.PRETEST), + }; + + const question = await this.questionRepository.findOne({ + where: { id: questionId }, + }); + + if (!question) { + throw new NotFoundException('Question not found.'); + } + + const questionOption = await find({ + where: whereCondition, + relations: [ + 'question', + 'question.exam', + 'question.exam.courseModule', + 'question.exam.courseModule.course', + 'question.exam.courseModule.course.teacher', + ], + select: { + question: this.selectPopulateQuestion(), + }, + }).run(); + return questionOption; + } + + private validateAndCreateCondition( + userId: string, + role: Role, + search: string, + ): FindOptionsWhere { + const baseSearch = search ? { optionText: ILike(`%${search}%`) } : {}; + + if (role === Role.STUDENT) { + return { + ...baseSearch, + question: { + exam: { + status: ExamStatus.PUBLISHED, + courseModule: { + course: { + status: CourseStatus.PUBLISHED, + enrollments: { + user: { + id: userId, + }, + }, + }, + }, + }, + }, + }; + } + + if (role === Role.TEACHER) { + return { + ...baseSearch, + question: { + exam: [ + { + courseModule: { + course: { + teacher: { id: userId }, + }, + }, + }, + ], + }, + }; + } + + if (role === Role.ADMIN) { + return { ...baseSearch }; + } + + return { + ...baseSearch, + question: { + exam: { + status: ExamStatus.PUBLISHED, + courseModule: { + course: { + status: CourseStatus.PUBLISHED, + enrollments: { + user: { + id: userId, + }, + }, + }, + }, + }, + }, + }; + } + + async createQuestionOption( + createQuestionOptionDto: CreateQuestionOptionDto, + ): Promise { + const question = await this.questionRepository.findOne({ + where: { id: createQuestionOptionDto.questionId }, + select: this.selectPopulateQuestion(), + relations: [ + 'exam', + 'exam.courseModule', + 'exam.courseModule.course', + 'exam.courseModule.course.teacher', + ], + }); + if (!question) { + throw new NotFoundException('Question option not found.'); + } + const questionOption = await this.questionOptionRepository.create({ + ...createQuestionOptionDto, + question, + }); + if (!questionOption) { + throw new BadRequestException('Question option not create.'); + } + await this.questionOptionRepository.save(questionOption); + return questionOption; + } + + async createQuestionOptionPretest( + createQuestionOptionDto: CreateQuestionOptionDto, + ): Promise { + const question = await this.questionRepository.findOne({ + where: { id: createQuestionOptionDto.questionId }, + select: this.selectPopulateQuestion(), + relations: ['pretest', 'pretest.user'], + }); + if (!question) { + throw new NotFoundException('Question option not found.'); + } + const questionOption = await this.questionOptionRepository.create({ + ...createQuestionOptionDto, + question, + }); + if (!questionOption) { + throw new BadRequestException('Question option not create.'); + } + await this.questionOptionRepository.save(questionOption); + return questionOption; + } + + async updateQuestionOption( + userId: string, + role: Role, + id: string, + updateQuestionOptionDto: UpdateQuestionOptionDto, + ): Promise { + const questionOptionInData = await this.findOne(userId, role, { + where: { id }, + }); + if (this.checkPermission(userId, role, questionOptionInData) === false) + throw new ForbiddenException('Can not change this question option'); + const questionOption = await this.questionOptionRepository.update( + id, + updateQuestionOptionDto, + ); + if (!questionOption) + throw new BadRequestException("Can't update question option"); + return await this.questionOptionRepository.findOne({ + where: { id }, + relations: [ + 'question', + 'question.exam', + 'question.exam.courseModule', + 'question.exam.courseModule.course', + 'question.exam.courseModule.course.teacher', + ], + select: { + question: this.selectPopulateQuestion(), + }, + }); + } + + async deleteQuestionOption( + userId: string, + role: Role, + id: string, + ): Promise { + try { + const questionOption = await this.findOne(userId, role, { + where: { id }, + }); + if (this.checkPermission(userId, role, questionOption) === false) + throw new ForbiddenException('Can not change this question option'); + return await this.questionOptionRepository.remove(questionOption); + } catch (error) { + if (error instanceof Error) + throw new NotFoundException('Question option not found'); + } + } + + private selectPopulateQuestion(): FindOptionsSelect { + return { + id: true, + question: true, + type: true, + points: true, + orderIndex: true, + exam: { + id: true, + courseModule: { + id: true, + course: { + id: true, + teacher: { + id: true, + }, + }, + }, + }, + }; + } + + private checkPermission( + userId: string, + role: Role, + questionOption: QuestionOption, + ): boolean { + switch (role) { + case Role.ADMIN: + return true; + case Role.TEACHER: + return ( + questionOption.question?.exam?.courseModule?.course?.teacher?.id === + userId + ); + case Role.STUDENT: + return false; + } + } + + async findAllQuestionOptionPretest( + userId: string, + role: Role, + { + page = 1, + limit = 20, + search = '', + }: { + page?: number; + limit?: number; + search?: string; + }, + ): Promise { + const { find } = await createPagination(this.questionOptionRepository, { + page, + limit, + }); + + const whereCondition = this.validateAndCreateConditionForPretest( + userId, + role, + search, + ); + const question = await find({ + where: whereCondition, + relations: ['question', 'question.pretest', 'question.pretest.user'], + select: { + question: this.selectPopulateQuestionForPretest(), + }, + }).run(); + + return question; + } + + async findQuestionOptionPretestByQuestionId( + userId: string, + role: Role, + questionId: string, + { + page = 1, + limit = 20, + search = '', + }: { + page?: number; + limit?: number; + search?: string; + }, + ): Promise { + const { find } = await createPagination(this.questionOptionRepository, { + page, + limit, + }); + + const whereCondition = this.validateAndCreateConditionForPretest( + userId, + role, + search, + ); + whereCondition['question'] = { id: questionId, type: QuestionType.PRETEST }; + + const question = await this.questionRepository.findOne({ + where: { id: questionId }, + }); + + if (!question) { + throw new NotFoundException('Question not found.'); + } + + const questionOption = await find({ + where: whereCondition, + relations: ['question', 'question.pretest', 'question.pretest.user'], + select: { + question: this.selectPopulateQuestionForPretest(), + }, + }).run(); + return questionOption; + } + + private validateAndCreateConditionForPretest( + userId: string, + role: Role, + search: string, + ): FindOptionsWhere { + const baseSearch = search ? { optionText: ILike(`%${search}%`) } : {}; + + if (role === Role.STUDENT) { + return { + ...baseSearch, + question: { + pretest: { + user: { + id: userId, + }, + }, + }, + }; + } + + if (role === Role.ADMIN) { + return { ...baseSearch }; + } + + return { + ...baseSearch, + question: { + pretest: { + user: { + id: userId, + }, + }, + }, + }; + } + + private selectPopulateQuestionForPretest(): FindOptionsSelect { + return { + id: true, + question: true, + type: true, + points: true, + orderIndex: true, + pretest: { + id: true, + timeLimit: true, + title: true, + description: true, + passingScore: true, + maxAttempts: true, + user: { + id: true, + username: true, + fullname: true, + role: true, + email: true, + profileKey: true, + }, + }, + }; + } +} diff --git a/src/question/dtos/create-question.dto.ts b/src/question/dtos/create-question.dto.ts new file mode 100644 index 0000000..125aae6 --- /dev/null +++ b/src/question/dtos/create-question.dto.ts @@ -0,0 +1,98 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsInt, IsNotEmpty, IsOptional, Min } from 'class-validator'; +import { QuestionType } from 'src/shared/enums'; +export class CreateQuestionDto { + @IsNotEmpty() + @ApiProperty({ + description: 'Exam ID', + type: String, + example: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + }) + examId: string; + + @IsNotEmpty() + @ApiProperty({ + description: 'Question exam', + type: String, + example: 'What is this?', + }) + question: string; + + @IsNotEmpty() + @IsEnum(QuestionType) + @ApiProperty({ + description: 'Type question', + type: String, + example: QuestionType.TRUE_FALSE, + enum: QuestionType, + }) + type: QuestionType; + + @IsNotEmpty() + @IsInt() + @Min(1) + @ApiProperty({ + description: 'Points in 1 question', + type: Number, + example: 1, + }) + points: number; + + @IsOptional() + @IsInt() + @Min(1) + @ApiProperty({ + description: 'Order question', + type: Number, + example: 1, + }) + orderIndex?: number; +} + +export class CreateQuestionPretestDto { + @IsNotEmpty() + @ApiProperty({ + description: 'Pretest ID', + type: String, + example: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + }) + pretestId: string; + + @IsNotEmpty() + @ApiProperty({ + description: 'Question exam', + type: String, + example: 'What is this?', + }) + question: string; + + @IsNotEmpty() + @IsEnum(QuestionType) + @ApiProperty({ + description: 'Type question', + type: String, + example: QuestionType.PRETEST, + enum: QuestionType, + }) + type: QuestionType; + + @IsNotEmpty() + @IsInt() + @Min(1) + @ApiProperty({ + description: 'Points in 1 question', + type: Number, + example: 1, + }) + points: number; + + @IsOptional() + @IsInt() + @Min(1) + @ApiProperty({ + description: 'Order question', + type: Number, + example: 1, + }) + orderIndex?: number; +} diff --git a/src/question/dtos/question-pretest-response.dto.ts b/src/question/dtos/question-pretest-response.dto.ts new file mode 100644 index 0000000..6ae6aa2 --- /dev/null +++ b/src/question/dtos/question-pretest-response.dto.ts @@ -0,0 +1,107 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { PaginatedResponse } from 'src/shared/pagination/dtos/paginate-response.dto'; +import { Question } from '../question.entity'; +import { QuestionType } from 'src/shared/enums'; +import { Pretest } from 'src/pretest/pretest.entity'; +import { PretestResponseDto } from 'src/pretest/dtos/pretest-response.dto'; + +export class QuestionPretestResponseDto { + @ApiProperty({ + description: 'Question ID', + type: String, + example: '123e4567-e89b-12d3-a456-426614174000', + }) + id: string; + + @ApiProperty({ + description: 'Pretest Data', + type: PretestResponseDto, + example: { + id: '80ff2ec3-7c6d-4427-a98d-58ac3aa68697', + user: { + id: 'a12e1e37-3504-4711-a389-09a1734d7b1c', + username: 'johndoe', + fullname: 'John Doe', + role: 'student', + email: 'johndoe@gmail.com', + profileKey: null, + }, + title: 'Biology', + description: 'This course is an introduction to biology', + timeLimit: 20, + passingScore: 3, + maxAttempts: 1, + }, + }) + pretest: Pretest; + + @ApiProperty({ + description: 'Pretest question', + type: String, + example: 'What is this?', + }) + question: string; + + @ApiProperty({ + description: 'Type question pretest', + type: String, + example: QuestionType.PRETEST, + enum: QuestionType, + }) + type: QuestionType; + + @ApiProperty({ + description: 'Points in 1 question', + type: Number, + example: 0, + }) + points: Number; + + @ApiProperty({ + description: 'Index in exam.', + type: Number, + example: 1, + }) + orderIndex: Number; + + @ApiProperty({ + description: 'Exam created date', + type: Date, + example: new Date(), + }) + createdAt: Date; + + @ApiProperty({ + description: 'Exam updated date', + type: Date, + example: new Date(), + }) + updatedAt: Date; + + constructor(question: Question) { + this.id = question.id; + this.pretest = question.pretest; + this.question = question.question; + this.type = question.type; + this.points = question.points; + this.orderIndex = question.orderIndex; + this.createdAt = question.createdAt; + this.updatedAt = question.updatedAt; + } +} + +export class PaginatedQuestionPretestResponseDto extends PaginatedResponse( + QuestionPretestResponseDto, +) { + constructor( + question: Question[], + total: number, + pageSize: number, + currentPage: number, + ) { + const questionDtos = question.map( + (question) => new QuestionPretestResponseDto(question), + ); + super(questionDtos, total, pageSize, currentPage); + } +} diff --git a/src/question/dtos/question-response.dto.ts b/src/question/dtos/question-response.dto.ts new file mode 100644 index 0000000..d79fd9e --- /dev/null +++ b/src/question/dtos/question-response.dto.ts @@ -0,0 +1,110 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { PaginatedResponse } from 'src/shared/pagination/dtos/paginate-response.dto'; +import { Question } from '../question.entity'; +import { Exam } from 'src/exam/exam.entity'; +import { ExamResponseDto } from 'src/exam/dtos/exam-response.dto'; +import { QuestionType } from 'src/shared/enums'; + +export class QuestionResponseDto { + @ApiProperty({ + description: 'Question ID', + type: String, + example: '123e4567-e89b-12d3-a456-426614174000', + }) + id: string; + + @ApiProperty({ + description: 'Exam Data', + type: ExamResponseDto, + example: { + id: 'e20ffe51-2514-4f14-9bea-4bb28bb97fdd', + title: 'Biology', + description: 'This course is an introduction to biology', + timeLimit: 20, + passingScore: 50, + maxAttempts: 1, + shuffleQuestions: false, + status: 'published', + courseModule: { + id: '7093a5ae-cc1d-4017-8445-cba7ea978b22', + course: { + id: 'b7634715-9536-46be-ae06-650dc0d719fb', + teacher: { + id: '75af7b82-d765-40a3-82aa-bc4f572c492c', + }, + }, + }, + }, + }) + exam: Exam; + + @ApiProperty({ + description: 'Exam question', + type: String, + example: 'What is this?', + }) + question: string; + + @ApiProperty({ + description: 'Type question', + type: String, + example: QuestionType.TRUE_FALSE, + enum: QuestionType, + }) + type: QuestionType; + + @ApiProperty({ + description: 'Points in 1 question', + type: Number, + example: 0, + }) + points: Number; + + @ApiProperty({ + description: 'Index in exam.', + type: Number, + example: 1, + }) + orderIndex: Number; + + @ApiProperty({ + description: 'Exam created date', + type: Date, + example: new Date(), + }) + createdAt: Date; + + @ApiProperty({ + description: 'Exam updated date', + type: Date, + example: new Date(), + }) + updatedAt: Date; + + constructor(question: Question) { + this.id = question.id; + this.exam = question.exam; + this.question = question.question; + this.type = question.type; + this.points = question.points; + this.orderIndex = question.orderIndex; + this.createdAt = question.createdAt; + this.updatedAt = question.updatedAt; + } +} + +export class PaginatedQuestionResponseDto extends PaginatedResponse( + QuestionResponseDto, +) { + constructor( + question: Question[], + total: number, + pageSize: number, + currentPage: number, + ) { + const questionDtos = question.map( + (question) => new QuestionResponseDto(question), + ); + super(questionDtos, total, pageSize, currentPage); + } +} diff --git a/src/question/dtos/update-question.dto.ts b/src/question/dtos/update-question.dto.ts new file mode 100644 index 0000000..41ee21f --- /dev/null +++ b/src/question/dtos/update-question.dto.ts @@ -0,0 +1,13 @@ +import { OmitType, PartialType } from '@nestjs/swagger'; +import { + CreateQuestionDto, + CreateQuestionPretestDto, +} from './create-question.dto'; + +export class UpdateQuestionDto extends PartialType( + OmitType(CreateQuestionDto, ['examId'] as const), +) {} + +export class UpdateQuestionPretestDto extends PartialType( + OmitType(CreateQuestionPretestDto, ['pretestId'] as const), +) {} diff --git a/src/question/question.controller.ts b/src/question/question.controller.ts new file mode 100644 index 0000000..1da9c85 --- /dev/null +++ b/src/question/question.controller.ts @@ -0,0 +1,316 @@ +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Injectable, + Param, + ParseUUIDPipe, + Patch, + Post, + Query, + Req, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth, ApiResponse, ApiQuery } from '@nestjs/swagger'; +import { QuestionService } from './question.service'; +import { + PaginatedQuestionResponseDto, + QuestionResponseDto, +} from './dtos/question-response.dto'; +import { AuthenticatedRequest } from 'src/auth/interfaces/authenticated-request.interface'; +import { PaginateQueryDto } from 'src/shared/pagination/dtos/paginate-query.dto'; +import { Roles } from 'src/shared/decorators/role.decorator'; +import { Role } from 'src/shared/enums'; +import { CreateQuestionDto } from './dtos/create-question.dto'; +import { UpdateQuestionDto } from './dtos/update-question.dto'; +import { PaginatedQuestionPretestResponseDto } from './dtos/question-pretest-response.dto'; + +@Controller('question') +@Injectable() +@ApiTags('Question') +@ApiBearerAuth() +export class QuestionController { + constructor(private readonly questionService: QuestionService) {} + + @Get() + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns all questions', + type: PaginatedQuestionResponseDto, + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by title', + }) + async findAll( + @Req() request: AuthenticatedRequest, + @Query() query: PaginateQueryDto, + ): Promise { + return await this.questionService.findAll( + request.user.id, + request.user.role, + { + page: query.page, + limit: query.limit, + search: query.search, + }, + ); + } + + @Get('/pretest') + @Roles(Role.STUDENT, Role.ADMIN) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns all questions pretest', + type: PaginatedQuestionPretestResponseDto, + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by title', + }) + async findAllQuestionPretest( + @Req() request: AuthenticatedRequest, + @Query() query: PaginateQueryDto, + ): Promise { + return await this.questionService.findAllQuestionPretest( + request.user.id, + request.user.role, + { + page: query.page, + limit: query.limit, + search: query.search, + }, + ); + } + + @Get(':id') + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns an question', + type: QuestionResponseDto, + }) + async findOne( + @Req() request: AuthenticatedRequest, + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + const question = await this.questionService.findOne( + request.user.id, + request.user.role, + { + where: { id }, + }, + ); + return new QuestionResponseDto(question); + } + + @Get('pretest/:pretestId') + @Roles(Role.STUDENT, Role.ADMIN) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns all questions pretest with pretest id', + type: PaginatedQuestionPretestResponseDto, + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by title', + }) + async findQuestionPretestByPretestId( + @Req() request: AuthenticatedRequest, + @Query() query: PaginateQueryDto, + @Param( + 'pretestId', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + pretestId: string, + ): Promise { + const question = await this.questionService.findQuestionPretestByPretestId( + request.user.id, + request.user.role, + pretestId, + { + page: query.page, + limit: query.limit, + search: query.search, + }, + ); + return question; + } + + @Get('exam/:examId') + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns all questions in exam', + type: PaginatedQuestionResponseDto, + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by title', + }) + async findQuestionByExamId( + @Req() request: AuthenticatedRequest, + @Query() query: PaginateQueryDto, + @Param( + 'examId', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + examId: string, + ): Promise { + return await this.questionService.findQuestionByExamId( + request.user.id, + request.user.role, + examId, + { + page: query.page, + limit: query.limit, + search: query.search, + }, + ); + } + + @Post() + @Roles(Role.TEACHER) + @ApiResponse({ + status: HttpStatus.CREATED, + description: 'Create an question', + type: QuestionResponseDto, + }) + @HttpCode(HttpStatus.CREATED) + async createQuestion( + @Body() createQuestionDto: CreateQuestionDto, + ): Promise { + const question = await this.questionService.createQuestion( + createQuestionDto, + ); + return new QuestionResponseDto(question); + } + + @Patch(':id') + @Roles(Role.TEACHER) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Update an question', + type: QuestionResponseDto, + }) + async updateQuestion( + @Req() request: AuthenticatedRequest, + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + @Body() updateQuestionDto: UpdateQuestionDto, + ): Promise { + const question = await this.questionService.updateQuestion( + request.user.id, + request.user.role, + id, + updateQuestionDto, + ); + return new QuestionResponseDto(question); + } + + @Delete(':id') + @Roles(Role.TEACHER, Role.ADMIN) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Delete an question', + type: QuestionResponseDto, + }) + async deleteExam( + @Req() request: AuthenticatedRequest, + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + const question = await this.questionService.deleteQuestion( + request.user.id, + request.user.role, + id, + ); + + return new QuestionResponseDto(question); + } +} diff --git a/src/question/question.entity.ts b/src/question/question.entity.ts new file mode 100644 index 0000000..0dac85e --- /dev/null +++ b/src/question/question.entity.ts @@ -0,0 +1,88 @@ +import { ExamAnswer } from 'src/exam-answer/exam-answer.entity'; +import { Exam } from 'src/exam/exam.entity'; +import { Pretest } from 'src/pretest/pretest.entity'; +import { QuestionOption } from 'src/question-option/question-option.entity'; +import { QuestionType } from 'src/shared/enums'; +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, + JoinColumn, + Unique, + OneToMany, +} from 'typeorm'; +@Entity() +@Unique(['examId', 'orderIndex']) +export class Question { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => Exam, (exam) => exam.question, { + onDelete: 'CASCADE', + nullable: true, + }) + @JoinColumn({ name: 'exam_id' }) + exam: Exam; + + @Column({ name: 'exam_id', nullable: true }) + examId: string; + + @ManyToOne(() => Pretest, (pretest) => pretest.question, { + onDelete: 'CASCADE', + nullable: true, + }) + @JoinColumn({ name: 'pretest_id' }) + pretest: Pretest; + + @Column({ name: 'pretest_id', nullable: true }) + pretestId: string; + + @OneToMany( + () => QuestionOption, + (questionOption) => questionOption.question, + { + cascade: true, + }, + ) + questionOption: QuestionOption[]; + + @OneToMany(() => ExamAnswer, (examAnswer) => examAnswer.question, { + cascade: true, + }) + examAnswer: ExamAnswer[]; + + @Column({ + nullable: false, + }) + question: string; + + @Column({ + nullable: false, + }) + type: QuestionType; + + @Column({ + nullable: false, + default: 1, + }) + points: number = 1; + + @Column({ + nullable: false, + default: 1, + }) + orderIndex: number = 1; + + @CreateDateColumn({ + type: 'timestamp with time zone', + }) + createdAt: Date; + + @UpdateDateColumn({ + type: 'timestamp with time zone', + }) + updatedAt: Date; +} diff --git a/src/question/question.module.ts b/src/question/question.module.ts new file mode 100644 index 0000000..0e362f1 --- /dev/null +++ b/src/question/question.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Question } from './question.entity'; +import { DatabaseModule } from 'src/database/database.module'; +import { questionProviders } from './question.providers'; +import { QuestionController } from './question.controller'; +import { QuestionService } from './question.service'; +import { Exam } from 'src/exam/exam.entity'; +import { Pretest } from 'src/pretest/pretest.entity'; + +@Module({ + imports: [ + DatabaseModule, + TypeOrmModule.forFeature([Question, Exam, Pretest]), + ], + controllers: [QuestionController], + providers: [...questionProviders, QuestionService], + exports: [QuestionService], +}) +export class QuestionModule {} diff --git a/src/question/question.providers.ts b/src/question/question.providers.ts new file mode 100644 index 0000000..bc5bd59 --- /dev/null +++ b/src/question/question.providers.ts @@ -0,0 +1,10 @@ +import { DataSource } from 'typeorm'; +import { Question } from './question.entity'; + +export const questionProviders = [ + { + provide: 'QuestionRepository', + useFactory: (dataSource: DataSource) => dataSource.getRepository(Question), + inject: ['DataSource'], + }, +]; diff --git a/src/question/question.service.ts b/src/question/question.service.ts new file mode 100644 index 0000000..729be92 --- /dev/null +++ b/src/question/question.service.ts @@ -0,0 +1,565 @@ +import { + Injectable, + Inject, + NotFoundException, + ConflictException, + BadRequestException, +} from '@nestjs/common'; +import { Question } from './question.entity'; +import { + FindOneOptions, + FindOptionsSelect, + FindOptionsWhere, + ILike, + Not, + Repository, +} from 'typeorm'; +import { PaginatedQuestionResponseDto } from './dtos/question-response.dto'; +import { createPagination } from 'src/shared/pagination'; +import { CourseStatus, ExamStatus, QuestionType, Role } from 'src/shared/enums'; +import { + CreateQuestionDto, + CreateQuestionPretestDto, +} from './dtos/create-question.dto'; +import { UpdateQuestionDto } from './dtos/update-question.dto'; +import { Exam } from 'src/exam/exam.entity'; +import { Pretest } from 'src/pretest/pretest.entity'; +import { PaginatedQuestionPretestResponseDto } from './dtos/question-pretest-response.dto'; +@Injectable() +export class QuestionService { + constructor( + @Inject('QuestionRepository') + private readonly questionRepository: Repository, + @Inject('ExamRepository') + private readonly examRepository: Repository, + @Inject('PretestRepository') + private readonly pretestRepository: Repository, + ) {} + + async findAll( + userId: string, + role: Role, + { + page = 1, + limit = 20, + search = '', + }: { + page?: number; + limit?: number; + search?: string; + }, + ): Promise { + const { find } = await createPagination(this.questionRepository, { + page, + limit, + }); + + const whereCondition = this.validateAndCreateCondition( + userId, + role, + search, + ); + const question = await find({ + order: { + orderIndex: 'ASC', + }, + where: whereCondition, + relations: [ + 'exam', + 'exam.courseModule', + 'exam.courseModule.course', + 'exam.courseModule.course.teacher', + ], + select: { + exam: this.selectPopulateExam(), + }, + }).run(); + + return question; + } + + async findOne( + userId: string, + role: Role, + options: FindOneOptions = {}, + ): Promise { + const whereCondition = this.validateAndCreateCondition(userId, role, ''); + + const where = Array.isArray(whereCondition) + ? [ + { ...whereCondition[0], ...options.where }, + { ...whereCondition[1], ...options.where }, + ] + : { ...whereCondition, ...options.where }; + + const question = await this.questionRepository.findOne({ + ...options, + where, + relations: [ + 'exam', + 'exam.courseModule', + 'exam.courseModule.course', + 'exam.courseModule.course.teacher', + ], + select: { + exam: this.selectPopulateExam(), + }, + }); + + if (!question) { + throw new NotFoundException('Question not found'); + } + + return question; + } + + async findQuestionByExamId( + userId: string, + role: Role, + examId: string, + { + page = 1, + limit = 20, + search = '', + }: { + page?: number; + limit?: number; + search?: string; + }, + ): Promise { + const { find } = await createPagination(this.questionRepository, { + page, + limit, + }); + + const whereCondition = this.validateAndCreateCondition( + userId, + role, + search, + ); + whereCondition['exam'] = { id: examId }; + + const exam = await this.examRepository.findOne({ + where: { id: examId }, + }); + + if (!exam) { + throw new NotFoundException('Exam not found.'); + } + + if (!exam.shuffleQuestions) { + const question = await find({ + order: { + orderIndex: 'ASC', + }, + where: whereCondition, + relations: [ + 'exam', + 'exam.courseModule', + 'exam.courseModule.course', + 'exam.courseModule.course.teacher', + ], + select: { + exam: this.selectPopulateExam(), + }, + }).run(); + return question; + } + + const baseQuery = this.questionRepository + .createQueryBuilder('question') + .where(whereCondition) + .orderBy('RANDOM()'); + + return await find({ + where: whereCondition, + ...{ + __queryBuilder: baseQuery, + }, + relations: [ + 'exam', + 'exam.courseModule', + 'exam.courseModule.course', + 'exam.courseModule.course.teacher', + ], + select: { + exam: this.selectPopulateExam(), + }, + }).run(); + } + + private validateAndCreateCondition( + userId: string, + role: Role, + search: string, + ): FindOptionsWhere { + const baseSearch = search ? { question: ILike(`%${search}%`) } : {}; + + if (role === Role.STUDENT) { + return { + ...baseSearch, + exam: { + status: ExamStatus.PUBLISHED, + courseModule: { + course: { + status: CourseStatus.PUBLISHED, + enrollments: { + user: { + id: userId, + }, + }, + }, + }, + }, + }; + } + + if (role === Role.TEACHER) { + return { + ...baseSearch, + exam: [ + { + courseModule: { + course: { + teacher: { id: userId }, + }, + }, + }, + ], + }; + } + + if (role === Role.ADMIN) { + return { ...baseSearch }; + } + + return { + ...baseSearch, + exam: { + status: ExamStatus.PUBLISHED, + courseModule: { + course: { + status: CourseStatus.PUBLISHED, + enrollments: { + user: { + id: userId, + }, + }, + }, + }, + }, + }; + } + + async getMaxOrderIndex(examId: string): Promise { + const result = await this.questionRepository + .createQueryBuilder('question') + .select('MAX(question.orderIndex)', 'max') + .where('question.examId = :examId', { examId }) + .getRawOne(); + + return result.max ? Number(result.max) : 0; + } + + async reOrderIndex(examId: string): Promise { + const questionToReorder = await this.questionRepository.find({ + where: { examId }, + order: { orderIndex: 'ASC' }, + }); + + for (let i = 0; i < questionToReorder.length; i++) { + questionToReorder[i].orderIndex = i + 1; + } + + await this.questionRepository.save(questionToReorder); + } + + async createQuestion( + createQuestionDto: CreateQuestionDto, + ): Promise { + if (!createQuestionDto.orderIndex) { + createQuestionDto.orderIndex = + (await this.getMaxOrderIndex(createQuestionDto.examId)) + 1; + } + const exam = await this.examRepository.findOne({ + where: { id: createQuestionDto.examId }, + select: this.selectPopulateExam(), + relations: [ + 'courseModule', + 'courseModule.course', + 'courseModule.course.teacher', + ], + }); + if (!exam) { + throw new NotFoundException('Exam not found.'); + } + const question = await this.questionRepository.create({ + ...createQuestionDto, + exam, + }); + if (!question) throw new BadRequestException('Can not create question'); + try { + await this.questionRepository.save(question); + return question; + } catch (error) { + if (error.code === '23505') { + throw new ConflictException( + 'Same orderIndex already exists in this exam.', + ); + } + throw new BadRequestException( + "Can't create question. Please check your input.", + ); + } + } + + async updateQuestion( + userId: string, + role: Role, + id: string, + updateQuestionDto: UpdateQuestionDto, + ): Promise { + const question = await this.findOne(userId, role, { where: { id } }); + if (this.checkPermission(userId, role, question) === false) + throw new BadRequestException('Can not change this question'); + try { + const question = await this.questionRepository.update( + id, + updateQuestionDto, + ); + if (!question) throw new BadRequestException("Can't update question"); + return await this.questionRepository.findOne({ + where: { id }, + relations: [ + 'exam', + 'exam.courseModule', + 'exam.courseModule.course', + 'exam.courseModule.course.teacher', + ], + select: { + exam: this.selectPopulateExam(), + }, + }); + } catch (error) { + if (error.code === '23505') { + throw new ConflictException( + 'Same orderIndex already exists in this exam.', + ); + } + throw new BadRequestException( + "Can't update question. Please check your input.", + ); + } + } + + async deleteQuestion( + userId: string, + role: Role, + id: string, + ): Promise { + try { + const question = await this.findOne(userId, role, { where: { id } }); + if (this.checkPermission(userId, role, question) === false) + throw new BadRequestException('Can not change this question'); + const removeQuestion = await this.questionRepository.remove(question); + await this.reOrderIndex(question.examId); + return removeQuestion; + } catch (error) { + if (error instanceof Error) + throw new NotFoundException('Question not found'); + } + } + + private selectPopulateExam(): FindOptionsSelect { + return { + id: true, + title: true, + description: true, + timeLimit: true, + passingScore: true, + maxAttempts: true, + shuffleQuestions: true, + status: true, + courseModule: { + id: true, + course: { + id: true, + teacher: { + id: true, + }, + }, + }, + }; + } + + async findAllQuestionPretest( + userId: string, + role: Role, + { + page = 1, + limit = 20, + search = '', + }: { + page?: number; + limit?: number; + search?: string; + }, + ): Promise { + const { find } = await createPagination(this.questionRepository, { + page, + limit, + }); + + const whereCondition = this.validateAndCreateConditionForPretest( + userId, + role, + search, + ); + const question = await find({ + order: { + orderIndex: 'ASC', + }, + where: whereCondition, + relations: ['pretest', 'pretest.user'], + select: { + pretest: this.selectPopulatePretest(), + }, + }).run(); + + return question; + } + + async findQuestionPretestByPretestId( + userId: string, + role: Role, + pretestId: string, + { + page = 1, + limit = 20, + search = '', + }: { + page?: number; + limit?: number; + search?: string; + }, + ): Promise { + const { find } = await createPagination(this.questionRepository, { + page, + limit, + }); + + const whereCondition = this.validateAndCreateConditionForPretest( + userId, + role, + search, + ); + whereCondition['pretest'] = { id: pretestId }; + + const pretest = await this.pretestRepository.findOne({ + where: { id: pretestId }, + }); + + if (!pretest) { + throw new NotFoundException('Pretest not found.'); + } + + const question = await find({ + order: { + orderIndex: 'ASC', + }, + where: whereCondition, + relations: ['pretest', 'pretest.user'], + select: { + pretest: this.selectPopulatePretest(), + }, + }).run(); + return question; + } + + private validateAndCreateConditionForPretest( + userId: string, + role: Role, + search: string, + ): FindOptionsWhere { + const baseSearch = search ? { question: ILike(`%${search}%`) } : {}; + + if (role === Role.STUDENT) { + return { + ...baseSearch, + pretest: { + user: { + id: userId, + }, + }, + }; + } + + if (role === Role.ADMIN) { + return { ...baseSearch }; + } + + return { + ...baseSearch, + pretest: { + user: { + id: userId, + }, + }, + }; + } + + async createQuestionForPretest( + createQuestionPretestDto: CreateQuestionPretestDto, + ): Promise { + const pretest = await this.pretestRepository.findOne({ + where: { id: createQuestionPretestDto.pretestId }, + }); + + if (!pretest) throw new NotFoundException('Not found pretest'); + + const question = this.questionRepository.create({ + ...createQuestionPretestDto, + pretest, + }); + + if (!question) + throw new BadRequestException('Cannot create pretest question'); + + await this.questionRepository.save(question); + return question; + } + + private selectPopulatePretest(): FindOptionsSelect { + return { + id: true, + title: true, + description: true, + timeLimit: true, + passingScore: true, + maxAttempts: true, + user: { + id: true, + username: true, + fullname: true, + role: true, + email: true, + profileKey: true, + }, + }; + } + + private checkPermission( + userId: string, + role: Role, + question: Question, + ): boolean { + switch (role) { + case Role.ADMIN: + return true; + case Role.TEACHER: + return question.exam.courseModule?.course?.teacher?.id == userId; + case Role.STUDENT: + return false; + } + } +} diff --git a/src/reward/dtos/create-reward.dto.ts b/src/reward/dtos/create-reward.dto.ts new file mode 100644 index 0000000..9cf3967 --- /dev/null +++ b/src/reward/dtos/create-reward.dto.ts @@ -0,0 +1,72 @@ +import { + IsEnum, + IsNotEmpty, + IsNumber, + IsOptional, + IsString, +} from '@nestjs/class-validator'; +import { Type } from '../enums/type.enum'; +import { Status } from '../enums/status.enum'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class CreateRewardDto { + @IsString() + @IsNotEmpty() + @ApiProperty({ + description: 'reward name', + type: String, + example: 'gift card', + }) + name: string; + + @IsOptional() + @IsString() + @ApiPropertyOptional({ + description: 'description of reward (optional)', + type: String, + example: 'get 15% of in next course', + }) + description?: string; + + @IsEnum(Type, { + message: `Invalid type. Type should be ${Type.BADGE} ${Type.CERTIFICATE} or ${Type.ITEM}`, + }) + @IsNotEmpty() + @ApiProperty({ + description: 'type of reward', + type: String, + example: Type.BADGE, + enum: Type, + }) + type: Type.BADGE | Type.CERTIFICATE | Type.ITEM; + + @IsNumber() + @IsNotEmpty() + @ApiProperty({ + description: 'how many points require for this reward', + type: Number, + example: 100, + }) + points: number; + + @IsNumber() + @IsNotEmpty() + @ApiProperty({ + description: 'how many reward left', + type: Number, + example: 5, + }) + stock: number; + + @IsEnum(Status, { + message: `Invalid status. Status should be ${Status.ACTIVE} or ${Status.INACTIVE}`, + }) + @IsNotEmpty() + @ApiProperty({ + description: 'status of reward', + type: String, + example: Status.INACTIVE, + enum: Status, + }) + status: Status.ACTIVE | Status.INACTIVE; +} diff --git a/src/reward/dtos/paginate-reward-query.dto.ts b/src/reward/dtos/paginate-reward-query.dto.ts new file mode 100644 index 0000000..1b25acf --- /dev/null +++ b/src/reward/dtos/paginate-reward-query.dto.ts @@ -0,0 +1,23 @@ +import { ApiProperty, IntersectionType } from '@nestjs/swagger'; +import { Type } from '../enums/type.enum'; +import { IsEnum, IsOptional } from 'class-validator'; +import { PaginateQueryDto } from 'src/shared/pagination/dtos/paginate-query.dto'; + +class TypeQueryDto { + @ApiProperty({ + name: 'type', + enum: Type, + required: false, + description: `sort by type (${Type.BADGE} ${Type.CERTIFICATE} or ${Type.ITEM})`, + }) + @IsOptional() + @IsEnum(Type, { + message: `type must be ${Type.BADGE} ${Type.CERTIFICATE} or ${Type.ITEM}`, + }) + type: Type; +} + +export class PaginateRewardQueryDto extends IntersectionType( + PaginateQueryDto, + TypeQueryDto, +) {} diff --git a/src/reward/dtos/reward-response.dto.ts b/src/reward/dtos/reward-response.dto.ts new file mode 100644 index 0000000..c7f8ddb --- /dev/null +++ b/src/reward/dtos/reward-response.dto.ts @@ -0,0 +1,105 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Status } from '../enums/status.enum'; +import { Type } from '../enums/type.enum'; +import { Reward } from '../reward.entity'; +import { PaginatedResponse } from 'src/shared/pagination/dtos/paginate-response.dto'; + +export class RewardResponseDto { + @ApiProperty({ + description: 'Reward ID', + type: String, + example: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + }) + id: string; + + @ApiProperty({ + description: 'reward name', + type: String, + example: 'gift card', + }) + name: string; + + @ApiPropertyOptional({ + description: 'description of reward (optional)', + type: String, + example: 'get 15% of in next course', + }) + description?: string; + + @ApiPropertyOptional({ + description: 'thumbnail of reward (optional)', + type: String, + example: 'url.png', + }) + thumbnail?: string; + + @ApiProperty({ + description: 'type of reward', + type: String, + example: Type.BADGE, + enum: Type, + }) + type: Type; + + @ApiProperty({ + description: 'how many points require for this reward', + type: Number, + example: 100, + }) + points: number; + + @ApiProperty({ + description: 'how many reward left', + type: Number, + example: 5, + }) + stock: number; + + @ApiProperty({ + description: 'status of reward', + type: String, + example: Status.INACTIVE, + enum: Status, + }) + status: Status; + + @ApiProperty({ + description: 'created date', + type: Date, + example: new Date(), + }) + createdAt: Date; + + @ApiProperty({ + description: 'updated date', + type: Date, + example: new Date(), + }) + updatedAt: Date; + + constructor(reward: Reward) { + this.id = reward.id; + this.name = reward.name; + this.description = reward.description; + this.thumbnail = reward.thumbnail; + this.type = reward.type; + this.points = reward.points; + this.stock = reward.stock; + this.status = reward.status; + this.createdAt = reward.createdAt; + this.updatedAt = reward.updatedAt; + } +} +export class PaginatedRewardResponseDto extends PaginatedResponse( + RewardResponseDto, +) { + constructor( + rewards: Reward[], + total: number, + pageSize: number, + currentPage: number, + ) { + const rewardDtos = rewards.map((reward) => new RewardResponseDto(reward)); + super(rewardDtos, total, pageSize, currentPage); + } +} diff --git a/src/reward/dtos/update-reward.dto.ts b/src/reward/dtos/update-reward.dto.ts new file mode 100644 index 0000000..094fca9 --- /dev/null +++ b/src/reward/dtos/update-reward.dto.ts @@ -0,0 +1,80 @@ +import { + IsEnum, + IsNumber, + IsOptional, + IsString, +} from '@nestjs/class-validator'; +import { Type } from '../enums/type.enum'; +import { Status } from '../enums/status.enum'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class UpdateRewardDto { + @IsString() + @IsOptional() + @ApiPropertyOptional({ + description: 'reward name', + type: String, + example: 'gift card', + }) + name?: string; + + @IsOptional() + @IsString() + @ApiPropertyOptional({ + description: 'description of reward (optional)', + type: String, + example: 'get 15% of in next course', + }) + description?: string; + + @IsString() + @IsOptional() + @ApiPropertyOptional({ + description: 'thumbnail of reward (optional)', + type: String, + example: 'url.png', + }) + thumbnail?: string; + + @IsEnum(Type, { + message: `Invalid type. Type should be ${Type.BADGE} ${Type.CERTIFICATE} or ${Type.ITEM}`, + }) + @IsOptional() + @ApiPropertyOptional({ + description: 'type of reward', + type: String, + example: Type.BADGE, + enum: Type, + }) + type?: Type.BADGE | Type.CERTIFICATE | Type.ITEM; + + @IsNumber() + @IsOptional() + @ApiPropertyOptional({ + description: 'how many points require for this reward', + type: Number, + example: 100, + }) + points?: number; + + @IsNumber() + @IsOptional() + @ApiPropertyOptional({ + description: 'how many reward left', + type: Number, + example: 5, + }) + stock?: number; + + @IsEnum(Status, { + message: `Invalid status. Status should be ${Status.ACTIVE} or ${Status.INACTIVE}`, + }) + @IsOptional() + @ApiPropertyOptional({ + description: 'status of reward', + type: String, + example: Status.INACTIVE, + enum: Status, + }) + status?: Status.ACTIVE | Status.INACTIVE; +} diff --git a/src/reward/enums/status.enum.ts b/src/reward/enums/status.enum.ts new file mode 100644 index 0000000..78be8fd --- /dev/null +++ b/src/reward/enums/status.enum.ts @@ -0,0 +1,4 @@ +export enum Status { + ACTIVE = 'active', + INACTIVE = 'inactive', +} diff --git a/src/reward/enums/type.enum.ts b/src/reward/enums/type.enum.ts new file mode 100644 index 0000000..3848346 --- /dev/null +++ b/src/reward/enums/type.enum.ts @@ -0,0 +1,5 @@ +export enum Type { + BADGE = 'badge', + CERTIFICATE = 'certificate', + ITEM = 'item', +} diff --git a/src/reward/reward.controllers.ts b/src/reward/reward.controllers.ts new file mode 100644 index 0000000..9282c24 --- /dev/null +++ b/src/reward/reward.controllers.ts @@ -0,0 +1,252 @@ +import { + Body, + Controller, + Get, + HttpCode, + HttpStatus, + Injectable, + Param, + ParseUUIDPipe, + Post, + Patch, + Delete, + Query, + UploadedFile, + ParseFilePipeBuilder, + StreamableFile, + UseInterceptors, + Req, +} from '@nestjs/common'; +import { + ApiBearerAuth, + ApiParam, + ApiResponse, + ApiTags, + ApiQuery, + ApiConsumes, + ApiBody, +} from '@nestjs/swagger'; +import { RewardService } from './reward.service'; +import { Roles } from 'src/shared/decorators/role.decorator'; +import { + PaginatedRewardResponseDto, + RewardResponseDto, +} from './dtos/reward-response.dto'; +import { CreateRewardDto } from './dtos/create-reward.dto'; +import { Role } from 'src/shared/enums/roles.enum'; +import { UpdateRewardDto } from './dtos/update-reward.dto'; +import { FileService } from 'src/file/file.service'; +import { PaginateQueryDto } from 'src/shared/pagination/dtos/paginate-query.dto'; +import { Folder } from 'src/file/enums/folder.enum'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { AuthenticatedRequest } from 'src/auth/interfaces/authenticated-request.interface'; +import { PaginateRewardQueryDto } from './dtos/paginate-reward-query.dto'; +import { Type } from './enums/type.enum'; + +@Controller('reward') +@Injectable() +@ApiTags('Reward') +@ApiBearerAuth() +export class RewardController { + constructor( + private readonly rewardService: RewardService, + private readonly fileService: FileService, + ) {} + + @Get() + @ApiResponse({ + status: HttpStatus.OK, + type: PaginatedRewardResponseDto, + description: 'get all reward', + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by name', + }) + @ApiQuery({ + name: 'type', + type: String, + required: false, + description: `search by ${Type.BADGE} ${Type.CERTIFICATE} or ${Type.ITEM}`, + }) + async findAll( + @Req() request: AuthenticatedRequest, + @Query() query: PaginateRewardQueryDto, + ): Promise { + return this.rewardService.findAll(query, request.user.role); + } + + @Get(':id') + @ApiResponse({ + status: HttpStatus.OK, + type: RewardResponseDto, + description: 'get reward', + }) + async findOne( + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + @Req() request: AuthenticatedRequest, + ): Promise { + const reward = await this.rewardService.findOne(id, request.user.role); + return new RewardResponseDto(reward); + } + + @Post() + @HttpCode(HttpStatus.CREATED) + @Roles(Role.ADMIN) + @ApiResponse({ + status: HttpStatus.OK, + type: RewardResponseDto, + description: 'create new reward', + }) + async create( + @Body() CreateRewardDto: CreateRewardDto, + ): Promise { + return this.rewardService.create(CreateRewardDto); + } + + @Get('thumbnail/:id') + @ApiResponse({ + status: HttpStatus.OK, + description: 'Get thumbnail reward', + type: StreamableFile, + }) + async getThumbnail( + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + @Req() request: AuthenticatedRequest, + ): Promise { + const reward = await this.rewardService.findOne(id, request.user.role); + const file = await this.fileService.get( + Folder.REWARD_THUMBNAILS, + reward.thumbnail, + ); + return new StreamableFile(file, { + disposition: 'inline', + type: `image/${reward.thumbnail.split('.').pop()}`, + }); + } + + @Patch('thumbnail/:id') + @Roles(Role.ADMIN) + @HttpCode(HttpStatus.NO_CONTENT) + @UseInterceptors(FileInterceptor('file')) + @ApiResponse({ + description: 'upload file thumbnail of reward successfully', + status: HttpStatus.NO_CONTENT, + }) + @ApiConsumes('multipart/form-data') + @ApiBody({ + schema: { + type: 'object', + properties: { + file: { + type: 'string', + format: 'binary', + }, + }, + }, + }) + async uploadThumbnail( + @UploadedFile( + new ParseFilePipeBuilder() + .addFileTypeValidator({ fileType: 'image/*' }) + .build({ + fileIsRequired: true, + errorHttpStatusCode: HttpStatus.BAD_REQUEST, + }), + ) + file: Express.Multer.File, + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + @Req() request: AuthenticatedRequest, + ): Promise { + const reward = await this.rewardService.findOne(id, request.user.role); + if (reward.thumbnail) + await this.fileService.update( + Folder.REWARD_THUMBNAILS, + reward.thumbnail, + file, + ); + else { + await this.fileService.upload(Folder.REWARD_THUMBNAILS, id, file); + } + await this.rewardService.update(id, { + thumbnail: `${id}.${file.originalname.split('.').pop()}`, + }); + } + + @Patch(':id') + @Roles(Role.ADMIN) + @ApiResponse({ + status: HttpStatus.OK, + type: RewardResponseDto, + description: 'edit reward', + }) + async update( + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + @Body() UpdateRewardDto: UpdateRewardDto, + ): Promise { + const reward = await this.rewardService.update(id, UpdateRewardDto); + return new RewardResponseDto(reward); + } + + @Delete(':id') + @Roles(Role.ADMIN) + @ApiResponse({ + status: HttpStatus.NO_CONTENT, + description: 'delete reward', + }) + async delete( + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + await this.rewardService.delete(id); + } +} diff --git a/src/reward/reward.entity.ts b/src/reward/reward.entity.ts new file mode 100644 index 0000000..feb8727 --- /dev/null +++ b/src/reward/reward.entity.ts @@ -0,0 +1,74 @@ +import { + Column, + CreateDateColumn, + Entity, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; +import { Type } from './enums/type.enum'; +import { Status } from './enums/status.enum'; +import { UserReward } from 'src/user-reward/user-reward.entity'; + +@Entity() +export class Reward { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ + nullable: false, + unique: true, + }) + name: string; + + @Column({ + nullable: true, + }) + description: string; + + @Column({ + nullable: true, + default: null, + }) + thumbnail: string; + + @Column({ + nullable: false, + type: 'enum', + enum: Type, + }) + type: Type; + + @Column({ + nullable: false, + }) + points: number; + + @Column({ + nullable: false, + }) + stock: number; + + @Column({ + nullable: false, + type: 'enum', + enum: Status, + }) + status: Status; + + @OneToMany(() => UserReward, (userReward) => userReward.reward) + userReward: UserReward; + + @CreateDateColumn({ + type: 'timestamp with time zone', + nullable: false, + }) + createdAt: Date; + + @UpdateDateColumn({ + type: 'timestamp with time zone', + nullable: false, + }) + updatedAt: Date; +} diff --git a/src/reward/reward.module.ts b/src/reward/reward.module.ts new file mode 100644 index 0000000..23f9772 --- /dev/null +++ b/src/reward/reward.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { DatabaseModule } from 'src/database/database.module'; +import { RewardController } from './reward.controllers'; +import { rewardProviders } from './reward.providers'; +import { RewardService } from './reward.service'; +import { FileModule } from 'src/file/file.module'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Reward } from './reward.entity'; + +@Module({ + imports: [DatabaseModule, TypeOrmModule.forFeature([Reward]), FileModule], + controllers: [RewardController], + providers: [...rewardProviders, RewardService], + exports: [RewardService], +}) +export class RewardModule {} diff --git a/src/reward/reward.providers.ts b/src/reward/reward.providers.ts new file mode 100644 index 0000000..b8a2e2c --- /dev/null +++ b/src/reward/reward.providers.ts @@ -0,0 +1,10 @@ +import { DataSource } from 'typeorm'; +import { Reward } from './reward.entity'; + +export const rewardProviders = [ + { + provide: 'RewardRepository', + useFactory: (DataSource: DataSource) => DataSource.getRepository(Reward), + inject: ['DataSource'], + }, +]; diff --git a/src/reward/reward.service.ts b/src/reward/reward.service.ts new file mode 100644 index 0000000..0bfbb7d --- /dev/null +++ b/src/reward/reward.service.ts @@ -0,0 +1,111 @@ +import { + BadRequestException, + Inject, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { FindOneOptions, ILike, Repository, FindOptionsWhere } from 'typeorm'; +import { Reward } from './reward.entity'; +import { CreateRewardDto } from './dtos/create-reward.dto'; +import { UpdateRewardDto } from './dtos/update-reward.dto'; +import { ConfigService } from '@nestjs/config'; +import { createPagination } from 'src/shared/pagination'; +import { PaginatedRewardResponseDto } from './dtos/reward-response.dto'; +import { Status } from './enums/status.enum'; +import { Role } from 'src/shared/enums'; +import { Type } from './enums/type.enum'; + +@Injectable() +export class RewardService { + constructor( + @Inject('RewardRepository') + private readonly rewardRepository: Repository, + private readonly configService: ConfigService, + ) {} + + async findAll( + { + page = 1, + limit = 20, + search = '', + type = '', + }: { + page?: number; + limit?: number; + search?: string; + type?: Type | ''; + }, + role?: Role, + ): Promise { + const { find } = await createPagination(this.rewardRepository, { + page, + limit, + }); + const conditionSearch = search ? { name: ILike(`%${search}%`) } : {}; + const conditionType = type ? { type: type } : {}; + if (role && role === Role.ADMIN) { + const rewards = await find({ + where: { + ...conditionSearch, + ...conditionType, + }, + }).run(); + return rewards; + } + const rewards = await find({ + where: { + ...conditionSearch, + ...conditionType, + status: Status.ACTIVE, + }, + }).run(); + return rewards; + } + + async findOne(id: string, role: Role): Promise { + const condition = + role === Role.ADMIN ? { id } : { id, status: Status.ACTIVE }; + const reward = await this.rewardRepository.findOne({ where: condition }); + if (!reward) throw new NotFoundException('reward not found'); + return reward; + } + + async create(CreateRewardDto: CreateRewardDto): Promise { + try { + const reward = await this.rewardRepository.findOne({ + where: { + name: CreateRewardDto.name, + }, + }); + if (reward) throw new BadRequestException('reward already exists'); + if (CreateRewardDto.points < 0) + throw new BadRequestException('points should not be less than zero'); + return this.rewardRepository.save(CreateRewardDto); + } catch (error) { + if (error instanceof Error) { + throw new BadRequestException(error.message); + } + } + } + + async update(id: string, UpdateRewardDto: UpdateRewardDto): Promise { + try { + if (UpdateRewardDto.points < 0) + throw new BadRequestException('points should not be less than zero'); + await this.rewardRepository.update(id, UpdateRewardDto); + return this.rewardRepository.findOne({ where: { id } }); + } catch (error) { + if (error instanceof Error) + throw new NotFoundException('reward not found'); + } + } + + async delete(id: string): Promise { + try { + this.rewardRepository.delete(id); + } catch (error) { + if (error instanceof Error) + throw new NotFoundException('reward not found'); + } + } +} diff --git a/src/roadmap/dtos/create-roadmap.dto.ts b/src/roadmap/dtos/create-roadmap.dto.ts new file mode 100644 index 0000000..3de89e4 --- /dev/null +++ b/src/roadmap/dtos/create-roadmap.dto.ts @@ -0,0 +1,31 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsNumber, IsString, IsUUID } from 'class-validator'; + +export class CreateRoadmapDto { + @IsString() + @IsNotEmpty() + @ApiProperty({ + description: 'Roadmap duration', + type: String, + example: '2 months', + }) + duration: string; + + @IsNumber() + @IsNotEmpty() + @ApiProperty({ + description: 'Roadmap priority', + type: Number, + example: 1, + }) + priority: number; + + @IsUUID('4', { each: true }) + @IsNotEmpty() + @ApiProperty({ + description: 'Roadmap courses', + type: [String], + example: ['8d4887aa-28e7-4d0e-844c-28a8ccead003'], + }) + courses: string[]; +} diff --git a/src/roadmap/dtos/create-roadmp-ai.dto.ts b/src/roadmap/dtos/create-roadmp-ai.dto.ts new file mode 100644 index 0000000..90b773c --- /dev/null +++ b/src/roadmap/dtos/create-roadmp-ai.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class CreateRoadmapAiDto { + @IsString() + @IsNotEmpty() + @ApiProperty({ + description: 'Roadmap AI Pretest Description', + type: String, + example: 'AI Roadmap', + }) + preTestDescription: string; +} diff --git a/src/roadmap/dtos/index.ts b/src/roadmap/dtos/index.ts new file mode 100644 index 0000000..17157e8 --- /dev/null +++ b/src/roadmap/dtos/index.ts @@ -0,0 +1,3 @@ +export * from './create-roadmap.dto'; +export * from './roadmap-response.dto'; +export * from './update-roadmap.dto'; diff --git a/src/roadmap/dtos/roadmap-response.dto.ts b/src/roadmap/dtos/roadmap-response.dto.ts new file mode 100644 index 0000000..39486b2 --- /dev/null +++ b/src/roadmap/dtos/roadmap-response.dto.ts @@ -0,0 +1,77 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { CourseResponseDto } from 'src/course/dtos'; +import { PaginatedResponse } from 'src/shared/pagination/dtos/paginate-response.dto'; +import { UserResponseDto } from 'src/user/dtos/user-response.dto'; +import { Roadmap } from '../roadmap.entity'; + +export class RoadmapResponseDto { + @ApiProperty({ + description: 'Roadmap ID', + type: String, + example: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + }) + id: string; + + @ApiProperty({ + description: 'Roadmap duration', + type: String, + example: '2 months', + }) + duration: string; + + @ApiProperty({ + description: 'Roadmap priority', + type: Number, + example: 1, + }) + priority: number; + + @ApiProperty({ + description: 'Roadmap user', + type: UserResponseDto, + }) + user: UserResponseDto; + + @ApiProperty({ + description: 'Roadmap courses', + type: [CourseResponseDto], + }) + courses: CourseResponseDto[]; + + @ApiProperty({ + description: 'Roadmap creation date', + type: Date, + }) + createdAt: Date; + + @ApiProperty({ + description: 'Roadmap update date', + type: Date, + }) + updatedAt: Date; + + constructor(roadmap: Roadmap) { + this.id = roadmap.id; + this.duration = roadmap.duration; + this.priority = roadmap.priority; + this.user = new UserResponseDto(roadmap.user); + this.courses = roadmap.courses.map( + (course) => new CourseResponseDto(course), + ); + this.createdAt = roadmap.createdAt; + this.updatedAt = roadmap.updatedAt; + } +} + +export class PaginatedRoadmapResponseDto extends PaginatedResponse( + RoadmapResponseDto, +) { + constructor( + roadmaps: RoadmapResponseDto[], + total: number, + pageSize: number, + currentPage: number, + ) { + super(roadmaps, total, pageSize, currentPage); + } +} diff --git a/src/roadmap/dtos/update-roadmap.dto.ts b/src/roadmap/dtos/update-roadmap.dto.ts new file mode 100644 index 0000000..e50a445 --- /dev/null +++ b/src/roadmap/dtos/update-roadmap.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateRoadmapDto } from './create-roadmap.dto'; + +export class UpdateRoadmapDto extends PartialType(CreateRoadmapDto) {} diff --git a/src/roadmap/roadmap.controller.ts b/src/roadmap/roadmap.controller.ts new file mode 100644 index 0000000..fdde46c --- /dev/null +++ b/src/roadmap/roadmap.controller.ts @@ -0,0 +1,139 @@ +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Injectable, + Param, + ParseUUIDPipe, + Patch, + Post, + Query, + Req, +} from '@nestjs/common'; +import { ApiBearerAuth, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { AuthenticatedRequest } from 'src/auth/interfaces/authenticated-request.interface'; +import { Roles } from 'src/shared/decorators/role.decorator'; +import { Role } from 'src/shared/enums'; +import { PaginateQueryDto } from 'src/shared/pagination/dtos/paginate-query.dto'; +import { CreateRoadmapDto, RoadmapResponseDto } from './dtos'; +import { RoadmapService } from './roadmap.service'; +import { CreateRoadmapAiDto } from './dtos/create-roadmp-ai.dto'; + +@Controller('roadmap') +@Injectable() +@ApiTags('Roadmap') +@ApiBearerAuth() +export class RoadmapController { + constructor(private readonly roadmapService: RoadmapService) {} + + @Get() + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns all roadmaps', + type: RoadmapResponseDto, + isArray: true, + }) + @Roles(Role.ADMIN) + async findAll(@Query() query: PaginateQueryDto) { + return await this.roadmapService.findAll({ + ...query, + }); + } + + @Get('/user') + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns all roadmaps by user id', + type: RoadmapResponseDto, + isArray: true, + }) + @Roles(Role.STUDENT) + @Roles(Role.ADMIN) + async findAllByUser( + @Req() request: AuthenticatedRequest, + @Query() query: PaginateQueryDto, + ) { + return await this.roadmapService.findAll({ + userId: request.user.id, + ...query, + }); + } + @Get(':id') + @ApiResponse({ + status: HttpStatus.OK, + description: 'Returns a roadmap by id', + type: RoadmapResponseDto, + }) + @Roles(Role.STUDENT) + @Roles(Role.ADMIN) + async findOne( + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + }), + ) + id: string, + ) { + return await this.roadmapService.findOne({ where: { id } }); + } + + @Post() + @ApiResponse({ + status: HttpStatus.CREATED, + description: 'Creates a new roadmap', + type: RoadmapResponseDto, + }) + @Roles(Role.STUDENT) + @HttpCode(HttpStatus.CREATED) + async create( + @Req() request: AuthenticatedRequest, + @Body() createRoadmapDto: CreateRoadmapAiDto, + ) { + return await this.roadmapService.create(request.user.id, createRoadmapDto); + } + + @Patch(':id') + @ApiResponse({ + status: HttpStatus.OK, + description: 'Updates a roadmap by id', + type: RoadmapResponseDto, + }) + @Roles(Role.STUDENT) + @Roles(Role.ADMIN) + async update( + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + }), + ) + id: string, + @Body() updateRoadmapDto: CreateRoadmapDto, + ) { + return await this.roadmapService.update({ id }, updateRoadmapDto); + } + + @Delete(':id') + @ApiResponse({ + status: HttpStatus.OK, + description: 'Deletes a roadmap by id', + type: RoadmapResponseDto, + }) + @Roles(Role.ADMIN) + @HttpCode(HttpStatus.NO_CONTENT) + async remove( + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + }), + ) + id: string, + ) { + return await this.roadmapService.delete({ id }); + } +} diff --git a/src/roadmap/roadmap.entity.ts b/src/roadmap/roadmap.entity.ts new file mode 100644 index 0000000..3dd3264 --- /dev/null +++ b/src/roadmap/roadmap.entity.ts @@ -0,0 +1,52 @@ +import { Course } from 'src/course/course.entity'; +import { User } from 'src/user/user.entity'; +import { + Column, + CreateDateColumn, + Entity, + JoinTable, + ManyToMany, + ManyToOne, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity() +export class Roadmap { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => User, { + nullable: false, + onDelete: 'CASCADE', + }) + user: User; + + @ManyToMany(() => Course, (course) => course.roadmaps) + @JoinTable() + courses: Course[]; + + @Column({ + nullable: false, + type: String, + }) + duration: string; + + @Column({ + nullable: false, + type: Number, + }) + priority: number; + + @CreateDateColumn({ + type: 'timestamp with time zone', + nullable: false, + }) + createdAt: Date; + + @UpdateDateColumn({ + type: 'timestamp with time zone', + nullable: false, + }) + updatedAt: Date; +} diff --git a/src/roadmap/roadmap.module.ts b/src/roadmap/roadmap.module.ts new file mode 100644 index 0000000..4674dcf --- /dev/null +++ b/src/roadmap/roadmap.module.ts @@ -0,0 +1,23 @@ +import { HttpModule } from '@nestjs/axios'; +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { CourseModule } from 'src/course-module/course-module.entity'; +import { UserBackgroundModule } from 'src/user-background/user-background.module'; +import { RoadmapController } from './roadmap.controller'; +import { Roadmap } from './roadmap.entity'; +import { RoadmapService } from './roadmap.service'; +import { Course } from 'src/course/course.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([Roadmap, Course]), + HttpModule, + ConfigModule, + UserBackgroundModule, + CourseModule, + ], + controllers: [RoadmapController], + providers: [RoadmapService], +}) +export class RoadmapModule {} diff --git a/src/roadmap/roadmap.service.ts b/src/roadmap/roadmap.service.ts new file mode 100644 index 0000000..9dfc84a --- /dev/null +++ b/src/roadmap/roadmap.service.ts @@ -0,0 +1,196 @@ +import { HttpService } from '@nestjs/axios'; +import { + Inject, + Injectable, + InternalServerErrorException, + NotFoundException, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Course } from 'src/course/course.entity'; +import { CourseStatus } from 'src/shared/enums'; +import { createPagination } from 'src/shared/pagination'; +import { UserBackgroundService } from 'src/user-background/user-background.service'; +import { FindOneOptions, FindOptionsWhere, Repository } from 'typeorm'; +import { PaginatedRoadmapResponseDto, UpdateRoadmapDto } from './dtos'; +import { CreateRoadmapAiDto } from './dtos/create-roadmp-ai.dto'; +import { Roadmap } from './roadmap.entity'; +@Injectable() +export class RoadmapService { + constructor( + @Inject('RoadmapRepository') + private readonly roadmapRepository: Repository, + @Inject('CourseRepository') + private readonly courseRepository: Repository, + private readonly httpService: HttpService, + private readonly configService: ConfigService, + private readonly userBackgroundService: UserBackgroundService, + ) {} + + private readonly defaultRelations = { + teacher: true, + category: true, + }; + + async fetchRoadmapData( + userId: string, + createRoadmapAiDto: CreateRoadmapAiDto, + ) { + const userBackground = await this.userBackgroundService.findOneByUserId( + userId, + ); + const course = await this.courseRepository.find({ + relations: this.defaultRelations, + where: { status: CourseStatus.PUBLISHED }, + }); + try { + const requestBody = { + courses: course, + user_data: { + age: '39', + department: 'Computer Science', + interest: userBackground.topics.map((topic) => topic.title), + name: userBackground.user.fullname, + preTestDescription: createRoadmapAiDto.preTestDescription, + preTestScore: 70, + university: 'Yale', + userID: userId, + }, + }; + const response = await this.httpService.axiosRef.post( + `https://ai.edusaig.com/ai/generate-roadmap`, + requestBody, + ); + return { data: response.data }; + } catch (error) { + throw new InternalServerErrorException(error); + } + } + + async create(userId: string, createRoadmapAiDto: CreateRoadmapAiDto) { + try { + const roadmap = await this.fetchRoadmapData(userId, createRoadmapAiDto); + + await Promise.all( + roadmap.data.validated_roadmap.recommended_courses.map( + async (course) => { + const courseData = await this.courseRepository.findOne({ + where: { id: course.id }, + }); + + if (courseData) { + const existingRoadmap = await this.roadmapRepository.findOne({ + where: { + user: { id: userId }, + courses: { id: course.id }, + }, + }); + + if (!existingRoadmap) { + const requestBody = { + duration: course.duration.toString(), + priority: course.priority, + courses: [course.id], + }; + + await this.roadmapRepository.save({ + ...requestBody, + user: { id: userId }, + courses: requestBody.courses.map((courseId) => ({ + id: courseId, + })), + }); + } + } + }, + ), + ); + } catch (error) { + if (error instanceof NotFoundException) throw error; + throw new InternalServerErrorException(error.message); + } + } + + async findAll({ + userId, + page = 1, + limit = 20, + search = '', + }: { + userId?: string; + page?: number; + limit?: number; + search?: string; + }): Promise { + const { find } = await createPagination(this.roadmapRepository, { + page, + limit, + }); + + const queryBuilder = this.roadmapRepository + .createQueryBuilder('roadmap') + .leftJoinAndSelect('roadmap.user', 'user') + .leftJoinAndSelect('roadmap.courses', 'courses') + .leftJoinAndSelect('courses.teacher', 'teacher') + .leftJoinAndSelect('courses.category', 'category') + .orderBy('roadmap.priority', 'ASC'); + + if (userId) { + queryBuilder.andWhere('user.id = :userId', { userId }); + } + + if (search) { + queryBuilder.andWhere( + '(roadmap.duration ILIKE :search OR courses.title ILIKE :search)', + { search: `%${search}%` }, + ); + } + + const [data, total] = await queryBuilder + .skip((page - 1) * limit) + .take(limit) + .getManyAndCount(); + + return new PaginatedRoadmapResponseDto(data, total, limit, page); + } + + async findOne(options: FindOneOptions): Promise { + try { + const roadmap = await this.roadmapRepository.findOne(options); + if (!roadmap) throw new NotFoundException('Roadmap not found'); + return roadmap; + } catch (error) { + if (error instanceof NotFoundException) throw error; + throw new InternalServerErrorException(error.message); + } + } + + async update( + criteria: FindOptionsWhere, + updateRoadmapDto: UpdateRoadmapDto, + ): Promise { + try { + const roadmap = await this.findOne({ where: criteria }); + return await this.roadmapRepository.save({ + ...roadmap, + ...updateRoadmapDto, + courses: updateRoadmapDto.courses.map((courseId) => ({ id: courseId })), + }); + } catch (error) { + if (error instanceof NotFoundException) throw error; + throw new InternalServerErrorException(error.message); + } + } + + async delete(criteria: FindOptionsWhere): Promise { + try { + const roadmap = await this.findOne({ where: criteria }); + if (!roadmap) { + throw new NotFoundException('Roadmap not found'); + } + return await this.roadmapRepository.remove(roadmap); + } catch (error) { + if (error instanceof NotFoundException) throw error; + throw new InternalServerErrorException(error.message); + } + } +} \ No newline at end of file diff --git a/src/shared/configs/database.config.ts b/src/shared/configs/database.config.ts new file mode 100644 index 0000000..f20ba7b --- /dev/null +++ b/src/shared/configs/database.config.ts @@ -0,0 +1,64 @@ +import { ConfigService } from '@nestjs/config'; +import 'dotenv/config'; +import { Category } from 'src/category/category.entity'; +import { Chapter } from 'src/chapter/chapter.entity'; +import { ChatMessage } from 'src/chat-message/chat-message.entity'; +import { ChatRoom } from 'src/chat-room/chat-room.entity'; +import { CourseModule } from 'src/course-module/course-module.entity'; +import { Course } from 'src/course/course.entity'; +import { Enrollment } from 'src/enrollment/enrollment.entity'; +import { ExamAnswer } from 'src/exam-answer/exam-answer.entity'; +import { ExamAttempt } from 'src/exam-attempt/exam-attempt.entity'; +import { Exam } from 'src/exam/exam.entity'; +import { Progress } from 'src/progress/progress.entity'; +import { QuestionOption } from 'src/question-option/question-option.entity'; +import { Question } from 'src/question/question.entity'; +import { Reward } from 'src/reward/reward.entity'; +import { Roadmap } from 'src/roadmap/roadmap.entity'; +import { UserBackgroundTopic } from 'src/user-background-topic/user-background-topic.entity'; +import { UserBackground } from 'src/user-background/user-background.entity'; +import { UserOccupation } from 'src/user-occupation/user-occupation.entity'; +import { UserStreak } from 'src/user-streak/user-streak.entity'; +import { User } from 'src/user/user.entity'; +import { UserReward } from 'src/user-reward/user-reward.entity'; +import { DataSource, DataSourceOptions } from 'typeorm'; +import { GLOBAL_CONFIG } from '../constants/global-config.constant'; +import { Pretest } from 'src/pretest/pretest.entity'; + +const configService = new ConfigService(); + +export const databaseConfig: DataSourceOptions = { + type: 'postgres', + host: configService.get(GLOBAL_CONFIG.DB_HOST), + port: configService.get(GLOBAL_CONFIG.DB_PORT), + username: configService.get(GLOBAL_CONFIG.DB_USERNAME), + password: configService.get(GLOBAL_CONFIG.DB_PASSWORD), + database: configService.get(GLOBAL_CONFIG.DB_DATABASE), + logging: configService.get(GLOBAL_CONFIG.IS_DEVELOPMENT), + entities: [ + User, + UserStreak, + Category, + Course, + CourseModule, + Chapter, + Reward, + UserReward, + Enrollment, + Exam, + Progress, + ExamAttempt, + Question, + QuestionOption, + ExamAnswer, + UserOccupation, + UserBackgroundTopic, + UserBackground, + ChatRoom, + ChatMessage, + Roadmap, + Pretest, + ], +}; + +export default new DataSource(databaseConfig); diff --git a/src/shared/configs/dotenv.config.ts b/src/shared/configs/dotenv.config.ts new file mode 100644 index 0000000..f49d547 --- /dev/null +++ b/src/shared/configs/dotenv.config.ts @@ -0,0 +1,32 @@ +import * as Joi from 'joi'; +import { Environment } from '../enums/environment.enum'; + +export const dotenvConfig = Joi.object({ + PORT: Joi.number().required(), + NODE_ENV: Joi.string() + .valid(...Object.values(Environment)) + .default(Environment.DEVELOPMENT), + IS_DEVELOPMENT: Joi.boolean().when('NODE_ENV', { + is: Joi.equal(Environment.DEVELOPMENT), + then: Joi.boolean().default(true), + otherwise: Joi.boolean().default(false), + }), + DB_HOST: Joi.string().required(), + DB_PORT: Joi.number().required(), + DB_USERNAME: Joi.string().required(), + DB_PASSWORD: Joi.string().required(), + DB_DATABASE: Joi.string().required(), + CORS_ALLOW_ORIGIN: Joi.string().required(), + JWT_ACCESS_SECRET: Joi.string().required(), + JWT_REFRESH_SECRET: Joi.string().required(), + JWT_ACCESS_EXPIRATION: Joi.string().required(), + JWT_REFRESH_EXPIRATION: Joi.string().required(), + AWS_ACCESS_KEY_ID: Joi.string().required(), + AWS_SECRET_ACCESS_KEY: Joi.string().required(), + AWS_REGION: Joi.string().required(), + AWS_BUCKET_NAME: Joi.string().required(), + ADMIN_EMAIL: Joi.string().required(), + ADMIN_PASSWORD: Joi.string().required(), + AI_URL: Joi.string().required(), + API_URL: Joi.string().required(), +}); diff --git a/src/shared/constants/global-config.constant.ts b/src/shared/constants/global-config.constant.ts new file mode 100644 index 0000000..33b9435 --- /dev/null +++ b/src/shared/constants/global-config.constant.ts @@ -0,0 +1,23 @@ +export const GLOBAL_CONFIG = { + PORT: 'PORT', + NODE_ENV: 'NODE_ENV', + IS_DEVELOPMENT: 'IS_DEVELOPMENT', + DB_HOST: 'DB_HOST', + DB_PORT: 'DB_PORT', + DB_USERNAME: 'DB_USERNAME', + DB_PASSWORD: 'DB_PASSWORD', + DB_DATABASE: 'DB_DATABASE', + CORS_ALLOW_ORIGIN: 'CORS_ALLOW_ORIGIN', + JWT_ACCESS_SECRET: 'JWT_ACCESS_SECRET', + JWT_REFRESH_SECRET: 'JWT_REFRESH_SECRET', + JWT_ACCESS_EXPIRATION: 'JWT_ACCESS_EXPIRATION', + JWT_REFRESH_EXPIRATION: 'JWT_REFRESH_EXPIRATION', + AWS_ACCESS_KEY_ID: 'AWS_ACCESS_KEY_ID', + AWS_SECRET_ACCESS_KEY: 'AWS_SECRET_ACCESS_KEY', + AWS_REGION: 'AWS_REGION', + AWS_BUCKET_NAME: 'AWS_BUCKET_NAME', + ADMIN_EMAIL: 'ADMIN_EMAIL', + ADMIN_PASSWORD: 'ADMIN_PASSWORD', + AI_URL: 'AI_URL', + API_URL: 'API_URL', +}; diff --git a/src/shared/decorators/course-ownership.decorator.ts b/src/shared/decorators/course-ownership.decorator.ts new file mode 100644 index 0000000..1c227c8 --- /dev/null +++ b/src/shared/decorators/course-ownership.decorator.ts @@ -0,0 +1,29 @@ +import { SetMetadata } from '@nestjs/common'; + +export const COURSE_OWNERSHIP_KEY = 'courseOwnership' as const; +export const ADMIN_DRAFT_ONLY_KEY = 'adminDraftOnly' as const; + +type DecoratorFunction = ( + target: object | Function, + propertyKey?: string | symbol, + descriptor?: PropertyDescriptor, +) => any; + +export const CourseOwnership = ( + config: { adminDraftOnly?: boolean } = {}, +): DecoratorFunction => { + return ( + target: object | Function, + propertyKey?: string | symbol, + descriptor?: PropertyDescriptor, + ) => { + if (config.adminDraftOnly) { + SetMetadata(ADMIN_DRAFT_ONLY_KEY, true)(target, propertyKey, descriptor); + } + return SetMetadata(COURSE_OWNERSHIP_KEY, true)( + target, + propertyKey, + descriptor, + ); + }; +}; diff --git a/src/shared/decorators/public.decorator.ts b/src/shared/decorators/public.decorator.ts new file mode 100644 index 0000000..7659150 --- /dev/null +++ b/src/shared/decorators/public.decorator.ts @@ -0,0 +1,5 @@ +import { CustomDecorator, SetMetadata } from '@nestjs/common'; + +export const IS_PUBLIC_KEY = 'isPublic'; +export const Public = (): CustomDecorator => + SetMetadata(IS_PUBLIC_KEY, true); diff --git a/src/shared/decorators/role.decorator.ts b/src/shared/decorators/role.decorator.ts new file mode 100644 index 0000000..a2c709a --- /dev/null +++ b/src/shared/decorators/role.decorator.ts @@ -0,0 +1,5 @@ +import { SetMetadata } from '@nestjs/common'; +import { Role } from '../enums/roles.enum'; + +export const ROLES_KEY = 'roles'; +export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles); diff --git a/src/shared/enums/course-level.enum.ts b/src/shared/enums/course-level.enum.ts new file mode 100644 index 0000000..b1bbf11 --- /dev/null +++ b/src/shared/enums/course-level.enum.ts @@ -0,0 +1,5 @@ +export enum CourseLevel { + BEGINNER = 'beginner', + INTERMEDIATE = 'intermediate', + ADVANCED = 'advanced', +} diff --git a/src/shared/enums/course-status.enum.ts b/src/shared/enums/course-status.enum.ts new file mode 100644 index 0000000..9fd3908 --- /dev/null +++ b/src/shared/enums/course-status.enum.ts @@ -0,0 +1,5 @@ +export enum CourseStatus { + DRAFT = 'draft', + PUBLISHED = 'published', + ARCHIVED = 'archived', +} diff --git a/src/shared/enums/environment.enum.ts b/src/shared/enums/environment.enum.ts new file mode 100644 index 0000000..60d3494 --- /dev/null +++ b/src/shared/enums/environment.enum.ts @@ -0,0 +1,4 @@ +export enum Environment { + DEVELOPMENT = 'development', + PRODUCTION = 'production', +} diff --git a/src/shared/enums/exam-attempt-status.enum.ts b/src/shared/enums/exam-attempt-status.enum.ts new file mode 100644 index 0000000..d7416ad --- /dev/null +++ b/src/shared/enums/exam-attempt-status.enum.ts @@ -0,0 +1,6 @@ +export enum ExamAttemptStatus { + IN_PROGRESS = 'in_progress', + COMPLETED = 'completed', + FAILED = 'failed', + PASSED = 'passed', +} diff --git a/src/shared/enums/exam-status.enum.ts b/src/shared/enums/exam-status.enum.ts new file mode 100644 index 0000000..0226abc --- /dev/null +++ b/src/shared/enums/exam-status.enum.ts @@ -0,0 +1,5 @@ +export enum ExamStatus { + ARCHIVED = 'archived', + DRAFT = 'draft', + PUBLISHED = 'published', +} diff --git a/src/shared/enums/index.ts b/src/shared/enums/index.ts new file mode 100644 index 0000000..02a2722 --- /dev/null +++ b/src/shared/enums/index.ts @@ -0,0 +1,7 @@ +export * from './course-level.enum'; +export * from './course-status.enum'; +export * from './roles.enum'; +export * from './environment.enum'; +export * from './exam-status.enum'; +export * from './exam-attempt-status.enum'; +export * from './question-type.enum'; diff --git a/src/shared/enums/question-type.enum.ts b/src/shared/enums/question-type.enum.ts new file mode 100644 index 0000000..b5ce356 --- /dev/null +++ b/src/shared/enums/question-type.enum.ts @@ -0,0 +1,6 @@ +export enum QuestionType { + MULTIPLE_CHOICE = 'multiple_choice', + TRUE_FALSE = 'true_false', + ESSAY = 'essay', + PRETEST = 'pretest', +} diff --git a/src/shared/enums/roles.enum.ts b/src/shared/enums/roles.enum.ts new file mode 100644 index 0000000..cefe86d --- /dev/null +++ b/src/shared/enums/roles.enum.ts @@ -0,0 +1,10 @@ +export enum Role { + ADMIN = 'admin', + STUDENT = 'student', + TEACHER = 'teacher', +} + +export enum AvailableRoles { + STUDENT = Role.STUDENT, + TEACHER = Role.TEACHER, +} diff --git a/src/shared/guards/course-ownership.guard.ts b/src/shared/guards/course-ownership.guard.ts new file mode 100644 index 0000000..7fbe395 --- /dev/null +++ b/src/shared/guards/course-ownership.guard.ts @@ -0,0 +1,123 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Chapter } from 'src/chapter/chapter.entity'; +import { CourseModule } from 'src/course-module/course-module.entity'; +import { Course } from 'src/course/course.entity'; +import { Repository } from 'typeorm'; +import { CourseStatus, Role } from '../enums'; +import { + ADMIN_DRAFT_ONLY_KEY, + COURSE_OWNERSHIP_KEY, +} from '../decorators/course-ownership.decorator'; + +type ResourceType = 'course' | 'module' | 'chapter'; + +@Injectable() +export class CourseOwnershipGuard implements CanActivate { + private readonly pathToType: Record = { + '/course/': 'course', + '/course-module/': 'module', + '/chapter/': 'chapter', + }; + + constructor( + private reflector: Reflector, + @InjectRepository(Course) + private courseRepo: Repository, + @InjectRepository(CourseModule) + private moduleRepo: Repository, + @InjectRepository(Chapter) + private chapterRepo: Repository, + ) {} + + async canActivate(context: ExecutionContext): Promise { + if (!this.isOwnershipRequired(context)) return true; + + const { user, params, path } = this.getRequestData(context); + const course = await this.getCourse(path, params.id); + + if (!course) throw new UnauthorizedException('Course not found'); + + if (user.role === Role.ADMIN) { + return this.validateAdminAccess(context, course); + } + + if (user.role === Role.TEACHER) { + return this.validateTeacherAccess(user.id, course); + } + + throw new UnauthorizedException('Insufficient permissions'); + } + + private isOwnershipRequired(context: ExecutionContext): boolean { + return this.reflector.getAllAndOverride(COURSE_OWNERSHIP_KEY, [ + context.getHandler(), + context.getClass(), + ]); + } + + private getRequestData(context: ExecutionContext) { + const request = context.switchToHttp().getRequest(); + return { user: request.user, params: request.params, path: request.path }; + } + + private async getCourse(path: string, id: string): Promise { + const type = Object.entries(this.pathToType).find(([key]) => + path.includes(key), + )?.[1]; + + if (!type) return null; + + const queries = { + course: () => + this.courseRepo.findOne({ + where: { id }, + relations: { teacher: true }, + }), + module: () => + this.moduleRepo + .findOne({ + where: { id }, + relations: { course: { teacher: true } }, + }) + .then((m) => m?.course), + chapter: () => + this.chapterRepo + .findOne({ + where: { id }, + relations: { module: { course: { teacher: true } } }, + }) + .then((c) => c?.module?.course), + }; + + return await queries[type]().catch(() => null); + } + + private validateAdminAccess( + context: ExecutionContext, + course: Course, + ): boolean { + const adminDraftOnly = this.reflector.getAllAndOverride( + ADMIN_DRAFT_ONLY_KEY, + [context.getHandler(), context.getClass()], + ); + + if (adminDraftOnly && course.status !== CourseStatus.DRAFT) { + throw new UnauthorizedException('Admin can only access draft courses'); + } + return true; + } + + private validateTeacherAccess(userId: string, course: Course): boolean { + if (course.teacher.id !== userId) { + throw new UnauthorizedException('You can only access your own courses'); + } + return true; + } +} diff --git a/src/shared/guards/role.guard.ts b/src/shared/guards/role.guard.ts new file mode 100644 index 0000000..39dcde8 --- /dev/null +++ b/src/shared/guards/role.guard.ts @@ -0,0 +1,20 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { Role } from '../enums/roles.enum'; +import { ROLES_KEY } from '../decorators/role.decorator'; +import { AuthenticatedRequest } from 'src/auth/interfaces/authenticated-request.interface'; + +@Injectable() +export class RolesGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean { + const requiredRoles = this.reflector.getAllAndOverride(ROLES_KEY, [ + context.getHandler(), + context.getClass(), + ]); + if (!requiredRoles) return true; + const { user } = context.switchToHttp().getRequest(); + return requiredRoles.some((role) => role === user.role); + } +} diff --git a/src/shared/pagination/dtos/paginate-meta.dto.ts b/src/shared/pagination/dtos/paginate-meta.dto.ts new file mode 100644 index 0000000..51a4908 --- /dev/null +++ b/src/shared/pagination/dtos/paginate-meta.dto.ts @@ -0,0 +1,56 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class PaginationMetaDto { + @ApiProperty({ + description: 'Total number of items', + type: Number, + example: 100, + }) + total: number; + + @ApiProperty({ + description: 'Number of items per page', + type: Number, + example: 10, + }) + pageSize: number; + + @ApiProperty({ + description: 'Current page number', + type: Number, + example: 1, + }) + currentPage: number; + + @ApiProperty({ + description: 'Next page number (null if no next page)', + type: Number, + nullable: true, + example: 2, + }) + nextPage: number | null; + + @ApiProperty({ + description: 'Previous page number (null if no previous page)', + type: Number, + nullable: true, + example: null, + }) + prevPage: number | null; + + @ApiProperty({ + description: 'Last page number', + type: Number, + example: 10, + }) + lastPage: number; + + constructor(total: number, pageSize: number, currentPage: number) { + this.total = total; + this.pageSize = pageSize; + this.currentPage = currentPage; + this.lastPage = Math.ceil(total / pageSize); + this.nextPage = currentPage < this.lastPage ? currentPage + 1 : null; + this.prevPage = currentPage > 1 ? currentPage - 1 : null; + } +} diff --git a/src/shared/pagination/dtos/paginate-query.dto.ts b/src/shared/pagination/dtos/paginate-query.dto.ts new file mode 100644 index 0000000..62ca12a --- /dev/null +++ b/src/shared/pagination/dtos/paginate-query.dto.ts @@ -0,0 +1,39 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { IsInt, IsOptional, IsString, Min } from 'class-validator'; + +export class PaginateQueryDto { + @ApiProperty({ + required: false, + default: 1, + description: 'Page number', + type: Number, + }) + @IsOptional() + @Transform(({ value }) => parseInt(value, 10)) + @IsInt() + @Min(1) + page: number = 1; + + @ApiProperty({ + required: false, + default: 10, + description: 'Items per page', + type: Number, + }) + @IsOptional() + @Transform(({ value }) => parseInt(value, 10)) + @IsInt() + @Min(1) + limit: number = 10; + + @ApiProperty({ + required: false, + default: '', + description: 'Search', + type: String, + }) + @IsOptional() + @IsString() + search: string = ''; +} diff --git a/src/shared/pagination/dtos/paginate-response.dto.ts b/src/shared/pagination/dtos/paginate-response.dto.ts new file mode 100644 index 0000000..208ae1d --- /dev/null +++ b/src/shared/pagination/dtos/paginate-response.dto.ts @@ -0,0 +1,35 @@ +import { Type } from '@nestjs/common'; +import { ApiProperty } from '@nestjs/swagger'; +import { PaginationMetaDto } from './paginate-meta.dto'; + +export interface IBaseEntity { + id: string; +} + +export function PaginatedResponse(ItemType: Type) { + class PaginatedResponseClass { + @ApiProperty({ + description: 'Array of items', + type: [ItemType], + }) + data: T[]; + + @ApiProperty({ + description: 'Pagination metadata', + type: PaginationMetaDto, + }) + meta: PaginationMetaDto; + + constructor( + items: T[], + total: number, + pageSize: number, + currentPage: number, + ) { + this.data = items; + this.meta = new PaginationMetaDto(total, pageSize, currentPage); + } + } + + return PaginatedResponseClass; +} diff --git a/src/shared/pagination/index.ts b/src/shared/pagination/index.ts new file mode 100644 index 0000000..aa5052b --- /dev/null +++ b/src/shared/pagination/index.ts @@ -0,0 +1 @@ +export * from './typeorm'; diff --git a/src/shared/pagination/typeorm.ts b/src/shared/pagination/typeorm.ts new file mode 100644 index 0000000..5276303 --- /dev/null +++ b/src/shared/pagination/typeorm.ts @@ -0,0 +1,61 @@ +import { BaseEntity, FindManyOptions, Repository } from 'typeorm'; + +class TypeORMPagination { + constructor( + private readonly repository: Repository, + private readonly queryOptions: FindManyOptions, + private readonly page: number, + private readonly limit: number, + ) {} + + protected format(data: [T[], number]) { + const [result, total] = data; + const lastPage = Math.ceil(total / this.limit); + const nextPage = this.page + 1 > lastPage ? null : this.page + 1; + const prevPage = this.page - 1 < 1 ? null : this.page - 1; + + const meta = { + total, + pageSize: Math.min(this.limit, total), + currentPage: this.page, + nextPage: nextPage, + prevPage: prevPage, + lastPage: lastPage, + }; + + return { + data: result, + meta, + }; + } + + async run() { + const data = await this.repository.findAndCount(this.queryOptions); + return this.format(data); + } +} +type Options = { + readonly page?: number; + readonly limit?: number; +}; +export async function createPagination( + repository: Repository, + { page = 1, limit = 20 }: Options, +) { + const _take = limit || 10; + const _page = page || 1; + const _skip = (_page - 1) * _take; + + const find = (options: FindManyOptions) => { + return new TypeORMPagination( + repository, + Object.assign(options, { + take: _take, + skip: _skip, + }), + _page, + _take, + ); + }; + return { find }; +} diff --git a/src/user-background-topic/dtos/create-user-background-topic.dto.ts b/src/user-background-topic/dtos/create-user-background-topic.dto.ts new file mode 100644 index 0000000..1a81a34 --- /dev/null +++ b/src/user-background-topic/dtos/create-user-background-topic.dto.ts @@ -0,0 +1,27 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsNotEmpty } from 'class-validator'; +import { UserBackgroundTopicLevel } from '../enums/user-background-topic-level.enum'; + +export class CreateUserBackgroundTopicDto { + @IsNotEmpty() + @ApiProperty({ + description: 'Title', + type: String, + }) + title: string; + + @IsNotEmpty() + @ApiProperty({ + description: 'Description', + type: String, + }) + description: string; + + @IsEnum(UserBackgroundTopicLevel) + @ApiProperty({ + description: 'Level', + enum: UserBackgroundTopicLevel, + default: UserBackgroundTopicLevel.BEGINNER, + }) + level: UserBackgroundTopicLevel; +} diff --git a/src/user-background-topic/dtos/update-user-background-topic.dto.ts b/src/user-background-topic/dtos/update-user-background-topic.dto.ts new file mode 100644 index 0000000..6410bf6 --- /dev/null +++ b/src/user-background-topic/dtos/update-user-background-topic.dto.ts @@ -0,0 +1,6 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateUserBackgroundTopicDto } from './create-user-background-topic.dto'; + +export class UpdateUserBackgroundTopicDto extends PartialType( + CreateUserBackgroundTopicDto, +) {} diff --git a/src/user-background-topic/dtos/user-background-response.dto.ts b/src/user-background-topic/dtos/user-background-response.dto.ts new file mode 100644 index 0000000..0be2a16 --- /dev/null +++ b/src/user-background-topic/dtos/user-background-response.dto.ts @@ -0,0 +1,72 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { PaginatedResponse } from 'src/shared/pagination/dtos/paginate-response.dto'; +import { UserBackgroundTopicLevel } from '../enums/user-background-topic-level.enum'; +import { UserBackgroundTopic } from '../user-background-topic.entity'; + +export class UserBackgroundTopicResponseDto { + @ApiProperty({ + description: 'User background topic ID', + type: String, + }) + id: string; + + @ApiProperty({ + description: 'User Background Title', + type: String, + }) + title: string; + + @ApiProperty({ + description: 'User Background Description', + type: String, + }) + description: string; + + @ApiProperty({ + description: 'User Background Level', + enum: UserBackgroundTopicLevel, + }) + level: UserBackgroundTopicLevel; + + @ApiProperty({ + description: 'Created date', + type: Date, + }) + createdAt: Date; + + @ApiProperty({ + description: 'Updated date', + type: Date, + }) + updatedAt: Date; + + constructor(userBackgroundTopic: UserBackgroundTopic) { + this.id = userBackgroundTopic.id; + this.title = userBackgroundTopic.title; + this.description = userBackgroundTopic.description; + this.level = userBackgroundTopic.level; + this.createdAt = userBackgroundTopic.createdAt; + this.updatedAt = userBackgroundTopic.updatedAt; + } +} + +export class PaginatedUserBackgroundTopicResponseDto extends PaginatedResponse( + UserBackgroundTopicResponseDto, +) { + constructor( + userBackgroundTopics: UserBackgroundTopic[], + total: number, + page: number, + limit: number, + ) { + super( + userBackgroundTopics.map( + (userBackgroundTopic) => + new UserBackgroundTopicResponseDto(userBackgroundTopic), + ), + total, + page, + limit, + ); + } +} diff --git a/src/user-background-topic/enums/user-background-topic-level.enum.ts b/src/user-background-topic/enums/user-background-topic-level.enum.ts new file mode 100644 index 0000000..578acc3 --- /dev/null +++ b/src/user-background-topic/enums/user-background-topic-level.enum.ts @@ -0,0 +1,5 @@ +export enum UserBackgroundTopicLevel { + BEGINNER = 'beginner', + INTERMEDIATE = 'intermediate', + ADVANCED = 'advanced', +} diff --git a/src/user-background-topic/user-background-topic.controller.ts b/src/user-background-topic/user-background-topic.controller.ts new file mode 100644 index 0000000..8338ea9 --- /dev/null +++ b/src/user-background-topic/user-background-topic.controller.ts @@ -0,0 +1,136 @@ +import { + Body, + Controller, + Delete, + Get, + HttpStatus, + Injectable, + Param, + ParseUUIDPipe, + Patch, + Post, + Query, +} from '@nestjs/common'; +import { + ApiBearerAuth, + ApiParam, + ApiQuery, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; +import { Roles } from 'src/shared/decorators/role.decorator'; +import { Role } from 'src/shared/enums'; +import { PaginateQueryDto } from 'src/shared/pagination/dtos/paginate-query.dto'; +import { CreateUserBackgroundTopicDto } from './dtos/create-user-background-topic.dto'; +import { UpdateUserBackgroundTopicDto } from './dtos/update-user-background-topic.dto'; +import { + PaginatedUserBackgroundTopicResponseDto, + UserBackgroundTopicResponseDto, +} from './dtos/user-background-response.dto'; +import { UserBackgroundTopicService } from './user-background-topic.service'; +import { Public } from 'src/shared/decorators/public.decorator'; + +@Controller('user-background-topic') +@ApiTags('User Background Topic') +@ApiBearerAuth() +@Injectable() +export class UserBackgroundTopicController { + constructor( + private readonly userBackgroundTopicService: UserBackgroundTopicService, + ) {} + + @Get() + @ApiResponse({ + status: HttpStatus.OK, + type: UserBackgroundTopicResponseDto, + description: 'Get all user background topics', + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + async findAll( + @Query() { page, limit }: PaginateQueryDto, + ): Promise { + return this.userBackgroundTopicService.findAll({ page, limit }); + } + + @Get(':id') + @ApiResponse({ + status: HttpStatus.OK, + type: UserBackgroundTopicResponseDto, + description: 'Get a user background topic by ID', + }) + @ApiParam({ + name: 'id', + type: String, + description: 'User background topic ID', + }) + async findOne( + @Param('id', new ParseUUIDPipe()) id: string, + ): Promise { + return this.userBackgroundTopicService.findOne(id, { where: { id } }); + } + + @Post() + @Roles(Role.ADMIN) + @Public() + @ApiResponse({ + status: HttpStatus.CREATED, + type: UserBackgroundTopicResponseDto, + description: 'Create a user background topic', + }) + async create( + @Body() createUserBackgroundTopicDto: CreateUserBackgroundTopicDto, + ): Promise { + return this.userBackgroundTopicService.create(createUserBackgroundTopicDto); + } + + @Patch(':id') + @Roles(Role.ADMIN) + @ApiResponse({ + status: HttpStatus.OK, + type: UserBackgroundTopicResponseDto, + description: 'Update a user background topic by ID', + }) + @ApiParam({ + name: 'id', + type: String, + description: 'User background topic ID', + }) + async update( + @Param('id', new ParseUUIDPipe()) id: string, + @Body() updateUserBackgroundTopicDto: UpdateUserBackgroundTopicDto, + ): Promise { + return this.userBackgroundTopicService.update( + id, + updateUserBackgroundTopicDto, + ); + } + + @Delete(':id') + @Roles(Role.ADMIN) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Delete a user background topic by ID', + }) + @ApiParam({ + name: 'id', + type: String, + description: 'User background topic ID', + }) + async remove( + @Param('id', new ParseUUIDPipe()) id: string, + ): Promise { + return this.userBackgroundTopicService.remove(id); + } +} diff --git a/src/user-background-topic/user-background-topic.entity.ts b/src/user-background-topic/user-background-topic.entity.ts new file mode 100644 index 0000000..5a8ccb2 --- /dev/null +++ b/src/user-background-topic/user-background-topic.entity.ts @@ -0,0 +1,52 @@ +import { UserBackground } from 'src/user-background/user-background.entity'; +import { + Column, + CreateDateColumn, + Entity, + ManyToMany, + OneToMany, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; +import { UserBackgroundTopicLevel } from './enums/user-background-topic-level.enum'; + +@Entity() +export class UserBackgroundTopic { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ + type: String, + nullable: false, + }) + title: string; + + @Column({ + type: String, + nullable: false, + }) + description: string; + + @Column({ + type: 'enum', + enum: UserBackgroundTopicLevel, + nullable: false, + default: UserBackgroundTopicLevel.BEGINNER, + }) + level: UserBackgroundTopicLevel; + + @ManyToMany(() => UserBackground, (background) => background.topics) + userBackgrounds: UserBackground[]; + + @CreateDateColumn({ + type: 'timestamp', + name: 'created_at', + }) + createdAt: Date; + + @UpdateDateColumn({ + type: 'timestamp', + name: 'updated_at', + }) + updatedAt: Date; +} diff --git a/src/user-background-topic/user-background-topic.module.ts b/src/user-background-topic/user-background-topic.module.ts new file mode 100644 index 0000000..0647926 --- /dev/null +++ b/src/user-background-topic/user-background-topic.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { UserBackgroundTopicController } from './user-background-topic.controller'; +import { UserBackgroundTopic } from './user-background-topic.entity'; +import { UserBackgroundTopicService } from './user-background-topic.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([UserBackgroundTopic])], + controllers: [UserBackgroundTopicController], + providers: [UserBackgroundTopicService], + exports: [UserBackgroundTopicService] +}) +export class UserBackgroundTopicModule {} diff --git a/src/user-background-topic/user-background-topic.service.ts b/src/user-background-topic/user-background-topic.service.ts new file mode 100644 index 0000000..2419598 --- /dev/null +++ b/src/user-background-topic/user-background-topic.service.ts @@ -0,0 +1,94 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { createPagination } from 'src/shared/pagination'; +import { FindOneOptions, FindOptionsWhere, Repository } from 'typeorm'; +import { CreateUserBackgroundTopicDto } from './dtos/create-user-background-topic.dto'; +import { UpdateUserBackgroundTopicDto } from './dtos/update-user-background-topic.dto'; +import { + PaginatedUserBackgroundTopicResponseDto, + UserBackgroundTopicResponseDto, +} from './dtos/user-background-response.dto'; +import { UserBackgroundTopic } from './user-background-topic.entity'; + +@Injectable() +export class UserBackgroundTopicService { + constructor( + @InjectRepository(UserBackgroundTopic) + private readonly userBackgroundTopicRepository: Repository, + ) {} + + async findAll({ + page = 1, + limit = 20, + }: { + page?: number; + limit?: number; + }): Promise { + const { find } = await createPagination( + this.userBackgroundTopicRepository, + { + page, + limit, + }, + ); + + const userBackgroundTopics = await find({}).run(); + + return userBackgroundTopics; + } + + async findOne( + id: string, + options: FindOneOptions, + ): Promise { + const baseWhere = options.where as FindOptionsWhere; + const whereCondition = { ...baseWhere, id }; + + const userBackgroundTopic = + await this.userBackgroundTopicRepository.findOne({ + where: whereCondition, + }); + + if (!userBackgroundTopic) { + throw new NotFoundException('User Background Topic not found'); + } + + return userBackgroundTopic; + } + + async create( + createUserBackgroundTopicDto: CreateUserBackgroundTopicDto, + ): Promise { + const userBackgroundTopic = this.userBackgroundTopicRepository.create( + createUserBackgroundTopicDto, + ); + + await this.userBackgroundTopicRepository.save(userBackgroundTopic); + + return userBackgroundTopic; + } + + async update( + id: string, + updateUserBackgroundTopicDto: UpdateUserBackgroundTopicDto, + ): Promise { + const userBackgroundTopic = await this.findOne(id, { where: { id } }); + + this.userBackgroundTopicRepository.merge( + userBackgroundTopic, + updateUserBackgroundTopicDto, + ); + + await this.userBackgroundTopicRepository.save(userBackgroundTopic); + + return userBackgroundTopic; + } + + async remove(id: string): Promise { + const userBackgroundTopic = await this.findOne(id, { where: { id } }); + + await this.userBackgroundTopicRepository.remove(userBackgroundTopic); + + return userBackgroundTopic; + } +} diff --git a/src/user-background/dtos/create-user-background.dto.ts b/src/user-background/dtos/create-user-background.dto.ts new file mode 100644 index 0000000..3878db5 --- /dev/null +++ b/src/user-background/dtos/create-user-background.dto.ts @@ -0,0 +1,29 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsNotEmpty, IsUUID } from 'class-validator'; + +export class CreateUserBackground { + @IsNotEmpty() + @IsUUID() + @ApiProperty({ + description: 'User ID', + type: String, + }) + userId: string; + + @IsNotEmpty() + @IsUUID() + @ApiProperty({ + description: 'Occupation ID', + type: String, + }) + occupationId: string; + + @IsArray() + @IsUUID(undefined, { each: true }) + @ApiProperty({ + description: 'Topic IDs', + type: [String], + default: [], + }) + topics: string[]; +} diff --git a/src/user-background/dtos/update-user-background.dto.ts b/src/user-background/dtos/update-user-background.dto.ts new file mode 100644 index 0000000..d0592bc --- /dev/null +++ b/src/user-background/dtos/update-user-background.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateUserBackground } from './create-user-background.dto'; + +export class UpdateUserBackground extends PartialType(CreateUserBackground) {} diff --git a/src/user-background/dtos/user-background-response.dto.ts b/src/user-background/dtos/user-background-response.dto.ts new file mode 100644 index 0000000..fc3bffa --- /dev/null +++ b/src/user-background/dtos/user-background-response.dto.ts @@ -0,0 +1,72 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { PaginatedResponse } from 'src/shared/pagination/dtos/paginate-response.dto'; +import { UserBackgroundTopicResponseDto } from 'src/user-background-topic/dtos/user-background-response.dto'; +import { UserOccupationResponseDto } from 'src/user-occupation/dtos/user-occupation-response.dto'; +import { UserResponseDto } from 'src/user/dtos/user-response.dto'; +import { UserBackground } from '../user-background.entity'; + +export class UserBackgroundResponseDto { + @ApiProperty({ + description: 'User background ID', + type: String, + }) + id: string; + + @ApiProperty({ + description: 'User data', + type: UserResponseDto, + }) + user: UserResponseDto; + + @ApiProperty({ + description: 'User occupation data', + type: UserOccupationResponseDto, + }) + occupation: UserOccupationResponseDto; + + @ApiProperty({ + description: 'User background topics', + type: UserBackgroundTopicResponseDto, + isArray: true, + }) + topics: UserBackgroundTopicResponseDto[]; + + @ApiProperty({ + description: 'Created date', + type: Date, + }) + createdAt: Date; + + @ApiProperty({ + description: 'Updated date', + type: Date, + }) + updatedAt: Date; + + constructor(data: UserBackground) { + this.id = data.id; + this.user = data.user; + this.occupation = data.occupation; + this.topics = data.topics; + this.createdAt = data.createdAt; + this.updatedAt = data.updatedAt; + } +} + +export class PaginatedUserBackgroundResponseDto extends PaginatedResponse( + UserBackgroundResponseDto, +) { + constructor( + userBackground: UserBackground[], + total: number, + page: number, + limit: number, + ) { + super( + userBackground.map((item) => new UserBackgroundResponseDto(item)), + total, + page, + limit, + ); + } +} diff --git a/src/user-background/user-background.controller.ts b/src/user-background/user-background.controller.ts new file mode 100644 index 0000000..b36c643 --- /dev/null +++ b/src/user-background/user-background.controller.ts @@ -0,0 +1,135 @@ +import { + Body, + Controller, + Delete, + Get, + HttpStatus, + Injectable, + Param, + ParseUUIDPipe, + Patch, + Post, + Query, + Req, +} from '@nestjs/common'; +import { + ApiBearerAuth, + ApiParam, + ApiQuery, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; +import { Roles } from 'src/shared/decorators/role.decorator'; +import { Role } from 'src/shared/enums'; +import { PaginateQueryDto } from 'src/shared/pagination/dtos/paginate-query.dto'; +import { CreateUserBackground } from './dtos/create-user-background.dto'; +import { UpdateUserBackground } from './dtos/update-user-background.dto'; +import { + PaginatedUserBackgroundResponseDto, + UserBackgroundResponseDto, +} from './dtos/user-background-response.dto'; +import { UserBackgroundService } from './user-background.service'; + +@Controller('user-background') +@ApiTags('User Background') +@ApiBearerAuth() +@Injectable() +export class UserBackgroundController { + constructor( + private readonly userBackgroundService: UserBackgroundService, + ) {} + + @Get() + @ApiResponse({ + status: HttpStatus.OK, + type: UserBackgroundResponseDto, + description: 'Get all user backgrounds', + isArray: true, + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @Roles(Role.ADMIN) + async findAll( + @Query() query: PaginateQueryDto, + ): Promise { + return this.userBackgroundService.findAll(query); + } + + @Get(':id') + @ApiResponse({ + status: HttpStatus.OK, + type: UserBackgroundResponseDto, + description: 'Get user background by ID', + }) + @ApiParam({ + name: 'id', + type: String, + description: 'User background ID', + }) + @Roles(Role.ADMIN) + async findOne( + @Param('id', ParseUUIDPipe) id: string, + ): Promise { + return this.userBackgroundService.findOne(id, { where: { id } }); + } + + @Post() + @ApiResponse({ + status: HttpStatus.CREATED, + type: UserBackgroundResponseDto, + description: 'Create a user background', + }) + @Roles(Role.STUDENT) + async create( + @Body() data: CreateUserBackground, + ): Promise { + return this.userBackgroundService.create(data); + } + + @Patch(':id') + @ApiResponse({ + status: HttpStatus.OK, + type: UserBackgroundResponseDto, + description: 'Update user background by ID', + }) + @ApiParam({ + name: 'id', + type: String, + description: 'User background ID', + }) + @Roles(Role.ADMIN) + async update( + @Param('id', ParseUUIDPipe) id: string, + @Body() data: UpdateUserBackground, + ): Promise { + return this.userBackgroundService.update(id, data); + } + + @Delete(':id') + @ApiResponse({ + status: HttpStatus.OK, + type: UserBackgroundResponseDto, + description: 'Delete user background by ID', + }) + @ApiParam({ + name: 'id', + type: String, + description: 'User background ID', + }) + @Roles(Role.ADMIN) + async remove( + @Param('id', new ParseUUIDPipe()) id: string, + ): Promise { + return this.userBackgroundService.remove(id); + } +} diff --git a/src/user-background/user-background.entity.ts b/src/user-background/user-background.entity.ts new file mode 100644 index 0000000..aa91505 --- /dev/null +++ b/src/user-background/user-background.entity.ts @@ -0,0 +1,52 @@ +import { UserBackgroundTopic } from 'src/user-background-topic/user-background-topic.entity'; +import { UserOccupation } from 'src/user-occupation/user-occupation.entity'; +import { User } from 'src/user/user.entity'; +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + JoinTable, + ManyToMany, + ManyToOne, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity() +export class UserBackground { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @Column({ name: 'user_id' }) + userId: string; + + @ManyToMany(() => UserBackgroundTopic, (topic) => topic.userBackgrounds) + @JoinTable({ + name: 'user_background_topics_mapping' + }) + topics: UserBackgroundTopic[]; + + @ManyToOne(() => UserOccupation, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'occupation_id' }) + occupation: UserOccupation; + + @Column({ name: 'occupation_id' }) + occupationId: string; + + @CreateDateColumn({ + type: 'timestamp', + name: 'created_at', + }) + createdAt: Date; + + @UpdateDateColumn({ + type: 'timestamp', + name: 'updated_at', + }) + updatedAt: Date; +} diff --git a/src/user-background/user-background.module.ts b/src/user-background/user-background.module.ts new file mode 100644 index 0000000..988ae90 --- /dev/null +++ b/src/user-background/user-background.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { UserBackgroundTopicModule } from 'src/user-background-topic/user-background-topic.module'; +import { UserBackgroundController } from './user-background.controller'; +import { UserBackground } from './user-background.entity'; +import { UserBackgroundService } from './user-background.service'; +@Module({ + imports: [ + TypeOrmModule.forFeature([UserBackground]), + UserBackgroundTopicModule, + ], + controllers: [UserBackgroundController], + providers: [UserBackgroundService], + exports: [UserBackgroundService], +}) +export class UserBackgroundModule {} diff --git a/src/user-background/user-background.service.ts b/src/user-background/user-background.service.ts new file mode 100644 index 0000000..89fe4e8 --- /dev/null +++ b/src/user-background/user-background.service.ts @@ -0,0 +1,143 @@ +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { createPagination } from 'src/shared/pagination'; +import { UserBackgroundTopicService } from 'src/user-background-topic/user-background-topic.service'; +import { + DeepPartial, + FindOneOptions, + FindOptionsWhere, + Repository, +} from 'typeorm'; +import { CreateUserBackground } from './dtos/create-user-background.dto'; +import { UpdateUserBackground } from './dtos/update-user-background.dto'; +import { + PaginatedUserBackgroundResponseDto, + UserBackgroundResponseDto, +} from './dtos/user-background-response.dto'; +import { UserBackground } from './user-background.entity'; + +@Injectable() +export class UserBackgroundService { + constructor( + @InjectRepository(UserBackground) + private readonly userBackgroundRepository: Repository, + private readonly userBackgroundTopicService: UserBackgroundTopicService, + ) {} + + async findAll({ + page = 1, + limit = 20, + }: { + page?: number; + limit?: number; + }): Promise { + const { find } = await createPagination(this.userBackgroundRepository, { + page, + limit, + }); + + const userBackground = await find({ + relations: { + user: true, + occupation: true, + topics: true, + }, + }).run(); + + return userBackground; + } + + async findOne( + id: string, + options: FindOneOptions, + ): Promise { + const baseWhere = options.where as FindOptionsWhere; + const whereCondition = { ...baseWhere, id }; + + const userBackground = await this.userBackgroundRepository.findOne({ + where: whereCondition, + relations: { + user: true, + occupation: true, + topics: true, + }, + }); + + if (!userBackground) { + throw new NotFoundException('User background not found'); + } + + return userBackground; + } + + async create(data: CreateUserBackground): Promise { + const topics = await Promise.all( + data.topics.map(async (topic) => { + return await this.userBackgroundTopicService.findOne(topic, { + where: { + id: topic, + }, + }); + }), + ); + const userBackground = this.userBackgroundRepository.create({ + userId: data.userId, + occupationId: data.occupationId, + topics, + }); + + const savedUserBackground = await this.userBackgroundRepository.save( + userBackground, + ); + return savedUserBackground; + } + + async update( + id: string, + data: UpdateUserBackground, + ): Promise { + const userBackground = await this.userBackgroundRepository.findOne({ + where: { id }, + }); + + const updateData: DeepPartial = { + userId: data.userId, + occupationId: data.occupationId, + }; + + this.userBackgroundRepository.merge(userBackground, updateData); + const savedUserBackground = await this.userBackgroundRepository.save( + userBackground, + ); + + return savedUserBackground; + } + + async remove(id: string): Promise { + const userBackground = await this.findOne(id, { + where: { id }, + }); + + await this.userBackgroundRepository.remove(userBackground); + + return userBackground; + } + + async findOneByUserId(userId: string): Promise { + const userBackground = this.userBackgroundRepository.findOne({ + where: { user: { id: userId } }, + order: { updatedAt: 'DESC' }, + relations: { + user: true, + topics: true, + occupation: true, + }, + }); + if (!userBackground) throw new NotFoundException('Not found this user'); + return userBackground; + } +} diff --git a/src/user-occupation/dtos/create-user-occupation.dto.ts b/src/user-occupation/dtos/create-user-occupation.dto.ts new file mode 100644 index 0000000..b48bb31 --- /dev/null +++ b/src/user-occupation/dtos/create-user-occupation.dto.ts @@ -0,0 +1,22 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class CreateUserOccupationDto { + @IsString() + @IsNotEmpty() + @ApiProperty({ + description: 'Occupation title', + type: String, + example: 'Software Engineer', + }) + title: string; + + @IsString() + @IsNotEmpty() + @ApiProperty({ + description: 'Occupation description', + type: String, + example: 'Software Engineer', + }) + description: string; +} diff --git a/src/user-occupation/dtos/update-user-occupation.dto.ts b/src/user-occupation/dtos/update-user-occupation.dto.ts new file mode 100644 index 0000000..c8a2857 --- /dev/null +++ b/src/user-occupation/dtos/update-user-occupation.dto.ts @@ -0,0 +1,6 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateUserOccupationDto } from './create-user-occupation.dto'; + +export class UpdateUserOccupationDto extends PartialType( + CreateUserOccupationDto, +) {} diff --git a/src/user-occupation/dtos/user-occupation-response.dto.ts b/src/user-occupation/dtos/user-occupation-response.dto.ts new file mode 100644 index 0000000..0bd3d5f --- /dev/null +++ b/src/user-occupation/dtos/user-occupation-response.dto.ts @@ -0,0 +1,64 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { PaginatedResponse } from 'src/shared/pagination/dtos/paginate-response.dto'; +import { UserOccupation } from '../user-occupation.entity'; + +export class UserOccupationResponseDto { + @ApiProperty({ + description: 'User occupation ID', + type: String, + }) + id: string; + + @ApiProperty({ + description: 'Occupation title', + type: String, + }) + title: string; + + @ApiProperty({ + description: 'Occupation description', + type: String, + }) + description: string; + + @ApiProperty({ + description: 'Created date', + type: Date, + }) + createdAt: Date; + + @ApiProperty({ + description: 'Updated date', + type: Date, + }) + updatedAt: Date; + + constructor(userOccupation: UserOccupation) { + this.id = userOccupation.id; + this.title = userOccupation.title; + this.description = userOccupation.description; + this.createdAt = userOccupation.createdAt; + this.updatedAt = userOccupation.updatedAt; + } +} + +export class PaginatedUserOccupationResponseDto extends PaginatedResponse( + UserOccupationResponseDto, +) { + constructor( + userOccupations: UserOccupation[], + total: number, + page: number, + limit: number, + ) { + super( + userOccupations.map( + (userOccupation: UserOccupation) => + new UserOccupationResponseDto(userOccupation), + ), + total, + page, + limit, + ); + } +} diff --git a/src/user-occupation/user-occupation.controller.ts b/src/user-occupation/user-occupation.controller.ts new file mode 100644 index 0000000..6fd55c6 --- /dev/null +++ b/src/user-occupation/user-occupation.controller.ts @@ -0,0 +1,146 @@ +import { + Body, + Controller, + Delete, + Get, + HttpStatus, + Injectable, + Param, + ParseUUIDPipe, + Patch, + Post, + Query, + Req, +} from '@nestjs/common'; +import { + ApiBearerAuth, + ApiParam, + ApiQuery, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; +import { Roles } from 'src/shared/decorators/role.decorator'; +import { Role } from 'src/shared/enums'; +import { PaginateQueryDto } from 'src/shared/pagination/dtos/paginate-query.dto'; +import { CreateUserOccupationDto } from './dtos/create-user-occupation.dto'; +import { UpdateUserOccupationDto } from './dtos/update-user-occupation.dto'; +import { + PaginatedUserOccupationResponseDto, + UserOccupationResponseDto, +} from './dtos/user-occupation-response.dto'; +import { UserOccupationService } from './user-occupation.service'; +import { Public } from 'src/shared/decorators/public.decorator'; + +@Controller('user-occupation') +@ApiTags('User Occupation') +@ApiBearerAuth() +@Injectable() +export class UserOccupationController { + constructor(private readonly userOccupationService: UserOccupationService) {} + + @Post() + @Roles(Role.ADMIN) + @ApiResponse({ + status: HttpStatus.CREATED, + type: UserOccupationResponseDto, + description: 'Create user occupation', + }) + async create( + @Body() createUserOccupationDto: CreateUserOccupationDto, + ): Promise { + const userOccupation = await this.userOccupationService.create( + createUserOccupationDto, + ); + + return new UserOccupationResponseDto(userOccupation); + } + + @Get() + @ApiResponse({ + status: HttpStatus.OK, + type: PaginatedUserOccupationResponseDto, + description: 'Get all user occupations', + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + async findAll( + @Query() query: PaginateQueryDto, + ): Promise { + return this.userOccupationService.findAll(query); + } + + @Get(':id') + @ApiResponse({ + status: HttpStatus.OK, + type: UserOccupationResponseDto, + description: 'Get user occupation by ID', + }) + @ApiParam({ + name: 'id', + type: String, + description: 'User occupation ID', + }) + async findOne( + @Param('id', new ParseUUIDPipe()) id: string, + ): Promise { + const userOccupation = await this.userOccupationService.findOne(id, { + where: { id }, + }); + + return new UserOccupationResponseDto(userOccupation); + } + + @Patch(':id') + @Roles(Role.ADMIN) + @ApiResponse({ + status: HttpStatus.OK, + type: UserOccupationResponseDto, + description: 'Update user occupation by ID', + }) + @ApiParam({ + name: 'id', + type: String, + description: 'User occupation ID', + }) + async update( + @Param('id', new ParseUUIDPipe()) id: string, + @Body() updateUserOccupationDto: UpdateUserOccupationDto, + ): Promise { + const userOccupation = await this.userOccupationService.update( + id, + updateUserOccupationDto, + ); + + return new UserOccupationResponseDto(userOccupation); + } + + @Delete(':id') + @Roles(Role.ADMIN) + @ApiResponse({ + status: HttpStatus.OK, + type: UserOccupationResponseDto, + description: 'Delete user occupation by ID', + }) + @ApiParam({ + name: 'id', + type: String, + description: 'User occupation ID', + }) + async remove( + @Param('id', new ParseUUIDPipe()) id: string, + ): Promise { + const userOccupation = await this.userOccupationService.remove(id); + + return new UserOccupationResponseDto(userOccupation); + } +} diff --git a/src/user-occupation/user-occupation.entity.ts b/src/user-occupation/user-occupation.entity.ts new file mode 100644 index 0000000..125266e --- /dev/null +++ b/src/user-occupation/user-occupation.entity.ts @@ -0,0 +1,44 @@ +import { UserBackground } from 'src/user-background/user-background.entity'; +import { + Column, + CreateDateColumn, + Entity, + OneToMany, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity() +export class UserOccupation { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ + type: String, + nullable: false, + }) + title: string; + + @Column({ + type: String, + nullable: false, + }) + description: string; + + @OneToMany(() => UserBackground, (background) => background.occupation) + backgrounds: UserBackground[]; + + @CreateDateColumn({ + name: 'created_at', + type: 'timestamp', + default: new Date(), + }) + createdAt: Date; + + @UpdateDateColumn({ + name: 'updated_at', + type: 'timestamp', + default: new Date(), + }) + updatedAt: Date; +} diff --git a/src/user-occupation/user-occupation.module.ts b/src/user-occupation/user-occupation.module.ts new file mode 100644 index 0000000..cc994d1 --- /dev/null +++ b/src/user-occupation/user-occupation.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { UserOccupationController } from './user-occupation.controller'; +import { UserOccupation } from './user-occupation.entity'; +import { UserOccupationService } from './user-occupation.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([UserOccupation])], + controllers: [UserOccupationController], + providers: [UserOccupationService], +}) +export class UserOccupationModule {} diff --git a/src/user-occupation/user-occupation.service.ts b/src/user-occupation/user-occupation.service.ts new file mode 100644 index 0000000..d20905a --- /dev/null +++ b/src/user-occupation/user-occupation.service.ts @@ -0,0 +1,83 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { createPagination } from 'src/shared/pagination'; +import { FindOneOptions, FindOptionsWhere, Repository } from 'typeorm'; +import { CreateUserOccupationDto } from './dtos/create-user-occupation.dto'; +import { UpdateUserOccupationDto } from './dtos/update-user-occupation.dto'; +import { PaginatedUserOccupationResponseDto } from './dtos/user-occupation-response.dto'; +import { UserOccupation } from './user-occupation.entity'; + +@Injectable() +export class UserOccupationService { + constructor( + @InjectRepository(UserOccupation) + private readonly userOccupationRepository: Repository, + ) {} + + async create( + createUserOccupationDto: CreateUserOccupationDto, + ): Promise { + const userOccupation = this.userOccupationRepository.create( + createUserOccupationDto, + ); + + return this.userOccupationRepository.save(userOccupation); + } + + async findAll({ + page = 1, + limit = 20, + }: { + page?: number; + limit?: number; + }): Promise { + const { find } = await createPagination(this.userOccupationRepository, { + page, + limit, + }); + + const userOccupations = await find({}).run(); + + return userOccupations; + } + + async findOne( + id: string, + options: FindOneOptions, + ): Promise { + const baseWhere = options.where as FindOptionsWhere; + const whereCondition = { ...baseWhere, id }; + + const userOccupation = await this.userOccupationRepository.findOne({ + where: whereCondition, + }); + + if (!userOccupation) { + throw new NotFoundException('User occupation not found'); + } + + return userOccupation; + } + + async update( + id: string, + updateUserOccupationDto: UpdateUserOccupationDto, + ): Promise { + const userOccupation = await this.findOne(id, {}); + + this.userOccupationRepository.merge( + userOccupation, + updateUserOccupationDto, + ); + + return this.userOccupationRepository.save(userOccupation); + } + + async remove(id: string): Promise { + const userOccupation = await this.findOne(id, { where: { id } }); + + await this.userOccupationRepository.remove(userOccupation); + + return userOccupation; + } +} diff --git a/src/user-reward/dtos/update-status-user-reward.dto.ts b/src/user-reward/dtos/update-status-user-reward.dto.ts new file mode 100644 index 0000000..d384e7f --- /dev/null +++ b/src/user-reward/dtos/update-status-user-reward.dto.ts @@ -0,0 +1,17 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { UserRewardStatus } from '../enums/user-reward-status.enum'; +import { IsEnum, IsNotEmpty } from 'class-validator'; + +export class UpdateStatusUserReward { + @ApiProperty({ + description: 'status of user-reward', + type: String, + example: UserRewardStatus.EXPIRED, + enum: UserRewardStatus, + }) + @IsNotEmpty() + @IsEnum(UserRewardStatus, { + message: `status should be either ${UserRewardStatus.DELIVERED} ${UserRewardStatus.EXPIRED} or ${UserRewardStatus.PENDING}`, + }) + status: UserRewardStatus; +} diff --git a/src/user-reward/dtos/update-user-reward.dto.ts b/src/user-reward/dtos/update-user-reward.dto.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/user-reward/dtos/user-reward-response.dto.ts b/src/user-reward/dtos/user-reward-response.dto.ts new file mode 100644 index 0000000..914e033 --- /dev/null +++ b/src/user-reward/dtos/user-reward-response.dto.ts @@ -0,0 +1,73 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { UserReward } from '../user-reward.entity'; +import { UserRewardStatus } from '../enums/user-reward-status.enum'; + +export class UserRewardResponseDto { + @ApiProperty({ + description: 'user-reward id', + type: String, + example: '4a3522e7-3080-46a6-83ff-778ce829e8ef', + }) + id: string; + + @ApiProperty({ + description: 'user matched by id', + type: Object, + example: { id: '4a3522e7-3080-46a6-83ff-778ce829e8ef' }, + }) + user: { id: string }; + + @ApiProperty({ + description: 'reward matched by id', + type: Object, + example: { id: '4a3522e7-3080-46a6-83ff-778ce829e8ef', name: 'reward' }, + }) + reward: { id: string; name: string }; + + @ApiProperty({ + description: 'how many points user spent on this reward', + type: Number, + example: 100, + }) + pointsSpent: number; + + @ApiProperty({ + description: 'status of reward', + type: String, + enum: UserRewardStatus, + example: UserRewardStatus.DELIVERED, + }) + status: UserRewardStatus; + + @ApiProperty({ + description: 'redeem date', + type: Date, + example: new Date(), + }) + redeemedAt: Date; + + @ApiProperty({ + description: 'create date', + type: Date, + example: new Date(), + }) + createdAt: Date; + + @ApiProperty({ + description: 'lastest update date', + type: Date, + example: new Date(), + }) + updatedAt: Date; + + constructor(userReward: UserReward) { + this.id = userReward.id; + this.user = userReward.user; + this.reward = userReward.reward; + this.pointsSpent = userReward.pointsSpent; + this.status = userReward.status; + this.redeemedAt = userReward.redeemedAt; + this.createdAt = userReward.createdAt; + this.updatedAt = userReward.updatedAt; + } +} diff --git a/src/user-reward/enums/user-reward-status.enum.ts b/src/user-reward/enums/user-reward-status.enum.ts new file mode 100644 index 0000000..db2635b --- /dev/null +++ b/src/user-reward/enums/user-reward-status.enum.ts @@ -0,0 +1,5 @@ +export enum UserRewardStatus { + PENDING = 'pending', + DELIVERED = 'delivered', + EXPIRED = 'expired', +} diff --git a/src/user-reward/user-reward.controllers.ts b/src/user-reward/user-reward.controllers.ts new file mode 100644 index 0000000..87a37d1 --- /dev/null +++ b/src/user-reward/user-reward.controllers.ts @@ -0,0 +1,151 @@ +import { + Body, + Controller, + Get, + HttpStatus, + Injectable, + Param, + ParseUUIDPipe, + Post, + Req, + Patch, + Delete, + HttpCode, +} from '@nestjs/common'; +import { AuthenticatedRequest } from 'src/auth/interfaces/authenticated-request.interface'; +import { Roles } from 'src/shared/decorators/role.decorator'; +import { Role } from 'src/shared/enums'; +import { UserRewardService } from './user-reward.service'; +import { ApiBearerAuth, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { UserRewardResponseDto } from './dtos/user-reward-response.dto'; +import { UpdateStatusUserReward } from './dtos/update-status-user-reward.dto'; +import { UserRewardStatus } from './enums/user-reward-status.enum'; + +@Controller('user-reward') +@Injectable() +@ApiTags('User Reward') +@ApiBearerAuth() +export class UserRewardController { + constructor(private readonly userRewardService: UserRewardService) {} + + @Get() + @Roles(Role.ADMIN) + @ApiResponse({ + status: HttpStatus.OK, + type: UserRewardResponseDto, + isArray: true, + description: 'get all user-reward', + }) + async findAll(): Promise { + const userRewards = await this.userRewardService.findAll(); + return userRewards; + } + + @Post(':rewardId') + @Roles(Role.STUDENT) + @HttpCode(HttpStatus.CREATED) + @ApiResponse({ + status: HttpStatus.CREATED, + type: UserRewardResponseDto, + description: 'create new user-reward', + }) + async create( + @Param( + 'rewardId', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + rewardId: string, + @Req() request: AuthenticatedRequest, + ): Promise { + const userReward = await this.userRewardService.create( + request.user.id, + rewardId, + ); + return userReward; + } + + @Get('user') + @Roles(Role.STUDENT) + @ApiResponse({ + status: HttpStatus.OK, + type: UserRewardResponseDto, + isArray: true, + description: "get all user reward by user's id", + }) + async findByUser( + @Req() request: AuthenticatedRequest, + ): Promise { + const userRewards = await this.userRewardService.findByUser( + request.user.id, + ); + return userRewards; + } + + @Get(':id') + @Roles(Role.STUDENT) + @ApiResponse({ + status: HttpStatus.OK, + type: UserRewardResponseDto, + description: 'get user reward by id', + }) + async findOne( + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + const userReward = await this.userRewardService.findOne(id); + return userReward; + } + + @Patch(':id') + @Roles(Role.STUDENT) + @ApiResponse({ + status: HttpStatus.OK, + type: UserRewardResponseDto, + description: `update status (${UserRewardStatus.PENDING} ${UserRewardStatus.DELIVERED} or ${UserRewardStatus.EXPIRED})`, + }) + async updateStatus( + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + @Body() updateUserRewardDto: UpdateStatusUserReward, + ): Promise { + const userReward = await this.userRewardService.updateStatus( + id, + updateUserRewardDto, + ); + return userReward; + } + + @Delete(':id') + @Roles(Role.ADMIN) + @ApiResponse({ + status: HttpStatus.NO_CONTENT, + description: 'delete user-reward by user-reward id', + }) + async delete( + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + this.userRewardService.delete(id); + } +} diff --git a/src/user-reward/user-reward.entity.ts b/src/user-reward/user-reward.entity.ts new file mode 100644 index 0000000..a0fc63e --- /dev/null +++ b/src/user-reward/user-reward.entity.ts @@ -0,0 +1,58 @@ +import { + Column, + Entity, + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, +} from 'typeorm'; +import { UserRewardStatus } from './enums/user-reward-status.enum'; +import { User } from 'src/user/user.entity'; +import { Reward } from 'src/reward/reward.entity'; + +@Entity() +export class UserReward { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => User, (user) => user.rewards, { + onDelete: 'CASCADE', + }) + user: User; + + @ManyToOne(() => Reward, (reward) => reward.userReward, { + onDelete: 'CASCADE', + }) + reward: Reward; + + @Column({ + nullable: false, + }) + pointsSpent: number; + + @Column({ + nullable: false, + type: 'enum', + enum: UserRewardStatus, + }) + status: UserRewardStatus; + + + @CreateDateColumn({ + type: 'timestamp with time zone', + nullable: false, + }) + redeemedAt: Date; + + @CreateDateColumn({ + type: 'timestamp with time zone', + nullable: false, + }) + createdAt: Date; + + @UpdateDateColumn({ + type: 'timestamp with time zone', + nullable: false, + }) + updatedAt: Date; +} diff --git a/src/user-reward/user-reward.module.ts b/src/user-reward/user-reward.module.ts new file mode 100644 index 0000000..1a66fc5 --- /dev/null +++ b/src/user-reward/user-reward.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { UserReward } from './user-reward.entity'; +import { UserRewardController } from './user-reward.controllers'; +import { UserRewardService } from './user-reward.service'; +import { UserRewardProviders } from './user-reward.providers'; +import { DatabaseModule } from 'src/database/database.module'; +import { User } from 'src/user/user.entity'; +import { Reward } from 'src/reward/reward.entity'; + +@Module({ + imports: [ + DatabaseModule, + TypeOrmModule.forFeature([UserReward, User, Reward]), + ], + controllers: [UserRewardController], + providers: [...UserRewardProviders, UserRewardService], + exports: [UserRewardService], +}) +export class UserRewardModule {} diff --git a/src/user-reward/user-reward.providers.ts b/src/user-reward/user-reward.providers.ts new file mode 100644 index 0000000..d2715b7 --- /dev/null +++ b/src/user-reward/user-reward.providers.ts @@ -0,0 +1,13 @@ +import { DataSource } from 'typeorm'; +import { UserReward } from './user-reward.entity'; +import { User } from 'src/user/user.entity'; +import { Reward } from 'src/reward/reward.entity'; + +export const UserRewardProviders = [ + { + provide: 'UserRewardRepository', + useFactory: (dataSource: DataSource) => + dataSource.getRepository(UserReward), + inject: ['DataSource'], + }, +]; diff --git a/src/user-reward/user-reward.service.ts b/src/user-reward/user-reward.service.ts new file mode 100644 index 0000000..cf31862 --- /dev/null +++ b/src/user-reward/user-reward.service.ts @@ -0,0 +1,178 @@ +import { + BadRequestException, + Inject, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { Repository } from 'typeorm'; +import { UserReward } from './user-reward.entity'; +import { User } from 'src/user/user.entity'; +import { Reward } from 'src/reward/reward.entity'; +import { Status } from 'src/reward/enums/status.enum'; +import { UserRewardStatus } from './enums/user-reward-status.enum'; +import { UpdateStatusUserReward } from './dtos/update-status-user-reward.dto'; + +@Injectable() +export class UserRewardService { + constructor( + @Inject('UserRewardRepository') + private readonly userRewardRepository: Repository, + @Inject('UserRepository') + private readonly userRepository: Repository, + @Inject('RewardRepository') + private readonly rewardRepository: Repository, + ) {} + + async create(userId: string, rewardId: string): Promise { + const user = await this.userRepository.findOne({ where: { id: userId } }); + const reward = await this.rewardRepository.findOne({ + where: { id: rewardId }, + }); + if (reward.status == Status.INACTIVE) + throw new BadRequestException( + `${reward.name} is currently not available.`, + ); + if (user.points < reward.points) + throw new BadRequestException( + `not enough points (${reward.points - user.points} points missing)`, + ); + if (reward.stock <= 0) throw new BadRequestException('reward not enough'); + if (reward.stock == 1) + this.rewardRepository.update(rewardId, { status: Status.INACTIVE }); + this.rewardRepository.update(rewardId, { stock: reward.stock - 1 }); + this.userRepository.update(userId, { points: user.points - reward.points }); + const newUserReward = new UserReward(); + newUserReward.user = user; + newUserReward.reward = reward; + newUserReward.pointsSpent = reward.points; + newUserReward.status = UserRewardStatus.PENDING; + const userRewardRes = await this.userRewardRepository.save(newUserReward); + return userRewardRes; + } + + async findAll(): Promise { + return this.userRewardRepository.find({ + relations: { + reward: true, + user: true, + }, + select: { + id: true, + pointsSpent: true, + status: true, + createdAt: true, + updatedAt: true, + user: { + id: true, + }, + reward: { + id: true, + name: true, + }, + }, + }); + } + + async findOne(id: string): Promise { + const userReward = await this.userRewardRepository.findOne({ + relations: { + user: true, + reward: true, + }, + where: { id }, + select: { + id: true, + pointsSpent: true, + status: true, + createdAt: true, + updatedAt: true, + user: { + id: true, + }, + reward: { + id: true, + name: true, + }, + }, + }); + if (!userReward) throw new NotFoundException('user reward not found'); + return userReward; + } + + async findByUser(id: string): Promise { + const userRewards = await this.userRewardRepository.find({ + relations: { + user: true, + reward: true, + }, + where: { + user: { + id, + }, + }, + select: { + id: true, + pointsSpent: true, + status: true, + createdAt: true, + updatedAt: true, + user: { + id: true, + }, + reward: { + id: true, + name: true, + }, + }, + }); + if (userRewards.length <= 0) { + throw new NotFoundException("user doesn't have reward yet"); + } + return userRewards; + } + + async updateStatus( + id: string, + userReward: UpdateStatusUserReward, + ): Promise { + try { + const userRewardRes = await this.userRewardRepository.update(id, { + status: userReward.status, + }); + if (userRewardRes.affected == 0) + throw new NotFoundException('user reward not found'); + return this.userRewardRepository.findOne({ + relations: { + user: true, + reward: true, + }, + where: { + id, + }, + select: { + id: true, + pointsSpent: true, + status: true, + createdAt: true, + updatedAt: true, + user: { + id: true, + }, + reward: { + id: true, + name: true, + }, + }, + }); + } catch (error) { + if (error instanceof Error) + throw new NotFoundException('user reward not found'); + } + } + + async delete(id: string): Promise { + const deleteResult = await this.userRewardRepository.delete(id); + if (deleteResult.affected == 0) + throw new NotFoundException('user-reward not found'); + } +} diff --git a/src/user-streak/dtos/user-streak-response.dto.ts b/src/user-streak/dtos/user-streak-response.dto.ts new file mode 100644 index 0000000..4871bf1 --- /dev/null +++ b/src/user-streak/dtos/user-streak-response.dto.ts @@ -0,0 +1,55 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { UserStreak } from '../user-streak.entity'; + +export class UserStreakResponseDto { + @ApiProperty({ + description: 'User streak ID', + type: String, + example: '123e4567-e89b-12d3-a456-426614174000', + }) + id: string; + + @ApiProperty({ + description: 'User current streak', + type: Date, + example: new Date(), + }) + currentStreak: Date; + + @ApiProperty({ + description: 'User longest streak', + type: Date, + example: new Date(), + }) + longestStreak: Date; + + @ApiProperty({ + description: 'User last activity date', + type: Date, + example: new Date(), + }) + lastActivityDate: Date; + + @ApiProperty({ + description: 'User created date', + type: Date, + example: new Date(), + }) + createdAt: Date; + + @ApiProperty({ + description: 'User updated date', + type: Date, + example: new Date(), + }) + updatedAt: Date; + + constructor(userStreak: UserStreak) { + this.id = userStreak.id; + this.currentStreak = userStreak.currentStreak; + this.longestStreak = userStreak.longestStreak; + this.lastActivityDate = userStreak.lastActivityDate; + this.createdAt = userStreak.createdAt; + this.updatedAt = userStreak.updatedAt; + } +} diff --git a/src/user-streak/user-streak.controller.ts b/src/user-streak/user-streak.controller.ts new file mode 100644 index 0000000..77625e5 --- /dev/null +++ b/src/user-streak/user-streak.controller.ts @@ -0,0 +1,82 @@ +import { + Controller, + Get, + HttpCode, + HttpStatus, + Injectable, + Post, + Req, +} from '@nestjs/common'; +import { ApiBearerAuth, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { AuthenticatedRequest } from 'src/auth/interfaces/authenticated-request.interface'; +import { Roles } from 'src/shared/decorators/role.decorator'; +import { Role } from 'src/shared/enums/roles.enum'; +import { UserStreakResponseDto } from './dtos/user-streak-response.dto'; +import { UserStreak } from './user-streak.entity'; +import { UserStreakService } from './user-streak.service'; +import { UserService } from 'src/user/user.service'; + +@Controller('user-streak') +@ApiTags('User Streak') +@ApiBearerAuth() +@Injectable() +export class UserStreakController { + constructor( + private readonly userStreakService: UserStreakService, + private readonly userService: UserService, + ) {} + + @Get() + @Roles(Role.ADMIN) + @ApiResponse({ + status: HttpStatus.OK, + type: UserStreak, + description: 'Get all user streaks', + isArray: true, + }) + async findAll(): Promise { + const userStreaks = await this.userStreakService.findAll(); + return userStreaks.map( + (userStreak) => new UserStreakResponseDto(userStreak), + ); + } + + @Get('profile') + @ApiResponse({ + status: HttpStatus.OK, + type: UserStreakResponseDto, + description: 'Get user streak', + isArray: true, + }) + async findOne( + @Req() request: AuthenticatedRequest, + ): Promise { + const userStreak = await this.userStreakService.findMany({ + where: { user: { id: request.user.id } }, + relations: { user: true }, + }); + return userStreak.map((userStreak) => new UserStreakResponseDto(userStreak)); + } + + @Post() + @ApiResponse({ + status: HttpStatus.CREATED, + type: UserStreakResponseDto, + description: 'Create user streak', + }) + @Roles(Role.STUDENT) + @HttpCode(HttpStatus.CREATED) + async create( + @Req() request: AuthenticatedRequest, + ): Promise { + const userStreak = await this.userStreakService.create(request.user.id); + await this.userService.increment( + { + id: request.user.id, + }, + 'points', + 5, + ); + return new UserStreakResponseDto(userStreak); + } +} diff --git a/src/user-streak/user-streak.entity.ts b/src/user-streak/user-streak.entity.ts new file mode 100644 index 0000000..5fb15ba --- /dev/null +++ b/src/user-streak/user-streak.entity.ts @@ -0,0 +1,55 @@ +import { User } from 'src/user/user.entity'; +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity() +export class UserStreak { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => User, (user) => user.streaks, { + onDelete: 'CASCADE', + eager: true, + }) + @JoinColumn() + user: User; + + @Column({ + default: new Date(), + nullable: false, + type: 'timestamp with time zone', + }) + currentStreak: Date; + + @Column({ + default: new Date(), + nullable: false, + type: 'timestamp with time zone', + }) + longestStreak: Date; + + @Column({ + type: 'timestamp with time zone', + default: () => 'CURRENT_TIMESTAMP', + }) + lastActivityDate: Date; + + @CreateDateColumn({ + type: 'timestamp with time zone', + nullable: false, + }) + createdAt: Date; + + @UpdateDateColumn({ + type: 'timestamp with time zone', + nullable: false, + }) + updatedAt: Date; +} diff --git a/src/user-streak/user-streak.module.ts b/src/user-streak/user-streak.module.ts new file mode 100644 index 0000000..b97a16b --- /dev/null +++ b/src/user-streak/user-streak.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { DatabaseModule } from 'src/database/database.module'; +import { UserStreakController } from './user-streak.controller'; +import { userStreakProviders } from './user-streak.providers'; +import { UserStreakService } from './user-streak.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { UserStreak } from './user-streak.entity'; +import { UserModule } from 'src/user/user.module'; + +@Module({ + imports: [DatabaseModule, TypeOrmModule.forFeature([UserStreak]), UserModule], + controllers: [UserStreakController], + providers: [...userStreakProviders, UserStreakService], + exports: [UserStreakService], +}) +export class UserStreakModule {} diff --git a/src/user-streak/user-streak.providers.ts b/src/user-streak/user-streak.providers.ts new file mode 100644 index 0000000..e3e719c --- /dev/null +++ b/src/user-streak/user-streak.providers.ts @@ -0,0 +1,11 @@ +import { DataSource } from 'typeorm'; +import { UserStreak } from './user-streak.entity'; + +export const userStreakProviders = [ + { + provide: 'UserStreakRepository', + useFactory: (dataSource: DataSource) => + dataSource.getRepository(UserStreak), + inject: ['DataSource'], + }, +]; diff --git a/src/user-streak/user-streak.service.ts b/src/user-streak/user-streak.service.ts new file mode 100644 index 0000000..38fb620 --- /dev/null +++ b/src/user-streak/user-streak.service.ts @@ -0,0 +1,47 @@ +import { + Inject, + Injectable, + InternalServerErrorException, + NotFoundException, +} from '@nestjs/common'; +import { FindManyOptions, FindOneOptions, FindOptionsWhere, Repository } from 'typeorm'; +import { UserStreak } from './user-streak.entity'; + +@Injectable() +export class UserStreakService { + constructor( + @Inject('UserStreakRepository') + private readonly userStreakRepository: Repository, + ) {} + + async create(userId: string): Promise { + const userStreak = await this.userStreakRepository.save({ + user: { id: userId }, + }); + return userStreak; + } + + async findAll(): Promise { + return this.userStreakRepository.find(); + } + + async findMany(options: FindManyOptions): Promise { + return this.userStreakRepository.find(options); + } + + async findOne(options: FindOneOptions): Promise { + const userStreak = await this.userStreakRepository.findOne(options); + if (!userStreak) throw new NotFoundException('User streak not found'); + return userStreak; + } + + async delete(options: FindOptionsWhere): Promise { + try { + await this.userStreakRepository.delete(options); + } catch (error) { + if (error instanceof Error) { + throw new NotFoundException('User streak not found'); + } + } + } +} diff --git a/src/user/dtos/create-user.dto.ts b/src/user/dtos/create-user.dto.ts new file mode 100644 index 0000000..044d76b --- /dev/null +++ b/src/user/dtos/create-user.dto.ts @@ -0,0 +1,57 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsEmail, + IsNotEmpty, + IsString, + IsStrongPassword, + IsEnum, +} from 'class-validator'; +import { AvailableRoles, Role } from 'src/shared/enums'; + +export class CreateUserDto { + @IsEmail() + @IsNotEmpty() + @ApiProperty({ + description: 'User email', + type: String, + example: 'johndoe@gmail.com', + }) + email: string; + + @IsString() + @IsNotEmpty() + @ApiProperty({ + description: 'User username', + type: String, + example: 'johndoe', + }) + username: string; + + @IsString() + @IsNotEmpty() + @ApiProperty({ + description: 'User fullname', + type: String, + example: 'John Doe', + }) + fullname: string; + + @IsStrongPassword() + @IsNotEmpty() + @ApiProperty({ + description: 'User password', + type: String, + example: 'P@ssw0rd!', + }) + password: string; + + @IsEnum(AvailableRoles) + @IsNotEmpty() + @ApiProperty({ + description: 'User role', + type: String, + example: AvailableRoles.STUDENT, + enum: AvailableRoles, + }) + role: Role; +} diff --git a/src/user/dtos/update-user.dto.ts b/src/user/dtos/update-user.dto.ts new file mode 100644 index 0000000..1bc1c83 --- /dev/null +++ b/src/user/dtos/update-user.dto.ts @@ -0,0 +1,39 @@ +import { + IsString, + IsOptional, + IsStrongPassword, + IsInt, + Min, +} from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class UpdateUserDto { + @IsString() + @IsOptional() + @ApiProperty({ + description: 'User email', + type: String, + example: 'johndoe', + }) + username?: string; + + @IsString() + @IsOptional() + @IsStrongPassword() + @ApiProperty({ + description: 'New User Password', + type: String, + example: 'P@ssw0rd!', + }) + password?: string; + + @IsInt() + @Min(0) + @IsOptional() + @ApiProperty({ + description: 'User points', + type: Number, + example: 100, + }) + points?: number; +} diff --git a/src/user/dtos/user-response.dto.ts b/src/user/dtos/user-response.dto.ts new file mode 100644 index 0000000..d52539e --- /dev/null +++ b/src/user/dtos/user-response.dto.ts @@ -0,0 +1,80 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Role } from 'src/shared/enums/roles.enum'; +import { PaginatedResponse } from 'src/shared/pagination/dtos/paginate-response.dto'; +import { User } from '../user.entity'; + +export class UserResponseDto { + @ApiProperty({ + description: 'User ID', + type: String, + example: '8d4887aa-28e7-4d0e-844c-28a8ccead003', + }) + id: string; + + @ApiProperty({ + description: 'User email', + type: String, + example: 'johndoe@gmail.com', + }) + email: string; + + @ApiProperty({ + description: 'points for reward', + type: Number, + example: '200', + }) + points: number; + + @ApiProperty({ + description: 'User role', + type: String, + example: Role.STUDENT, + enum: [Role.STUDENT, Role.TEACHER], + }) + role: Role; + + @ApiProperty({ + description: 'User created date', + type: Date, + example: new Date(), + }) + createdAt: Date; + + @ApiProperty({ + description: 'User updated date', + type: Date, + example: new Date(), + }) + updatedAt: Date; + + @ApiProperty({ + description: 'User fullname', + type: String, + example: 'John Doe', + }) + fullname: string; + + constructor(user: User) { + this.id = user.id; + this.email = user.email; + this.role = user.role; + this.points = user.points; + this.createdAt = user.createdAt; + this.updatedAt = user.updatedAt; + this.fullname = user.fullname; + } +} + +export class PaginatedUserResponseDto extends PaginatedResponse( + UserResponseDto, +) { + constructor( + users: User[], + total: number, + pageSize: number, + currentPage: number, + ) { + const userDtos = users.map((user) => new UserResponseDto(user)); + super(userDtos, total, pageSize, currentPage); + } +} diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts new file mode 100644 index 0000000..c3642f1 --- /dev/null +++ b/src/user/user.controller.ts @@ -0,0 +1,253 @@ +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Injectable, + Param, + ParseUUIDPipe, + Patch, + Query, + Req, + StreamableFile, + UseInterceptors, + UploadedFile, + ParseFilePipeBuilder, +} from '@nestjs/common'; +import { + ApiBearerAuth, + ApiBody, + ApiConsumes, + ApiParam, + ApiQuery, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; +import { AuthenticatedRequest } from 'src/auth/interfaces/authenticated-request.interface'; +import { Public } from 'src/shared/decorators/public.decorator'; +import { Roles } from 'src/shared/decorators/role.decorator'; +import { Role } from 'src/shared/enums/roles.enum'; +import { PaginateQueryDto } from 'src/shared/pagination/dtos/paginate-query.dto'; +import { UpdateUserDto } from './dtos/update-user.dto'; +import { + PaginatedUserResponseDto, + UserResponseDto, +} from './dtos/user-response.dto'; +import { UserService } from './user.service'; +import { FileService } from 'src/file/file.service'; +import { Folder } from 'src/file/enums/folder.enum'; +import { hash } from 'argon2'; +import { FileInterceptor } from '@nestjs/platform-express'; + +@Controller('user') +@ApiTags('User') +@Injectable() +export class UserController { + constructor( + private readonly userService: UserService, + private readonly fileService: FileService, + ) {} + + @Get(':id/avatar') + @Public() + @ApiParam({ + name: 'id', + type: String, + description: 'User id', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Get user avatar', + type: StreamableFile, + }) + async getAvatar( + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + const user = await this.userService.findOne({ where: { id } }); + const file = await this.fileService.get(Folder.PROFILES, user.profileKey); + return new StreamableFile(file, { + disposition: 'inline', + type: `image/${user.profileKey.split('.').pop()}`, + }); + } + + @Patch('avatar') + @ApiBearerAuth() + @ApiResponse({ + status: HttpStatus.NO_CONTENT, + description: 'User avatar updated successfully', + }) + @HttpCode(HttpStatus.NO_CONTENT) + @UseInterceptors(FileInterceptor('file')) + @ApiConsumes('multipart/form-data') + @ApiBody({ + schema: { + type: 'object', + properties: { + file: { + type: 'string', + format: 'binary', + }, + }, + }, + }) + async uploadProfileAvatar( + @Req() request: AuthenticatedRequest, + @UploadedFile( + new ParseFilePipeBuilder() + .addFileTypeValidator({ fileType: 'image/*' }) + .build({ + fileIsRequired: true, + errorHttpStatusCode: HttpStatus.BAD_REQUEST, + }), + ) + file: Express.Multer.File, + ): Promise { + const user = await this.userService.findOne({ + where: { id: request.user.id }, + }); + if (user.profileKey) + await this.fileService.update(Folder.PROFILES, user.profileKey, file); + else { + await this.fileService.upload(Folder.PROFILES, user.id, file); + } + await this.userService.update(request.user.id, { profileKey: `${user.id}.${file.originalname.split('.').pop()}` }); + } + + @Get('avatar') + @ApiBearerAuth() + @ApiResponse({ + status: HttpStatus.OK, + description: 'Get user avatar', + type: StreamableFile, + }) + async getProfileAvatar( + @Req() request: AuthenticatedRequest, + ): Promise { + const user = await this.userService.findOne({ + where: { id: request.user.id }, + }); + const file = await this.fileService.get(Folder.PROFILES, user.profileKey); + return new StreamableFile(file, { + disposition: 'inline', + type: `image/${user.profileKey.split('.').pop()}`, + }); + } + + @Get('profile') + @ApiBearerAuth() + @ApiResponse({ + status: HttpStatus.OK, + type: UserResponseDto, + description: 'Get user profile', + }) + async getProfile( + @Req() request: AuthenticatedRequest, + ): Promise { + const user = await this.userService.findOne({ + where: { id: request.user.id }, + }); + return new UserResponseDto(user); + } + + @Get() + @Roles(Role.ADMIN) + @ApiBearerAuth() + @ApiResponse({ + status: HttpStatus.OK, + type: PaginatedUserResponseDto, + description: 'Get all users', + }) + @ApiQuery({ + name: 'page', + type: Number, + required: false, + description: 'Page number', + }) + @ApiQuery({ + name: 'limit', + type: Number, + required: false, + description: 'Items per page', + }) + @ApiQuery({ + name: 'search', + type: String, + required: false, + description: 'Search by email', + }) + async findAll( + @Query() query: PaginateQueryDto, + ): Promise { + return this.userService.findAll({ + page: query.page, + limit: query.limit, + search: query.search, + }); + } + + @Get(':id') + @ApiParam({ + name: 'id', + type: String, + description: 'User id', + }) + @ApiResponse({ + status: HttpStatus.OK, + type: UserResponseDto, + description: 'Get user by id', + }) + @Public() + async findOne( + @Param( + 'id', + new ParseUUIDPipe({ + version: '4', + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }), + ) + id: string, + ): Promise { + const user = await this.userService.findOne({ where: { id } }); + return new UserResponseDto(user); + } + + @Patch() + @ApiBearerAuth() + @ApiResponse({ + status: HttpStatus.OK, + type: UserResponseDto, + description: 'Update user', + }) + async update( + @Req() request: AuthenticatedRequest, + @Body() updateUserDto: UpdateUserDto, + ): Promise { + if (updateUserDto.password) + updateUserDto.password = await hash(updateUserDto.password); + const user = await this.userService.update(request.user.id, updateUserDto); + return new UserResponseDto(user); + } + + @Delete() + @ApiBearerAuth() + @ApiResponse({ + status: HttpStatus.NO_CONTENT, + description: 'Delete user', + }) + @HttpCode(HttpStatus.NO_CONTENT) + async delete( + @Req() request: AuthenticatedRequest, + ): Promise { + await this.userService.delete({ id: request.user.id }); + } +} diff --git a/src/user/user.entity.ts b/src/user/user.entity.ts new file mode 100644 index 0000000..8b929cf --- /dev/null +++ b/src/user/user.entity.ts @@ -0,0 +1,113 @@ +import { Course } from 'src/course/course.entity'; +import { Enrollment } from 'src/enrollment/enrollment.entity'; +import { ExamAttempt } from 'src/exam-attempt/exam-attempt.entity'; +import { Roadmap } from 'src/roadmap/roadmap.entity'; +import { Role } from 'src/shared/enums/roles.enum'; +import { UserBackground } from 'src/user-background/user-background.entity'; +import { UserStreak } from 'src/user-streak/user-streak.entity'; +import { UserReward } from 'src/user-reward/user-reward.entity'; +import { + Column, + CreateDateColumn, + Entity, + OneToMany, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; +import { Pretest } from 'src/pretest/pretest.entity'; + +@Entity() +export class User { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ + nullable: false, + unique: true, + }) + username: string; + + @Column({ + nullable: false, + }) + fullname: string; + + @Column({ + type: 'enum', + enum: Role, + nullable: false, + default: Role.STUDENT, + }) + role: Role; + + @OneToMany(() => UserBackground, (background) => background.user) + backgrounds: UserBackground[]; + + @OneToMany(() => Roadmap, (roadmap) => roadmap.user) + roadmaps: Roadmap[]; + + @OneToMany(() => UserStreak, (streak) => streak.user) + streaks: UserStreak[]; + + @Column({ + nullable: false, + unique: true, + }) + password: string; + + @Column({ + nullable: false, + unique: true, + }) + email: string; + + @Column({ + nullable: true, + default: 0, + }) + points: number; + + @OneToMany(() => Course, (course) => course.teacher, { + nullable: true, + }) + courses: Course[]; + + @OneToMany(() => Enrollment, (enrollment) => enrollment.user, { + nullable: true, + }) + enrollments: Enrollment[]; + + @OneToMany(() => Pretest, (pretest) => pretest.user, { + cascade: true, + nullable: true, + }) + pretest: Pretest[]; + + @OneToMany(() => ExamAttempt, (examAttempt) => examAttempt.user, { + cascade: true, + nullable: true, + }) + examAttempt: ExamAttempt[]; + + @OneToMany(() => UserReward, (userReward) => userReward.user, { + nullable: true, + }) + rewards: UserReward[]; + + @CreateDateColumn({ + type: 'timestamp with time zone', + nullable: false, + }) + createdAt: Date; + + @UpdateDateColumn({ + type: 'timestamp with time zone', + nullable: false, + }) + updatedAt: Date; + + @Column({ + nullable: true, + }) + profileKey: string; +} diff --git a/src/user/user.module.ts b/src/user/user.module.ts new file mode 100644 index 0000000..1cd1f2e --- /dev/null +++ b/src/user/user.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { UserController } from './user.controller'; +import { User } from './user.entity'; +import { UserService } from './user.service'; +import { FileModule } from 'src/file/file.module'; + +@Module({ + imports: [TypeOrmModule.forFeature([User]), FileModule], + controllers: [UserController], + providers: [UserService], + exports: [UserService], +}) +export class UserModule {} diff --git a/src/user/user.providers.ts b/src/user/user.providers.ts new file mode 100644 index 0000000..813fb78 --- /dev/null +++ b/src/user/user.providers.ts @@ -0,0 +1,10 @@ +import { DataSource } from 'typeorm'; +import { User } from './user.entity'; + +export const userProviders = [ + { + provide: 'UserRepository', + useFactory: (dataSource: DataSource) => dataSource.getRepository(User), + inject: ['DataSource'], + }, +]; diff --git a/src/user/user.service.ts b/src/user/user.service.ts new file mode 100644 index 0000000..48d3368 --- /dev/null +++ b/src/user/user.service.ts @@ -0,0 +1,107 @@ +import { + BadRequestException, + Inject, + Injectable, + NotFoundException, + OnModuleInit, +} from '@nestjs/common'; +import { createPagination } from 'src/shared/pagination'; +import { FindOneOptions, ILike, Repository, FindOptionsWhere } from 'typeorm'; +import { CreateUserDto } from './dtos/create-user.dto'; +import { PaginatedUserResponseDto } from './dtos/user-response.dto'; +import { User } from './user.entity'; +import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; +import { ConfigService } from '@nestjs/config'; +import { Role } from 'src/shared/enums'; +import { hash } from 'argon2'; + +@Injectable() +export class UserService implements OnModuleInit { + constructor( + @Inject('UserRepository') + private readonly userRepository: Repository, + private readonly configService: ConfigService, + ) {} + + async onModuleInit() { + const adminEmail = this.configService.get('ADMIN_EMAIL'); + const adminPassword = this.configService.get('ADMIN_PASSWORD'); + const admin = await this.userRepository.findOne({ + where: { email: adminEmail }, + }); + if (!admin) { + await this.userRepository.save({ + email: adminEmail, + password: await hash(adminPassword), + fullname: 'Admin', + role: Role.ADMIN, + username: 'admin', + }); + } + } + + async findAll({ + page = 1, + limit = 20, + search = '', + }: { + page?: number; + limit?: number; + search?: string; + }): Promise { + const { find } = await createPagination(this.userRepository, { + page, + limit, + }); + + const users = await find({ + where: { email: ILike(`%${search}%`) }, + }).run(); + + return users; + } + + async findOne(options: FindOneOptions): Promise { + return await this.userRepository.findOne(options); + } + + async create(createUserDto: CreateUserDto): Promise { + try { + return await this.userRepository.save(createUserDto); + } catch (error) { + if (error instanceof Error) throw new BadRequestException(error.message); + } + } + + async update( + id: string, + partialEntity: QueryDeepPartialEntity, + ): Promise { + try { + await this.userRepository.update(id, partialEntity); + return await this.findOne({ where: { id } }); + } catch (error) { + if (error instanceof Error) throw new NotFoundException('User not found'); + } + } + + async delete(criteria: FindOptionsWhere): Promise { + try { + await this.userRepository.delete(criteria); + } catch (error) { + if (error instanceof Error) throw new NotFoundException(error.message); + } + } + + async increment( + where: FindOptionsWhere, + propertyPath: string, + value: number, + ): Promise { + try { + await this.userRepository.increment(where, propertyPath, value); + } catch { + throw new NotFoundException('User not found'); + } + } +} diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts new file mode 100644 index 0000000..50cda62 --- /dev/null +++ b/test/app.e2e-spec.ts @@ -0,0 +1,24 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { AppModule } from './../src/app.module'; + +describe('AppController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('/ (GET)', () => { + return request(app.getHttpServer()) + .get('/') + .expect(200) + .expect('Hello World!'); + }); +}); diff --git a/test/jest-e2e.json b/test/jest-e2e.json new file mode 100644 index 0000000..e9d912f --- /dev/null +++ b/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +}