diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..7be255af5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,393 @@ +# Created by .ignore support plugin (hsz.mobi) +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# CMake +cmake-build-debug/ + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties +### VisualStudio template +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Typescript v1 declaration files +typings/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ +coverage/ + +### macOS template +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +======= +# Local +dist +# /.env +/.env.*.local +*._local.ts diff --git a/.env.example b/.env.example index f1dd7fa5d..018066e39 100644 --- a/.env.example +++ b/.env.example @@ -2,14 +2,22 @@ NODE_ENV=development PROFILE=local PORT=5000 HOST= +REDIS_PORT= 6379 DB_SSL=true + +JWT_SECRET=someSecrets +JWT_EXPIRY_TIMEFRAME=3600 +REDIS_HOST=localhost +REDIS_PORT=6379 DB_TYPE= DB_USERNAME= DB_PASSWORD= DB_HOST= -DB_DATABASE=hng +DB_NAME=hng DB_ENTITIES=dist/src/modules/**/entities/**/*.entity{.ts,.js} DB_MIGRATIONS=dist/db/migrations/*{.ts,.js} +POSGRES_USER=$DB_USERNAME +POST JWT_SECRET=gsgs JWT_EXPIRY_TIMEFRAME=1500000 DB_SSL=false @@ -28,4 +36,6 @@ FRONTEND_URL= ADMIN_SECRET= SUPPORT_EMAIL= AUTH_PASSWORD= -BASE_URL= \ No newline at end of file +BASE_URL= +FLUTTERWAVE_SECRET_KEY= +FLUTTERWAVE_BASE_URL= diff --git a/.env.local b/.env.local index b57cd8954..6771b01b7 100644 --- a/.env.local +++ b/.env.local @@ -33,7 +33,7 @@ PORT=3000 DB_USERNAME=username DB_PASSWORD=password DB_TYPE=postgres -DB_DATABASE=database +DB_NAME=database DB_HOST=localhost DB_PORT=5432 DB_ENTITIES=dist/src/modules/**/entities/**/*.entity{.ts,.js} @@ -41,8 +41,7 @@ DB_ENTITIES=dist/src/modules/**/entities/**/*.entity{.ts,.js} DB_MIGRATIONS=dist/**/migrations/*{.ts,.js} DB_TYPE=postgres DB_SSL=true - JWT_SECRET=someSecrets JWT_EXPIRY_TIMEFRAME=3600 - - +BASE_URL= "https://staging.api-nestjs.boilerplate.hng.tech" +REDIS_PORT=6379 diff --git a/.github/Unused/pr-deploy.yaml b/.github/Unused/pr-deploy.yaml new file mode 100644 index 000000000..3bf206500 --- /dev/null +++ b/.github/Unused/pr-deploy.yaml @@ -0,0 +1,27 @@ +# name: PR Deploy +# on: +# pull_request_target: +# branches: +# - dev + +# jobs: +# deploy-pr-for-testing: +# environment: +# name: preview +# url: ${{ steps.deploy.outputs.preview-url }} +# runs-on: ubuntu-latest +# steps: +# - name: Checkout to branch +# uses: actions/checkout@v4 +# - id: deploy +# name: Pull Request Deploy +# uses: hngprojects/pr-deploy@dev +# with: +# server_host: ${{ secrets.HOST }} +# server_username: ${{ secrets.USERNAME }} +# server_password: ${{ secrets.PASSWORD }} +# comment: false +# context: . +# dockerfile: Dockerfile +# exposed_port: 5000 +# github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/Unused/scheduled-test.yaml b/.github/Unused/scheduled-test.yaml new file mode 100644 index 000000000..4b4df439a --- /dev/null +++ b/.github/Unused/scheduled-test.yaml @@ -0,0 +1,22 @@ +name: Scheduled Test + +on: + schedule: + - cron: '*/15 * * * *' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Run test script + env: + POSTMAN_API_KEY: ${{ secrets.POSTMAN_API_KEY }} + API_URL: ${{ secrets.API_URL }} + run: | + cd qa + chmod +x test.sh + ./test.sh diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml deleted file mode 100644 index 571460270..000000000 --- a/.github/workflows/dev.yml +++ /dev/null @@ -1,115 +0,0 @@ -name: CI/CD--Dev - -on: - pull_request_target: - branches: - - dev - push: - branches: - - dev - -env: - DB_USERNAME: ${{ secrets.DB_USERNAME }} - DB_PASSWORD: ${{ secrets.DB_PASSWORD }} - DB_DATABASE: ${{ secrets.DB_DATABASE }} - DB_HOST: ${{ secrets.DB_HOST }} - DB_PORT: ${{ secrets.DB_PORT }} - DB_ENTITIES: ${{ secrets.DB_ENTITIES }} - DB_MIGRATIONS: ${{ secrets.DB_MIGRATIONS }} - GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} - GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} - GOOGLE_REDIRECT_URI: ${{ secrets.GOOGLE_REDIRECT_URI }} - DB_TYPE: 'postgres' - PROFILE: 'staging' - NODE_ENV: 'development' - PORT: 3000 - -jobs: - test-and-build-dev: - runs-on: ubuntu-latest - # environment: pr_environment - steps: - - name: Checkout code - uses: actions/checkout@v3 - with: - ref: ${{ github.event.pull_request.head.sha }} - - - name: Set up Node.js - uses: actions/setup-node@v3 - with: - node-version: '18' - - - name: Install dependencies - run: npm install --include=dev - - - name: Run tests - run: npm run test - - - name: Build project - run: npm run build - - - name: Generate migrations - run: npm run migration:generate - - - name: Run migrations - run: npm run migration:run - - - name: Start application - run: | - npm run start:prod > app.log 2>&1 & - APP_PID=$! - echo $APP_PID - echo "Application started with PID $APP_PID" - sleep 30 # Wait for the application to start - tail -f app.log & - # Check application status with curl - if curl --retry 5 --retry-delay 5 --max-time 10 http://localhost:3000/health; then - echo "Application is up and running." - else - echo "Application failed to start. Logs:" - cat app.log - echo "Exiting workflow due to application failure." - kill $APP_PID - exit 1 - fi - - kill $APP_PID - echo "Application terminated Successfully." - - - name: Revert Migrations - run: npm run migration:revert - if: always() - - deploy-push: - runs-on: ubuntu-latest - if: github.event_name == 'push' - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Set up Node.js - uses: actions/setup-node@v3 - with: - node-version: '18' - - - name: Install dependencies - run: npm install --include=dev - - - name: Build project - run: npm run build - - - name: Run tests - run: npm run test - - - name: Deploying to virtual machine - uses: appleboy/ssh-action@v1.0.3 - with: - host: ${{ secrets.SERVER_HOST }} - username: ${{ secrets.SERVER_USER }} - password: ${{ secrets.SERVER_PASSWORD }} - port: ${{ secrets.SERVER_PORT }} - script: | - echo "hello" - export PATH=$PATH:/home/teamalpha/.nvm/versions/node/v20.15.1/bin - bash ~/deployment.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/lint-build-test.yaml similarity index 67% rename from .github/workflows/ci.yml rename to .github/workflows/lint-build-test.yaml index 0b4aa0237..144facb09 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/lint-build-test.yaml @@ -1,30 +1,26 @@ -name: ci +name: Lint, Build and Test on: - pull_request: - branches: - - dev - push: - branches: - - dev - + pull_request + jobs: - test-and-build-dev: + lint-build-and-test: runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - steps: - name: Checkout code uses: actions/checkout@v3 - + - name: Set up Node.js uses: actions/setup-node@v3 with: - node-version: '18' + node-version: '20' - name: Install dependencies run: npm install --include=dev + - name: Run lint + run: npm run lint + - name: Build project run: npm run build diff --git a/.github/workflows/main-deployment.yaml b/.github/workflows/main-deployment.yaml new file mode 100644 index 000000000..f9ceae387 --- /dev/null +++ b/.github/workflows/main-deployment.yaml @@ -0,0 +1,58 @@ +name: Production Deployment + +on: + workflow_dispatch: + push: + branches: + - main + +concurrency: + group: ${{ github.ref }} + cancel-in-progress: true + +jobs: + deploy-prod: + runs-on: ubuntu-latest + steps: + - name : Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: "20" + + - name: Install Dependencies + run: npm install + + - name: Build Application + run: | + npm run build + cp package.json deployment.sh main-ecosystem-config.json dist + + - name: Archive application build + run: | + tar -czf nestjs.tar.gz dist + + - name: Copy Artifacts to server + run: | + sshpass -p ${{ secrets.PASSWORD }} scp -o StrictHostKeyChecking=no nestjs.tar.gz ${{ secrets.USERNAME }}@${{ secrets.HOST }}:/tmp/nestjs + rm -f nestjs.tar.gz + + - name: Deploy on server + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.HOST }} + username: ${{ secrets.USERNAME }} + password: ${{ secrets.PASSWORD }} + script: | + cd backend/nestjs + tar -xzf /tmp/nestjs/nestjs.tar.gz -C . + rm -f /tmp/nestjs/nestjs.tar.gz + mv dist/package.json . + mv dist/main-ecosystem-config.json . + mv dist/deployment.sh . + + chmod +x deployment.sh + ./deployment.sh main + \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index befd697ce..000000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,112 +0,0 @@ -name: CI/CD--Main - -on: - pull_request: - branches: - - main - push: - branches: - - main - -env: - DB_USERNAME: ${{ secrets.DB_USERNAME }} - DB_PASSWORD: ${{ secrets.DB_PASSWORD }} - DB_DATABASE: ${{ secrets.DB_DATABASE }} - DB_HOST: ${{ secrets.DB_HOST }} - DB_PORT: ${{ secrets.DB_PORT }} - DB_ENTITIES: ${{ secrets.DB_ENTITIES }} - DB_MIGRATIONS: ${{ secrets.DB_MIGRATIONS }} - DB_TYPE: 'postgres' - PROFILE: 'staging' - NODE_ENV: 'development' - PORT: 3000 - -jobs: - test-and-build-main: - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Set up Node.js - uses: actions/setup-node@v3 - with: - node-version: '18' - - - name: Install dependencies - run: npm install --include=dev - - - name: Build project - run: npm run build - - - name: Run tests - run: npm run test - - - name: Generate migrations - run: npm run migration:generate - - - name: Run migrations - run: npm run migration:run - - - name: Start application - run: | - npm run start:prod > app.log 2>&1 & - APP_PID=$! - echo $APP_PID - echo "Application started with PID $APP_PID" - sleep 30 # Wait for the application to start - tail -f app.log & - # Check application status with curl - if curl --retry 5 --retry-delay 5 --max-time 10 http://localhost:3000/health; then - echo "Application is up and running." - else - echo "Application failed to start. Logs:" - cat app.log - echo "Exiting workflow due to application failure." - kill $APP_PID - exit 1 - fi - - kill $APP_PID - echo "Application terminated Successfully." - - - name: Revert Migrations - run: npm run migration:revert - if: always() - - deploy-main: - runs-on: ubuntu-latest - if: github.event_name == 'push' - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Set up Node.js - uses: actions/setup-node@v3 - with: - node-version: '18' - - - name: Install dependencies - run: npm install --include=dev - - - name: Build project - run: npm run build - - - name: Run tests - run: npm run test - - - name: Deploying to virtual machine - uses: appleboy/ssh-action@v1.0.3 - with: - host: ${{ secrets.SERVER_HOST }} - username: ${{ secrets.SERVER_USER }} - # key: ${{ secrets.SERVER_PRIVATE_KEY }} - password: ${{ secrets.SERVER_PASSWORD }} - port: ${{ secrets.SERVER_PORT }} - script: | - echo "hello" - export PATH=$PATH:/home/teamalpha/.nvm/versions/node/v20.15.1/bin - bash ~/main-deployment.sh diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml deleted file mode 100644 index 0249e9603..000000000 --- a/.github/workflows/staging.yml +++ /dev/null @@ -1,112 +0,0 @@ -name: CI/CD--Staging - -on: - pull_request: - branches: - - staging - push: - branches: - - staging - -env: - DB_USERNAME: ${{ secrets.DB_USERNAME }} - DB_PASSWORD: ${{ secrets.DB_PASSWORD }} - DB_DATABASE: ${{ secrets.DB_DATABASE }} - DB_HOST: ${{ secrets.DB_HOST }} - DB_PORT: ${{ secrets.DB_PORT }} - DB_ENTITIES: ${{ secrets.DB_ENTITIES }} - DB_MIGRATIONS: ${{ secrets.DB_MIGRATIONS }} - DB_TYPE: 'postgres' - PROFILE: 'staging' - NODE_ENV: 'development' - PORT: 3000 - -jobs: - test-and-build-staging: - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Set up Node.js - uses: actions/setup-node@v3 - with: - node-version: '18' - - - name: Install dependencies - run: npm install --include=dev - - - name: Build project - run: npm run build - - - name: Run tests - run: npm run test - - - name: Generate migrations - run: npm run migration:generate - - - name: Run migrations - run: npm run migration:run - - - name: Start application - run: | - npm run start:prod > app.log 2>&1 & - APP_PID=$! - echo $APP_PID - echo "Application started with PID $APP_PID" - sleep 30 # Wait for the application to start - tail -f app.log & - # Check application status with curl - if curl --retry 5 --retry-delay 5 --max-time 10 http://localhost:3000/health; then - echo "Application is up and running." - else - echo "Application failed to start. Logs:" - cat app.log - echo "Exiting workflow due to application failure." - kill $APP_PID - exit 1 - fi - - kill $APP_PID - echo "Application terminated Successfully." - - - name: Revert Migrations - run: npm run migration:revert - if: always() - - deploy-staging: - runs-on: ubuntu-latest - if: github.event_name == 'push' - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Set up Node.js - uses: actions/setup-node@v3 - with: - node-version: '18' - - - name: Install dependencies - run: npm install --include=dev - - - name: Build project - run: npm run build - - - name: Run tests - run: npm run test - - - name: Deploying to virtual machine - uses: appleboy/ssh-action@v1.0.3 - with: - host: ${{ secrets.SERVER_HOST }} - username: ${{ secrets.SERVER_USER }} - # key: ${{ secrets.SERVER_PRIVATE_KEY }} - password: ${{ secrets.SERVER_PASSWORD }} - port: ${{ secrets.SERVER_PORT }} - script: | - echo "hello" - export PATH=$PATH:/home/teamalpha/.nvm/versions/node/v20.15.1/bin - bash ~/staging-deployment.sh diff --git a/.gitignore b/.gitignore index f85daeb0b..29697a8be 100644 --- a/.gitignore +++ b/.gitignore @@ -389,9 +389,14 @@ Temporary Items # Local dist /.env -/.env.*.local +/.env.* *._local.ts # Migrations /db/migrations/*-migration.ts -/db \ No newline at end of file +/db + +*.staging +*.dev +*.prod + diff --git a/.vscode/settings.json b/.vscode/settings.json index b347739d5..33d4973e1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,9 +10,14 @@ "statusBar.background": "#1a1f15", "statusBarItem.hoverBackground": "#333d2a", "statusBar.foreground": "#e7e7e7", - "panel.border": "#333d2a", - "sideBar.border": "#333d2a", - "editorGroup.border": "#333d2a" + "commandCenter.border": "#e7e7e799", + "sash.hoverBorder": "#333d2a", + "statusBarItem.remoteBackground": "#1a1f15", + "statusBarItem.remoteForeground": "#e7e7e7", + "titleBar.activeBackground": "#1a1f15", + "titleBar.activeForeground": "#e7e7e7", + "titleBar.inactiveBackground": "#1a1f1599", + "titleBar.inactiveForeground": "#e7e7e799" }, "peacock.color": "#1a1f15", "eslint.validate": [ @@ -28,6 +33,6 @@ "source.fixAll": "explicit" }, "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "dbaeumer.vscode-eslint" } } diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..cf03f6409 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +# Use the official Node.js image as the base image +FROM node:20-alpine + +# Set the working directory inside the container +WORKDIR /app + +# Copy the rest of the application code to the working directory +COPY . . + +# Install the dependencies +RUN npm install + +RUN npm run build + +EXPOSE 5000 + +# Command to run the application +CMD ["npm", "run", "start:prod"] diff --git a/compose/compose.dev.yaml b/compose/compose.dev.yaml new file mode 100644 index 000000000..555a3d039 --- /dev/null +++ b/compose/compose.dev.yaml @@ -0,0 +1,17 @@ +name: nestjs_dev + +services: + app: + env_file: + - .env.dev + + db: + env_file: + - .env.dev + + redis: + env_file: + - .env.dev + nginx: + ports: + - 5000:80 diff --git a/compose/compose.green.yaml b/compose/compose.green.yaml new file mode 100644 index 000000000..19e9a0913 --- /dev/null +++ b/compose/compose.green.yaml @@ -0,0 +1,23 @@ +services: + app-green: + image: ${COMPOSE_PROJECT_NAME}:green + build: . + env_file: + - .env.${ENV} + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: "wget -qO- http://localhost:${PORT}" + interval: 10s + timeout: 10s + retries: 3 + + nginx: + volumes: + - ./nginx/nginx-green.conf:/etc/nginx/nginx.conf + depends_on: + app-green: + condition: service_healthy diff --git a/compose/compose.override.yaml b/compose/compose.override.yaml new file mode 100644 index 000000000..a6e378b04 --- /dev/null +++ b/compose/compose.override.yaml @@ -0,0 +1,4 @@ +services: + nginx: + ports: + - 5000:80 diff --git a/compose/compose.prod.yaml b/compose/compose.prod.yaml new file mode 100644 index 000000000..b09ecd3f7 --- /dev/null +++ b/compose/compose.prod.yaml @@ -0,0 +1,17 @@ +name: nestjs_prod + +services: + app: + env_file: + - .env.prod + + db: + env_file: + - .env.prod + + redis: + env_file: + - .env.prod + nginx: + ports: + - 5002:80 diff --git a/compose/compose.staging.yaml b/compose/compose.staging.yaml new file mode 100644 index 000000000..d4cc2f434 --- /dev/null +++ b/compose/compose.staging.yaml @@ -0,0 +1,17 @@ +name: nestjs_staging + +services: + app: + env_file: + - .env.staging + + db: + env_file: + - .env.staging + + redis: + env_file: + - .env.staging + nginx: + ports: + - 5001:80 diff --git a/compose/compose.yaml b/compose/compose.yaml new file mode 100644 index 000000000..335a2e94f --- /dev/null +++ b/compose/compose.yaml @@ -0,0 +1,65 @@ +name: nestjs + +services: + app: + image: ${COMPOSE_PROJECT_NAME} + build: . + env_file: + - .env + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: 'wget -qO- http://app:${PORT}' + interval: 10s + timeout: 10s + retries: 3 + + db: + image: postgres:16-alpine + env_file: + - .env + environment: + - POSTGRES_USER=${DB_USERNAME} + - POSTGRES_PASSWORD=${DB_PASSWORD} + - POSTGRES_DB=${DB_NAME} + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: 'pg_isready -U postgres' + interval: 5s + timeout: 5s + retries: 3 + restart: always + + redis: + image: redis:7-alpine + env_file: + - .env + volumes: + - redis_data:/data + healthcheck: + test: 'redis-cli ping | grep PONG' + interval: 5s + timeout: 5s + retries: 3 + restart: always + + nginx: + image: nginx:alpine + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf + depends_on: + app: + condition: service_healthy + healthcheck: + test: 'wget -qO- http://nginx:80' + interval: 5s + timeout: 5s + retries: 3 + +volumes: + postgres_data: + redis_data: diff --git a/compose/deploy.sh b/compose/deploy.sh new file mode 100755 index 000000000..f14695a98 --- /dev/null +++ b/compose/deploy.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +set -e + +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +if [ -z "$1" ]; then + echo -e "${RED}Usage: $0 ${NC}" + exit 1 +elif [[ "$1" != "dev" && "$1" != "staging" && "$1" != "prod" ]]; then + echo -e "${RED}Invalid environment specified. Use dev, staging, or prod.${NC}" + exit 1 +fi + +export ENV=$1 +BRANCH=$1 +PROJECT_NAME="nestjs_$ENV" + +if [ "$ENV" == "prod" ]; then + BRANCH="main" +fi + +echo -e "${BLUE}Preparing to deploy to the $ENV environment...${NC}" +echo -e "${YELLOW}Environment: $ENV${NC}" +echo -e "${YELLOW}Branch: $BRANCH${NC}" + +echo -e "${GREEN}Loading Docker image from /tmp/nestjs_${ENV}.tar.gz...${NC}" +gunzip -c /tmp/nestjs_${ENV}.tar.gz | docker load +rm -f /tmp/nestjs_${ENV}.tar.gz + +echo -e "${GREEN}Stashing local changes and Pulling the latest changes from branch $BRANCH...${NC}" +git add . +git stash +git checkout $BRANCH +git pull origin $BRANCH + +echo -e "${BLUE}Starting Blue-Green deployment for environment: $ENV...${NC}" + +echo -e "${GREEN}Deploying the green version of the app...${NC}" +docker compose -f compose.yaml -f compose/compose.$ENV.yaml -f compose/compose.green.yaml up -d --no-recreate + +echo -e "${GREEN}Transferring traffic to green environment...${NC}" +docker compose -f compose.yaml -f compose/compose.$ENV.yaml -f compose/compose.green.yaml create nginx + +echo -e "${YELLOW}Cleaning up the blue (old) containers and image...${NC}" +docker compose -f compose.yaml -f compose/compose.$ENV.yaml stop app +docker compose -f compose.yaml -f compose/compose.$ENV.yaml rm -f app +docker rmi -f ${PROJECT_NAME}:latest + +echo -e "${GREEN}Promoting the green version to blue (main version)...${NC}" +docker tag ${PROJECT_NAME}:green ${PROJECT_NAME}:latest + +echo -e "${BLUE}Starting the blue (main) version of the app...${NC}" +docker compose -f compose.yaml -f compose/compose.$ENV.yaml up -d + +echo -e "${YELLOW}Cleaning up the green version after promotion...${NC}" +docker compose -f compose.yaml -f compose/compose.$ENV.yaml -f compose/compose.green.yaml stop app-green +docker compose -f compose.yaml -f compose/compose.$ENV.yaml -f compose/compose.green.yaml rm -f app-green +docker rmi -f ${PROJECT_NAME}:green + +echo -e "${GREEN}Blue-Green deployment complete. The blue version is now live.${NC}" diff --git a/config/auth.config.ts b/config/auth.config.ts index b5a3ffb2e..5af6a828b 100644 --- a/config/auth.config.ts +++ b/config/auth.config.ts @@ -9,4 +9,10 @@ export default registerAs('auth', () => ({ clientID: process.env.GOOGLE_CLIENT_ID, callbackURL: process.env.GOOGLE_REDIRECT_URI, }, + redis: { + host: process.env.REDIS_HOST, + port: process.env.REDIS_PORT || 6379, + password: process.env.REDIS_PASSWORD, + username: process.env.REDIS_USERNAME, + }, })); diff --git a/deployment.sh b/deployment.sh new file mode 100644 index 000000000..d23d9bd37 --- /dev/null +++ b/deployment.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +BRANCH=$1 + +# checkout +git checkout $BRANCH; + +git pull origin $BRANCH; + +# install dependencies +npm install --include=dev; + +# run build +npm run build; + + +git stash + +# run migration +npm run migration:run; + +# run start +pm2 restart $BRANCH-ecosystem-config.json || pm2 start $BRANCH-ecosystem-config.json; diff --git a/main-ecosystem-config.json b/main-ecosystem-config.json index 220d76794..d31030f0d 100644 --- a/main-ecosystem-config.json +++ b/main-ecosystem-config.json @@ -1,11 +1,11 @@ { "apps": [ { - "name": "team-alpha-prod", + "name": "nestjs-prod", "script": "npm run start:prod", "log_file": "~/.pm2/logs/team-alpha-prod-out.log", "combine_logs": true, "log_date_format": "YYYY-MM-DD HH:mm:ss Z" } ] -} \ No newline at end of file +} diff --git a/nginx/nginx-green.conf b/nginx/nginx-green.conf new file mode 100644 index 000000000..19805c30b --- /dev/null +++ b/nginx/nginx-green.conf @@ -0,0 +1,16 @@ +worker_processes 1; + +events { worker_connections 1024; } + +http { + + resolver 127.0.0.11 valid=1s; + + server { + listen 80; + + location / { + proxy_pass http://app-green:5000; + } + } +} diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 000000000..1a86ce57b --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,16 @@ +worker_processes 1; + +events { worker_connections 1024; } + +http { + + resolver 127.0.0.11 valid=1s; + + server { + listen 80; + + location / { + proxy_pass http://app:5000; + } + } +} diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 24e16ce30..000000000 --- a/package-lock.json +++ /dev/null @@ -1,20428 +0,0 @@ -{ - "name": "hng_boilerplate_nestjs", - "version": "0.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "hng_boilerplate_nestjs", - "version": "0.0.1", - "license": "UNLICENSED", - "dependencies": { - "@css-inline/css-inline": "^0.14.1", - "@faker-js/faker": "^8.4.1", - "@nestjs-modules/mailer": "^2.0.2", - "@nestjs/bull": "^10.2.0", - "@nestjs/config": "^3.2.3", - "@nestjs/core": "^10.0.0", - "@nestjs/jwt": "^10.2.0", - "@nestjs/passport": "^10.0.3", - "@nestjs/platform-express": "^10.3.9", - "@nestjs/swagger": "^7.4.0", - "@nestjs/typeorm": "^10.0.2", - "@types/nodemailer": "^6.4.15", - "@types/speakeasy": "^2.0.10", - "bcrypt": "^5.1.1", - "bcryptjs": "^2.4.3", - "bull": "^4.15.1", - "class-transformer": "^0.5.1", - "class-validator": "^0.14.1", - "google-auth-library": "^9.12.0", - "handlebars": "^4.7.8", - "html-validator": "^6.0.1", - "ioredis": "^5.4.1", - "joi": "^17.6.0", - "nestjs-pino": "^4.1.0", - "nodemailer": "^6.9.14", - "passport": "^0.7.0", - "passport-google-oauth20": "^2.0.0", - "passport-jwt": "^4.0.1", - "pg": "^8.12.0", - "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1", - "speakeasy": "^2.0.0", - "supertest": "^7.0.0", - "typeorm": "^0.3.20", - "typeorm-extension": "^3.5.1", - "types-joi": "^2.1.0", - "uuid": "^10.0.0" - }, - "devDependencies": { - "@commitlint/cli": "^19.3.0", - "@commitlint/config-conventional": "^19.2.2", - "@nestjs-modules/mailer": "^2.0.2", - "@nestjs/cli": "^10.4.2", - "@nestjs/common": "^10.3.10", - "@nestjs/schematics": "^10.1.3", - "@nestjs/testing": "^10.3.10", - "@types/bcrypt": "^5.0.2", - "@types/express": "^4.17.17", - "@types/jest": "^29.5.2", - "@types/node": "^20.3.1", - "@types/passport-google-oauth2": "^0.1.8", - "@types/passport-jwt": "^4.0.1", - "@types/speakeasy": "^2.0.10", - "@types/supertest": "^6.0.0", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", - "eslint": "^8.42.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-prettier": "^5.0.0", - "husky": "^9.0.11", - "jest": "^29.7.0", - "lint-staged": "^15.2.5", - "prettier": "^3.0.0", - "source-map-support": "^0.5.21", - "ts-jest": "^29.2.3", - "ts-loader": "^9.4.3", - "ts-node": "^10.9.2", - "ts-node-dev": "^2.0.0", - "tsconfig-paths": "^4.2.0", - "typescript": "^5.5.4" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "devOptional": 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.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.8.tgz", - "integrity": "sha512-Q8q0voCGudbdCgJ7lXdnyaxKHbNQBARH68zPQV72WT8NWy+Gw/tys870i6L58NWbBaCJEUcIj/kb6KoakSRu+Q==", - "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/core/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/@angular-devkit/schematics": { - "version": "17.3.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.8.tgz", - "integrity": "sha512-QRVEYpIfgkprNHc916JlPuNbLzOgrm9DZalHasnLUz4P6g7pR21olb8YCyM2OTJjombNhya9ZpckcADU5Qyvlg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "17.3.8", - "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.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-17.3.8.tgz", - "integrity": "sha512-TjmiwWJarX7oqvNiRAroQ5/LeKUatxBOCNEuKXO/PV8e7pn/Hr/BqfFm+UcYrQoFdZplmtNAfqmbqgVziKvCpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "17.3.8", - "@angular-devkit/schematics": "17.3.8", - "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/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.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", - "license": "MIT", - "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz", - "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", - "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/helper-compilation-targets": "^7.25.2", - "@babel/helper-module-transforms": "^7.25.2", - "@babel/helpers": "^7.25.0", - "@babel/parser": "^7.25.0", - "@babel/template": "^7.25.0", - "@babel/traverse": "^7.25.2", - "@babel/types": "^7.25.2", - "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==", - "devOptional": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", - "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.25.0", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", - "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.25.2", - "@babel/helper-validator-option": "^7.24.8", - "browserslist": "^4.23.1", - "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==", - "devOptional": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", - "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "@babel/traverse": "^7.25.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", - "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", - "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", - "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", - "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", - "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/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==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/parser": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", - "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.25.2" - }, - "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==", - "devOptional": 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==", - "devOptional": 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==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "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==", - "devOptional": 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==", - "devOptional": 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.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", - "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "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==", - "devOptional": 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==", - "devOptional": 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==", - "devOptional": 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==", - "devOptional": 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==", - "devOptional": 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==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.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==", - "devOptional": 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.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", - "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", - "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", - "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.25.0", - "@babel/types": "^7.25.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz", - "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/parser": "^7.25.3", - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.2", - "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==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/types": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", - "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" - }, - "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==", - "devOptional": 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/@commitlint/cli": { - "version": "19.3.0", - "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-19.3.0.tgz", - "integrity": "sha512-LgYWOwuDR7BSTQ9OLZ12m7F/qhNY+NpAyPBgo4YNMkACE7lGuUnuQq1yi9hz1KA4+3VqpOYl8H1rY/LYK43v7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/format": "^19.3.0", - "@commitlint/lint": "^19.2.2", - "@commitlint/load": "^19.2.0", - "@commitlint/read": "^19.2.1", - "@commitlint/types": "^19.0.3", - "execa": "^8.0.1", - "yargs": "^17.0.0" - }, - "bin": { - "commitlint": "cli.js" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/config-conventional": { - "version": "19.2.2", - "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-19.2.2.tgz", - "integrity": "sha512-mLXjsxUVLYEGgzbxbxicGPggDuyWNkf25Ht23owXIH+zV2pv1eJuzLK3t1gDY5Gp6pxdE60jZnWUY5cvgL3ufw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/types": "^19.0.3", - "conventional-changelog-conventionalcommits": "^7.0.2" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/config-validator": { - "version": "19.0.3", - "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-19.0.3.tgz", - "integrity": "sha512-2D3r4PKjoo59zBc2auodrSCaUnCSALCx54yveOFwwP/i2kfEAQrygwOleFWswLqK0UL/F9r07MFi5ev2ohyM4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/types": "^19.0.3", - "ajv": "^8.11.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/ensure": { - "version": "19.0.3", - "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-19.0.3.tgz", - "integrity": "sha512-SZEpa/VvBLoT+EFZVb91YWbmaZ/9rPH3ESrINOl0HD2kMYsjvl0tF7nMHh0EpTcv4+gTtZBAe1y/SS6/OhfZzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/types": "^19.0.3", - "lodash.camelcase": "^4.3.0", - "lodash.kebabcase": "^4.1.1", - "lodash.snakecase": "^4.1.1", - "lodash.startcase": "^4.4.0", - "lodash.upperfirst": "^4.3.1" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/execute-rule": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-19.0.0.tgz", - "integrity": "sha512-mtsdpY1qyWgAO/iOK0L6gSGeR7GFcdW7tIjcNFxcWkfLDF5qVbPHKuGATFqRMsxcO8OUKNj0+3WOHB7EHm4Jdw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/format": { - "version": "19.3.0", - "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-19.3.0.tgz", - "integrity": "sha512-luguk5/aF68HiF4H23ACAfk8qS8AHxl4LLN5oxPc24H+2+JRPsNr1OS3Gaea0CrH7PKhArBMKBz5RX9sA5NtTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/types": "^19.0.3", - "chalk": "^5.3.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/is-ignored": { - "version": "19.2.2", - "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-19.2.2.tgz", - "integrity": "sha512-eNX54oXMVxncORywF4ZPFtJoBm3Tvp111tg1xf4zWXGfhBPKpfKG6R+G3G4v5CPlRROXpAOpQ3HMhA9n1Tck1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/types": "^19.0.3", - "semver": "^7.6.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/lint": { - "version": "19.2.2", - "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-19.2.2.tgz", - "integrity": "sha512-xrzMmz4JqwGyKQKTpFzlN0dx0TAiT7Ran1fqEBgEmEj+PU98crOFtysJgY+QdeSagx6EDRigQIXJVnfrI0ratA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/is-ignored": "^19.2.2", - "@commitlint/parse": "^19.0.3", - "@commitlint/rules": "^19.0.3", - "@commitlint/types": "^19.0.3" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/load": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-19.2.0.tgz", - "integrity": "sha512-XvxxLJTKqZojCxaBQ7u92qQLFMMZc4+p9qrIq/9kJDy8DOrEa7P1yx7Tjdc2u2JxIalqT4KOGraVgCE7eCYJyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/config-validator": "^19.0.3", - "@commitlint/execute-rule": "^19.0.0", - "@commitlint/resolve-extends": "^19.1.0", - "@commitlint/types": "^19.0.3", - "chalk": "^5.3.0", - "cosmiconfig": "^9.0.0", - "cosmiconfig-typescript-loader": "^5.0.0", - "lodash.isplainobject": "^4.0.6", - "lodash.merge": "^4.6.2", - "lodash.uniq": "^4.5.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/message": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-19.0.0.tgz", - "integrity": "sha512-c9czf6lU+9oF9gVVa2lmKaOARJvt4soRsVmbR7Njwp9FpbBgste5i7l/2l5o8MmbwGh4yE1snfnsy2qyA2r/Fw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/parse": { - "version": "19.0.3", - "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-19.0.3.tgz", - "integrity": "sha512-Il+tNyOb8VDxN3P6XoBBwWJtKKGzHlitEuXA5BP6ir/3loWlsSqDr5aecl6hZcC/spjq4pHqNh0qPlfeWu38QA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/types": "^19.0.3", - "conventional-changelog-angular": "^7.0.0", - "conventional-commits-parser": "^5.0.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/read": { - "version": "19.2.1", - "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-19.2.1.tgz", - "integrity": "sha512-qETc4+PL0EUv7Q36lJbPG+NJiBOGg7SSC7B5BsPWOmei+Dyif80ErfWQ0qXoW9oCh7GTpTNRoaVhiI8RbhuaNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/top-level": "^19.0.0", - "@commitlint/types": "^19.0.3", - "execa": "^8.0.1", - "git-raw-commits": "^4.0.0", - "minimist": "^1.2.8" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/resolve-extends": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-19.1.0.tgz", - "integrity": "sha512-z2riI+8G3CET5CPgXJPlzftH+RiWYLMYv4C9tSLdLXdr6pBNimSKukYP9MS27ejmscqCTVA4almdLh0ODD2KYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/config-validator": "^19.0.3", - "@commitlint/types": "^19.0.3", - "global-directory": "^4.0.1", - "import-meta-resolve": "^4.0.0", - "lodash.mergewith": "^4.6.2", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/rules": { - "version": "19.0.3", - "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-19.0.3.tgz", - "integrity": "sha512-TspKb9VB6svklxNCKKwxhELn7qhtY1rFF8ls58DcFd0F97XoG07xugPjjbVnLqmMkRjZDbDIwBKt9bddOfLaPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/ensure": "^19.0.3", - "@commitlint/message": "^19.0.0", - "@commitlint/to-lines": "^19.0.0", - "@commitlint/types": "^19.0.3", - "execa": "^8.0.1" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/to-lines": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-19.0.0.tgz", - "integrity": "sha512-vkxWo+VQU5wFhiP9Ub9Sre0FYe019JxFikrALVoD5UGa8/t3yOJEpEhxC5xKiENKKhUkTpEItMTRAjHw2SCpZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/top-level": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-19.0.0.tgz", - "integrity": "sha512-KKjShd6u1aMGNkCkaX4aG1jOGdn7f8ZI8TR1VEuNqUOjWTOdcDSsmglinglJ18JTjuBX5I1PtjrhQCRcixRVFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^7.0.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/types": { - "version": "19.0.3", - "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-19.0.3.tgz", - "integrity": "sha512-tpyc+7i6bPG9mvaBbtKUeghfyZSDgWquIDfMgqYtTbmZ9Y9VzEm2je9EYcQ0aoz5o7NvGS+rcDec93yO08MHYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/conventional-commits-parser": "^5.0.0", - "chalk": "^5.3.0" - }, - "engines": { - "node": ">=v18" - } - }, - "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/@css-inline/css-inline": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline/-/css-inline-0.14.1.tgz", - "integrity": "sha512-u4eku+hnPqqHIGq/ZUQcaP0TrCbYeLIYBaK7qClNRGZbnh8RC4gVxLEIo8Pceo1nOK9E5G4Lxzlw5KnXcvflfA==", - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@css-inline/css-inline-android-arm-eabi": "0.14.1", - "@css-inline/css-inline-android-arm64": "0.14.1", - "@css-inline/css-inline-darwin-arm64": "0.14.1", - "@css-inline/css-inline-darwin-x64": "0.14.1", - "@css-inline/css-inline-linux-arm-gnueabihf": "0.14.1", - "@css-inline/css-inline-linux-arm64-gnu": "0.14.1", - "@css-inline/css-inline-linux-arm64-musl": "0.14.1", - "@css-inline/css-inline-linux-x64-gnu": "0.14.1", - "@css-inline/css-inline-linux-x64-musl": "0.14.1", - "@css-inline/css-inline-win32-x64-msvc": "0.14.1" - } - }, - "node_modules/@css-inline/css-inline-android-arm-eabi": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline-android-arm-eabi/-/css-inline-android-arm-eabi-0.14.1.tgz", - "integrity": "sha512-LNUR8TY4ldfYi0mi/d4UNuHJ+3o8yLQH9r2Nt6i4qeg1i7xswfL3n/LDLRXvGjBYqeEYNlhlBQzbPwMX1qrU6A==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@css-inline/css-inline-android-arm64": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline-android-arm64/-/css-inline-android-arm64-0.14.1.tgz", - "integrity": "sha512-tH5us0NYGoTNBHOUHVV7j9KfJ4DtFOeTLA3cM0XNoMtArNu2pmaaBMFJPqECzavfXkLc7x5Z22UPZYjoyHfvCA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@css-inline/css-inline-darwin-arm64": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline-darwin-arm64/-/css-inline-darwin-arm64-0.14.1.tgz", - "integrity": "sha512-QE5W1YRIfRayFrtrcK/wqEaxNaqLULPI0gZB4ArbFRd3d56IycvgBasDTHPre5qL2cXCO3VyPx+80XyHOaVkag==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@css-inline/css-inline-darwin-x64": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline-darwin-x64/-/css-inline-darwin-x64-0.14.1.tgz", - "integrity": "sha512-mAvv2sN8awNFsbvBzlFkZPbCNZ6GCWY5/YcIz7V5dPYw+bHHRbjnlkNTEZq5BsDxErVrMIGvz05PGgzuNvZvdQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@css-inline/css-inline-linux-arm-gnueabihf": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline-linux-arm-gnueabihf/-/css-inline-linux-arm-gnueabihf-0.14.1.tgz", - "integrity": "sha512-AWC44xL0X7BgKvrWEqfSqkT2tJA5kwSGrAGT+m0gt11wnTYySvQ6YpX0fTY9i3ppYGu4bEdXFjyK2uY1DTQMHA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@css-inline/css-inline-linux-arm64-gnu": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline-linux-arm64-gnu/-/css-inline-linux-arm64-gnu-0.14.1.tgz", - "integrity": "sha512-drj0ciiJgdP3xKXvNAt4W+FH4KKMs8vB5iKLJ3HcH07sNZj58Sx++2GxFRS1el3p+GFp9OoYA6dgouJsGEqt0Q==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@css-inline/css-inline-linux-arm64-musl": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline-linux-arm64-musl/-/css-inline-linux-arm64-musl-0.14.1.tgz", - "integrity": "sha512-FzknI+st8eA8YQSdEJU9ykcM0LZjjigBuynVF5/p7hiMm9OMP8aNhWbhZ8LKJpKbZrQsxSGS4g9Vnr6n6FiSdQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@css-inline/css-inline-linux-x64-gnu": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline-linux-x64-gnu/-/css-inline-linux-x64-gnu-0.14.1.tgz", - "integrity": "sha512-yubbEye+daDY/4vXnyASAxH88s256pPati1DfVoZpU1V0+KP0BZ1dByZOU1ktExurbPH3gZOWisAnBE9xon0Uw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@css-inline/css-inline-linux-x64-musl": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline-linux-x64-musl/-/css-inline-linux-x64-musl-0.14.1.tgz", - "integrity": "sha512-6CRAZzoy1dMLPC/tns2rTt1ZwPo0nL/jYBEIAsYTCWhfAnNnpoLKVh5Nm+fSU3OOwTTqU87UkGrFJhObD/wobQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@css-inline/css-inline-win32-x64-msvc": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline-win32-x64-msvc/-/css-inline-win32-x64-msvc-0.14.1.tgz", - "integrity": "sha512-nzotGiaiuiQW78EzsiwsHZXbxEt6DiMUFcDJ6dhiliomXxnlaPyBfZb6/FMBgRJOf6sknDt/5695OttNmbMYzg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", - "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/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.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@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/@html-validate/stylish": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@html-validate/stylish/-/stylish-2.0.1.tgz", - "integrity": "sha512-iRLjgQnNq66rcsTukun6KwMhPEoUV2R3atPbTSapnEvD1aETjD+pfS+1yYrmaPeJFgXHzfsSYjAuyUVq7EID/Q==", - "dependencies": { - "kleur": "^4.0.0", - "text-table": "^0.2.0" - }, - "engines": { - "node": ">= 12.0" - } - }, - "node_modules/@html-validate/stylish/node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/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/@ioredis/commands": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", - "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" - }, - "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.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "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==", - "devOptional": 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==", - "devOptional": 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==", - "devOptional": 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==", - "devOptional": 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==", - "devOptional": 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==", - "devOptional": 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==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/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==", - "devOptional": 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==", - "devOptional": 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/console/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/console/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/console/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/console/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@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/core/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/core/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/core/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/core/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@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/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@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/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/reporters/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/reporters/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@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/transform/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/transform/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/transform/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/transform/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@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/@jest/types/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/types/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/types/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/types/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@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==", - "devOptional": 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==", - "devOptional": 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==", - "devOptional": 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/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", - "license": "BSD-3-Clause", - "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, - "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/@msgpackr-extract/msgpackr-extract-darwin-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", - "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", - "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", - "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", - "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", - "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", - "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@nestjs-modules/mailer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@nestjs-modules/mailer/-/mailer-2.0.2.tgz", - "integrity": "sha512-+z4mADQasg0H1ZaGu4zZTuKv2pu+XdErqx99PLFPzCDNTN/q9U59WPgkxVaHnsvKHNopLj5Xap7G4ZpptduoYw==", - "dev": true, - "dependencies": { - "@css-inline/css-inline": "0.14.1", - "glob": "10.3.12" - }, - "optionalDependencies": { - "@types/ejs": "^3.1.5", - "@types/mjml": "^4.7.4", - "@types/pug": "^2.0.10", - "ejs": "^3.1.10", - "handlebars": "^4.7.8", - "liquidjs": "^10.11.1", - "mjml": "^4.15.3", - "preview-email": "^3.0.19", - "pug": "^3.0.2" - }, - "peerDependencies": { - "@nestjs/common": ">=7.0.9", - "@nestjs/core": ">=7.0.9", - "@types/ejs": ">=3.0.3", - "@types/mjml": ">=4.7.4", - "@types/pug": ">=2.0.6", - "ejs": ">=3.1.2", - "handlebars": ">=4.7.6", - "liquidjs": ">=10.8.2", - "mjml": ">=4.15.3", - "nodemailer": ">=6.4.6", - "preview-email": ">=3.0.19", - "pug": ">=3.0.1" - } - }, - "node_modules/@nestjs/bull": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/@nestjs/bull/-/bull-10.2.0.tgz", - "integrity": "sha512-byQI3cgAVP8BNa71h5g33D1pNwOaK2O9+zYmT98eU8LxxJHh53aWMwEUrCHrwgSz4P8bD+jY8ObQ5vtU4SsidA==", - "dependencies": { - "@nestjs/bull-shared": "^10.2.0", - "tslib": "2.6.3" - }, - "peerDependencies": { - "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", - "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0", - "bull": "^3.3 || ^4.0.0" - } - }, - "node_modules/@nestjs/bull-shared": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.2.0.tgz", - "integrity": "sha512-cSi6CyPECHDFumnHWWfwLCnbc6hm5jXt7FqzJ0Id6EhGqdz5ja0FmgRwXoS4xoMA2RRjlxn2vGXr4YOaHBAeig==", - "dependencies": { - "tslib": "2.6.3" - }, - "peerDependencies": { - "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", - "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0" - } - }, - "node_modules/@nestjs/cli": { - "version": "10.4.2", - "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.2.tgz", - "integrity": "sha512-fQexIfLHfp6GUgX+CO4fOg+AEwV5ox/LHotQhyZi9wXUQDyIqS0NTTbumr//62EcX35qV4nU0359nYnuEdzG+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "17.3.8", - "@angular-devkit/schematics": "17.3.8", - "@angular-devkit/schematics-cli": "17.3.8", - "@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.3.3", - "webpack": "5.92.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", - "@swc/core": "^1.3.62" - }, - "peerDependenciesMeta": { - "@swc/cli": { - "optional": true - }, - "@swc/core": { - "optional": true - } - } - }, - "node_modules/@nestjs/cli/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@nestjs/cli/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@nestjs/cli/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@nestjs/cli/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@nestjs/cli/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/@nestjs/cli/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/@nestjs/cli/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==", - "dev": true, - "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/@nestjs/cli/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==", - "dev": true, - "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/@nestjs/cli/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@nestjs/cli/node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/@nestjs/cli/node_modules/webpack": { - "version": "5.92.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.1.tgz", - "integrity": "sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.7.1", - "acorn-import-attributes": "^1.9.5", - "browserslist": "^4.21.10", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.0", - "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/@nestjs/common": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.10.tgz", - "integrity": "sha512-H8k0jZtxk1IdtErGDmxFRy0PfcOAUg41Prrqpx76DQusGGJjsaovs1zjXVD1rZWaVYchfT1uczJ6L4Kio10VNg==", - "license": "MIT", - "dependencies": { - "iterare": "1.2.1", - "tslib": "2.6.3", - "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.2.3", - "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.3.tgz", - "integrity": "sha512-p6yv/CvoBewJ72mBq4NXgOAi2rSQNWx3a+IMJLVKS2uiwFCOQQuiIatGwq6MRjXV3Jr+B41iUO8FIf4xBrZ4/w==", - "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.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.10.tgz", - "integrity": "sha512-ZbQ4jovQyzHtCGCrzK5NdtW1SYO2fHSsgSY1+/9WdruYCUra+JDkWEXgZ4M3Hv480Dl3OXehAmY1wCOojeMyMQ==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@nuxtjs/opencollective": "0.3.2", - "fast-safe-stringify": "2.1.1", - "iterare": "1.2.1", - "path-to-regexp": "3.2.0", - "tslib": "2.6.3", - "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.5", - "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz", - "integrity": "sha512-bSJv4pd6EY99NX9CjBIyn4TVDoSit82DUZlL4I3bqNfy5Gt+gXTa86i3I/i0iIV9P4hntcGM5GyO+FhZAhxtyg==", - "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/passport": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-10.0.3.tgz", - "integrity": "sha512-znJ9Y4S8ZDVY+j4doWAJ8EuuVO7SkQN3yOBmzxbGaXbvcSwFDAdGJ+OMCg52NdzIO4tQoN4pYKx8W6M0ArfFRQ==", - "license": "MIT", - "peerDependencies": { - "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", - "passport": "^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0" - } - }, - "node_modules/@nestjs/platform-express": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.10.tgz", - "integrity": "sha512-wK2ow3CZI2KFqWeEpPmoR300OB6BcBLxARV1EiClJLCj4S1mZsoCmS0YWgpk3j1j6mo0SI8vNLi/cC2iZPEPQA==", - "license": "MIT", - "dependencies": { - "body-parser": "1.20.2", - "cors": "2.8.5", - "express": "4.19.2", - "multer": "1.4.4-lts.1", - "tslib": "2.6.3" - }, - "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.1.3", - "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.1.3.tgz", - "integrity": "sha512-aLJ4Nl/K/u6ZlgLa0NjKw5CuBOIgc6vudF42QvmGueu5FaMGM6IJrAuEvB5T2kr0PAfVwYmDFBBHCWdYhTw4Tg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "17.3.8", - "@angular-devkit/schematics": "17.3.8", - "comment-json": "4.2.3", - "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": "7.4.0", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.4.0.tgz", - "integrity": "sha512-dCiwKkRxcR7dZs5jtrGspBAe/nqJd1AYzOBTzw9iCdbq3BGrLpwokelk6lFZPe4twpTsPQqzNKBwKzVbI6AR/g==", - "license": "MIT", - "dependencies": { - "@microsoft/tsdoc": "^0.15.0", - "@nestjs/mapped-types": "2.0.5", - "js-yaml": "4.1.0", - "lodash": "4.17.21", - "path-to-regexp": "3.2.0", - "swagger-ui-dist": "5.17.14" - }, - "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.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.10.tgz", - "integrity": "sha512-i3HAtVQJijxNxJq1k39aelyJlyEIBRONys7IipH/4r8W0J+M1V+y5EKDOyi4j1SdNSb/vmNyWpZ2/ewZjl3kRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "2.6.3" - }, - "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/@nestjs/typeorm/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/@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/@nuxtjs/opencollective/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/@nuxtjs/opencollective/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/@nuxtjs/opencollective/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/@nuxtjs/opencollective/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/@one-ini/wasm": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", - "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "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/@selderee/plugin-htmlparser2": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", - "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "domhandler": "^5.0.3", - "selderee": "^0.11.0" - }, - "funding": { - "url": "https://ko-fi.com/killymxi" - } - }, - "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/@sidvind/better-ajv-errors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@sidvind/better-ajv-errors/-/better-ajv-errors-1.1.1.tgz", - "integrity": "sha512-CXnmMcV4QoyWuFA0zlDk0AWMHftaMFAIFWz68AH4EXOO2iUEq0gsonJEhY3OjM08xHYobqqDeCAPPEsL5E+8QA==", - "dependencies": { - "@babel/code-frame": "^7.16.0", - "chalk": "^4.1.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "peerDependencies": { - "ajv": "4.11.8 - 8" - } - }, - "node_modules/@sidvind/better-ajv-errors/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==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@sidvind/better-ajv-errors/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "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/@sidvind/better-ajv-errors/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==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@sidvind/better-ajv-errors/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==" - }, - "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==", - "devOptional": 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==", - "devOptional": 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==", - "devOptional": 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==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.20.7" - } - }, - "node_modules/@types/bcrypt": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", - "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "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/conventional-commits-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", - "integrity": "sha512-loB369iXNmAZglwWATL+WRe+CRMmmBPtpolYzIebFaX4YA3x+BEfLqhUAV9WanycKI3TG1IMr5bMJDajDKLlUQ==", - "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/ejs": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.5.tgz", - "integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@types/eslint": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.0.tgz", - "integrity": "sha512-gi6WQJ7cHRgZxtkQEoyHMppPjq9Kxo5Tjn2prSKDSmZrCz8TZ3jSRCeTJm+WoM+oB0WG37bRqLzaaU3q7JypGg==", - "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.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/express": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", - "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.5", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", - "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/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==", - "devOptional": 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==", - "devOptional": 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==", - "devOptional": 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==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/jest": { - "version": "29.5.12", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", - "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", - "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/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "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/mjml": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/@types/mjml/-/mjml-4.7.4.tgz", - "integrity": "sha512-vyi1vzWgMzFMwZY7GSZYX0GU0dmtC8vLHwpgk+NWmwbwRSrlieVyJ9sn5elodwUfklJM7yGl0zQeet1brKTWaQ==", - "dev": true, - "optional": true, - "dependencies": { - "@types/mjml-core": "*" - } - }, - "node_modules/@types/mjml-core": { - "version": "4.15.0", - "resolved": "https://registry.npmjs.org/@types/mjml-core/-/mjml-core-4.15.0.tgz", - "integrity": "sha512-jSRWTOpwRS/uHIBfGdvLl0a7MaoBZZYHKI+HhsFYChrUOKVJTnjSYsuV6wx0snv6ZaX3TUo5OP/gNsz/uzZz1A==", - "dev": true, - "optional": true - }, - "node_modules/@types/node": { - "version": "20.14.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.13.tgz", - "integrity": "sha512-+bHoGiZb8UiQ0+WEtmph2IWQCjIqg8MDZMAV+ppRRhUZnquF5mQkP/9vpSwJClEiSM/C7fZZExPzfU0vJTyp8w==", - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/nodemailer": { - "version": "6.4.15", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.15.tgz", - "integrity": "sha512-0EBJxawVNjPkng1zm2vopRctuWVCxk34JcIlRuXSf54habUWdz1FB7wHDqOqvDa8Mtpt0Q3LTXQkAs2LNyK5jQ==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/passport": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.16.tgz", - "integrity": "sha512-FD0qD5hbPWQzaM0wHUnJ/T0BBCJBxCeemtnCwc/ThhTg3x9jfrAcRUmj5Dopza+MfFS9acTe3wk7rcVnRIp/0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/passport-google-oauth2": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@types/passport-google-oauth2/-/passport-google-oauth2-0.1.8.tgz", - "integrity": "sha512-0lgGOVbGNTw7NcmrPh3pRwtGJXf1XZG7FZkYtM5DuQ8HJjHzi5mMIq28x7v/q5NNDIrFvhjE0CnyTv7hn4DIjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/passport-jwt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", - "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/jsonwebtoken": "*", - "@types/passport-strategy": "*" - } - }, - "node_modules/@types/passport-strategy": { - "version": "0.2.38", - "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", - "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express": "*", - "@types/passport": "*" - } - }, - "node_modules/@types/prettier": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", - "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", - "optional": true, - "peer": true - }, - "node_modules/@types/pug": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz", - "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@types/qs": { - "version": "6.9.15", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", - "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/semver": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", - "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/speakeasy": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@types/speakeasy/-/speakeasy-2.0.10.tgz", - "integrity": "sha512-QVRlDW5r4yl7p7xkNIbAIC/JtyOcClDIIdKfuG7PWdDT1MmyhtXSANsildohy0K+Lmvf/9RUtLbNLMacvrVwxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "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==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@types/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/strip-json-comments": { - "version": "0.0.30", - "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", - "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/superagent": { - "version": "8.1.8", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.8.tgz", - "integrity": "sha512-nTqHJ2OTa7PFEpLahzSEEeFeqbMpmcN7OeayiOc7v+xk+/vyTKljRe+o4MPqSnPeRCMvtxuLG+5QqluUVQJOnA==", - "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.0", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.0.tgz", - "integrity": "sha512-nH45Lk7oPIJ1RVOF6JgFI6Dy0QpHEzq4QecZhvguxYPDwT8c93prCMqAtiIttm39voZ+DDR+qkNnMpJmMBRqag==", - "license": "MIT" - }, - "node_modules/@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "devOptional": 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==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", - "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/type-utils": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.4", - "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", - "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", - "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "semver": "^7.5.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@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.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", - "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", - "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.12.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", - "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-opt": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1", - "@webassemblyjs/wast-printer": "1.12.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", - "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", - "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", - "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", - "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.12.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/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "license": "ISC" - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "license": "MIT", - "peer": true, - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^8" - } - }, - "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==", - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.3", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", - "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.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/alce": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/alce/-/alce-1.2.0.tgz", - "integrity": "sha512-XppPf2S42nO2WhvKzlwzlfcApcXHzjlod30pKmcWjRgLOtqoe5DMuqdiYoM6AgyXksc6A6pV4v1L/WW217e57w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "esprima": "^1.2.0", - "estraverse": "^1.5.0" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/alce/node_modules/esprima": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.5.tgz", - "integrity": "sha512-S9VbPDU0adFErpDai3qDkjq8+G05ONtKzcyNrPKg/ZKa+tf879nX2KexNU95b31UoTJjRLInNBHHHjFPoCd7lQ==", - "dev": true, - "optional": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/alce/node_modules/estraverse": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", - "integrity": "sha512-25w1fMXQrGdoquWnScXZGckOv+Wes+JDnuN/+7ex3SauFRS72r2lFDec0EKPt2YD1wUJ/IrfEex+9yp4hfSOJA==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "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==", - "devOptional": 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==", - "devOptional": 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": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "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==", - "devOptional": 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==", - "devOptional": 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/aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", - "license": "ISC" - }, - "node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/are-we-there-yet/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==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "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/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-buffer-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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-ify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", - "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", - "dev": true, - "license": "MIT" - }, - "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", - "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", - "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", - "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", - "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "license": "MIT" - }, - "node_modules/assert-never": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.3.0.tgz", - "integrity": "sha512-9Z3vxQ+berkL/JJo0dK+EY3Lp0s3NtSnP3VCLsh5HDcZPrh0M+KQRK5sWhUeyPPH+/RCxZqOxLMR+YC6vlviEQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", - "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==", - "license": "MIT" - }, - "node_modules/atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", - "dependencies": { - "follow-redirects": "^1.14.9", - "form-data": "^4.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-jest/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/babel-jest/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/babel-jest/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/babel-jest/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/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==", - "devOptional": 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==", - "devOptional": 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==", - "devOptional": 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.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", - "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", - "devOptional": 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.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", - "@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-top-level-await": "^7.8.3" - }, - "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/babel-walk": { - "version": "3.0.0-canary-5", - "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", - "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/types": "^7.9.6" - }, - "engines": { - "node": ">= 10.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/base32.js": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.0.1.tgz", - "integrity": "sha512-EGHIRiegFa62/SsA1J+Xs2tIzludPdzM064N9wjbiEgHnGnJ1V0WEpA4pEwCYT5nDvZk3ubf0shqaCS7k6xeUQ==", - "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/base64url": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", - "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/bcrypt": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", - "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.11", - "node-addon-api": "^5.0.0" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/bcryptjs": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", - "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", - "license": "MIT" - }, - "node_modules/bignumber.js": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", - "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "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.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", - "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.11.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/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true, - "license": "ISC", - "optional": true - }, - "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.23.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz", - "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==", - "devOptional": 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.30001640", - "electron-to-chromium": "^1.4.820", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.1.0" - }, - "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==", - "devOptional": 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/bull": { - "version": "4.16.0", - "resolved": "https://registry.npmjs.org/bull/-/bull-4.16.0.tgz", - "integrity": "sha512-dgHRLULPexLkpm9wP/7F7Vlf2fdvmffdwhv3Bqu5lFhO+XDDJ4yGqlTPE61Jj1zM8CgchLmJEgIfe7y69jtuOg==", - "dependencies": { - "cron-parser": "^4.2.1", - "get-port": "^5.1.1", - "ioredis": "^5.3.2", - "lodash": "^4.17.21", - "msgpackr": "^1.10.1", - "semver": "^7.5.2", - "uuid": "^8.3.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/bull/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "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==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camel-case": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", - "integrity": "sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "no-case": "^2.2.0", - "upper-case": "^1.1.1" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001645", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001645.tgz", - "integrity": "sha512-GFtY2+qt91kzyMk6j48dJcwJVq5uTkk71XxE3RtScx7XWRLsO7bU44LOFkOZYR8w9YMS0UhPSYpN/6rAMImmLw==", - "devOptional": 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": "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/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==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/character-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", - "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "is-regex": "^1.0.3" - } - }, - "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/cheerio": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", - "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "cheerio-select": "^2.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "htmlparser2": "^8.0.1", - "parse5": "^7.0.0", - "parse5-htmlparser2-tree-adapter": "^7.0.0" - }, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/cheeriojs/cheerio?sponsor=1" - } - }, - "node_modules/cheerio-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", - "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "dependencies": { - "boolbase": "^1.0.0", - "css-select": "^5.1.0", - "css-what": "^6.1.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "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/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "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==", - "devOptional": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cjs-module-lexer": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", - "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", - "devOptional": 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/clean-css": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", - "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "source-map": "~0.6.0" - }, - "engines": { - "node": ">= 4.0" - } - }, - "node_modules/clean-css/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", - "optional": true, - "engines": { - "node": ">=0.10.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/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/cli-highlight/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/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/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/cli-highlight/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/cli-highlight/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/cli-highlight/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/cli-highlight/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/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-truncate": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", - "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^5.0.0", - "string-width": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/cli-truncate/node_modules/emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", - "dev": true, - "license": "MIT" - }, - "node_modules/cli-truncate/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/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==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "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/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/cliui/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/cliui/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/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/cluster-key-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "devOptional": 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==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" - }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "license": "ISC", - "bin": { - "color-support": "bin.js" - } - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/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.3", - "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.3.tgz", - "integrity": "sha512-SsxdiOf064DWoZLH799Ata6u7iV658A11PlWtZATDlXPpKGJnbJZ5Z24ybixAi+LUUqJ/GKowAejtC5GFUG7Tw==", - "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/compare-func": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", - "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-ify": "^1.0.0", - "dot-prop": "^5.1.0" - } - }, - "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==", - "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==", - "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/config-chain": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", - "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" - } - }, - "node_modules/config-chain/node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC", - "optional": true - }, - "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/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "license": "ISC" - }, - "node_modules/constantinople": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", - "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/parser": "^7.6.0", - "@babel/types": "^7.6.1" - } - }, - "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/conventional-changelog-angular": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz", - "integrity": "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "compare-func": "^2.0.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/conventional-changelog-conventionalcommits": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz", - "integrity": "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==", - "dev": true, - "license": "ISC", - "dependencies": { - "compare-func": "^2.0.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/conventional-commits-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", - "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-text-path": "^2.0.0", - "JSONStream": "^1.3.5", - "meow": "^12.0.1", - "split2": "^4.0.0" - }, - "bin": { - "conventional-commits-parser": "cli.mjs" - }, - "engines": { - "node": ">=16" - } - }, - "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==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" - }, - "node_modules/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==", - "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": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/cosmiconfig-typescript-loader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-5.0.0.tgz", - "integrity": "sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jiti": "^1.19.1" - }, - "engines": { - "node": ">=v16" - }, - "peerDependencies": { - "@types/node": "*", - "cosmiconfig": ">=8.2", - "typescript": ">=4" - } - }, - "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-jest/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/create-jest/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/create-jest/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/create-jest/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/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/cron-parser": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", - "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", - "dependencies": { - "luxon": "^3.2.1" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/dargs": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/dargs/-/dargs-8.1.0.tgz", - "integrity": "sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/data-view-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", - "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", - "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", - "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/dayjs": { - "version": "1.11.12", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.12.tgz", - "integrity": "sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "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-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/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==", - "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/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "license": "MIT" - }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "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-indent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", - "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "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==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/dezalgo": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", - "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", - "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/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/display-notification": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/display-notification/-/display-notification-2.0.0.tgz", - "integrity": "sha512-TdmtlAcdqy1NU+j7zlkDdMnCL878zriLaBmoD9quOoq1ySSSGv03l0hXK5CvIFZlIfFI/hizqdQuW+Num7xuhw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "escape-string-applescript": "^1.0.0", - "run-applescript": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/doctypes": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", - "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause", - "optional": true - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dot-prop": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-obj": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "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/dynamic-dedupe": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", - "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - } - }, - "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/editorconfig": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", - "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@one-ini/wasm": "0.1.1", - "commander": "^10.0.0", - "minimatch": "9.0.1", - "semver": "^7.5.3" - }, - "bin": { - "editorconfig": "bin/editorconfig" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/editorconfig/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/editorconfig/node_modules/minimatch": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", - "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "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.4", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.4.tgz", - "integrity": "sha512-orzA81VqLyIGUEA77YkVA1D+N+nNfl2isJVjjmOyrlxuooZ19ynb+dOlaDTqd/idKRS9lDCSBmtzM+kyCsMnkA==", - "devOptional": 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": "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/encoding-japanese": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.1.0.tgz", - "integrity": "sha512-58XySVxUgVlBikBTbQ8WdDxBDHIdXucB16LO5PBHR8t75D54wQrNo4cg+58+R1CtJfKnsVsvt9XlteRaR8xw1w==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8.10.0" - } - }, - "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/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-abstract": { - "version": "1.23.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", - "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "data-view-buffer": "^1.0.1", - "data-view-byte-length": "^1.0.1", - "data-view-byte-offset": "^1.0.0", - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.4", - "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", - "hasown": "^2.0.2", - "internal-slot": "^1.0.7", - "is-array-buffer": "^3.0.4", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.1", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", - "object-keys": "^1.1.1", - "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.2", - "safe-array-concat": "^1.1.2", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.9", - "string.prototype.trimend": "^1.0.8", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.6", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.15" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.0" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-goat": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz", - "integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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-applescript": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/escape-string-applescript/-/escape-string-applescript-1.0.0.tgz", - "integrity": "sha512-4/hFwoYaC6TkpDn9A3pTC52zQPArFeXuIfhUtCGYdauTzXVP9H3BDr3oO/QzQehMpLDC7srvYgfwvImPFGfvBA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "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.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/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-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-module-utils": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", - "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", - "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.0", - "is-core-module": "^2.13.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", - "semver": "^6.3.1", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" - } - }, - "node_modules/eslint-plugin-import/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-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-import/node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/eslint-plugin-import/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-plugin-import/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/eslint-plugin-import/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/eslint-plugin-import/node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.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==", - "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/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/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/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eslint/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/eslint/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint/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/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/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/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/eslint/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/eslint/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/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "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==", - "devOptional": 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/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dev": true, - "license": "MIT" - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "devOptional": 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.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.2", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.6.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~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" - }, - "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.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", - "license": "MIT" - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/extend-object": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/extend-object/-/extend-object-1.0.0.tgz", - "integrity": "sha512-0dHDIXC7y7LDmCh/lp1oYkmv73K25AMugQI07r8eFopkW6f7Ufn1q+ETMsJjnV9Am14SlElkqy3O92r6xEaxPw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "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==", - "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==", - "devOptional": 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-redact": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", - "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6" - } - }, - "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/fast-uri": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", - "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==", - "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==", - "devOptional": 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.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "license": "MIT", - "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" - }, - "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": "7.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz", - "integrity": "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^7.2.0", - "path-exists": "^5.0.0", - "unicorn-magic": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/fixpack": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fixpack/-/fixpack-4.0.0.tgz", - "integrity": "sha512-5SM1+H2CcuJ3gGEwTiVo/+nd/hYpNj9Ch3iMDOQ58ndY+VGQ2QdvaUTkd3otjZvYnd/8LF/HkJ5cx7PBq0orCQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "alce": "1.2.0", - "chalk": "^3.0.0", - "detect-indent": "^6.0.0", - "detect-newline": "^3.1.0", - "extend-object": "^1.0.0", - "rc": "^1.2.8" - }, - "bin": { - "fixpack": "bin/fixpack" - } - }, - "node_modules/fixpack/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/fixpack/node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fixpack/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/fixpack/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT", - "optional": true - }, - "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/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.1.3" - } - }, - "node_modules/foreground-child": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", - "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", - "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/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/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/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/fork-ts-checker-webpack-plugin/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/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.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/formidable": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.1.tgz", - "integrity": "sha512-WJWKelbRHN41m5dumb0/k8TeAx7Id/y3a+Z7QfhxP/htI9Js5zYaEDtG8uMgG0vM0lOlqnmjE99/kfpOYi/0Og==", - "license": "MIT", - "dependencies": { - "dezalgo": "^1.0.4", - "hexoid": "^1.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-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs-minipass/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, - "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==", - "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==", - "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/function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/gauge/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==", - "license": "ISC" - }, - "node_modules/gaxios": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.0.tgz", - "integrity": "sha512-DSrkyMTfAnAm4ks9Go20QGOcXEyW/NmZhvTYBU2rb4afBB393WIMQPWPEDMl/k8xqiNN9HYq2zao3oWXsdl2Tg==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9", - "uuid": "^10.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/gaxios/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/gaxios/node_modules/https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/gaxios/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==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gcp-metadata": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", - "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^6.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "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==", - "devOptional": 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-east-asian-width": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", - "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-port": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", - "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-symbol-description": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", - "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/git-raw-commits": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-4.0.0.tgz", - "integrity": "sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "dargs": "^8.0.0", - "meow": "^12.0.1", - "split2": "^4.0.0" - }, - "bin": { - "git-raw-commits": "cli.mjs" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/glob": { - "version": "10.3.12", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", - "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.10.2" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "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/global-directory": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", - "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ini": "4.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/google-auth-library": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.13.0.tgz", - "integrity": "sha512-p9Y03Uzp/Igcs36zAaB0XTSwZ8Y0/tpYiz5KIde5By+H9DCVUSYtDWZu6aFXsWTqENMb8BD/pDT3hR8NVrPkfA==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^6.1.1", - "gcp-metadata": "^6.1.0", - "gtoken": "^7.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "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==", - "devOptional": 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/gtoken": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", - "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", - "license": "MIT", - "dependencies": { - "gaxios": "^6.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/handlebars/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==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "license": "ISC" - }, - "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/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "license": "MIT", - "optional": true, - "bin": { - "he": "bin/he" - } - }, - "node_modules/hexoid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", - "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", - "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==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/html-minifier": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-4.0.0.tgz", - "integrity": "sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "camel-case": "^3.0.0", - "clean-css": "^4.2.1", - "commander": "^2.19.0", - "he": "^1.2.0", - "param-case": "^2.1.1", - "relateurl": "^0.2.7", - "uglify-js": "^3.5.1" - }, - "bin": { - "html-minifier": "cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/html-minifier/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", - "optional": true - }, - "node_modules/html-to-text": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", - "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@selderee/plugin-htmlparser2": "^0.11.0", - "deepmerge": "^4.3.1", - "dom-serializer": "^2.0.0", - "htmlparser2": "^8.0.2", - "selderee": "^0.11.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/html-validator": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/html-validator/-/html-validator-6.0.1.tgz", - "integrity": "sha512-b21ESfN6FStukGoqGgtkxhs3DSp9ziw8zH9LRv+0YniC6/YhbfgWlq6lr/QLJmfzN2fKev6pVs4TKtl7F/Q/4Q==", - "dependencies": { - "axios": "0.27.2", - "html-validate": "7.0.0", - "valid-url": "1.0.9" - }, - "engines": { - "node": ">=14.19.2" - } - }, - "node_modules/html-validator/node_modules/@jest/console": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-28.1.3.tgz", - "integrity": "sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw==", - "optional": true, - "peer": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^28.1.3", - "jest-util": "^28.1.3", - "slash": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/@jest/core": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-28.1.3.tgz", - "integrity": "sha512-CIKBrlaKOzA7YG19BEqCw3SLIsEwjZkeJzf5bdooVnW4bH5cktqe3JX+G2YV1aK5vP8N9na1IGWFzYaTp6k6NA==", - "optional": true, - "peer": true, - "dependencies": { - "@jest/console": "^28.1.3", - "@jest/reporters": "^28.1.3", - "@jest/test-result": "^28.1.3", - "@jest/transform": "^28.1.3", - "@jest/types": "^28.1.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": "^28.1.3", - "jest-config": "^28.1.3", - "jest-haste-map": "^28.1.3", - "jest-message-util": "^28.1.3", - "jest-regex-util": "^28.0.2", - "jest-resolve": "^28.1.3", - "jest-resolve-dependencies": "^28.1.3", - "jest-runner": "^28.1.3", - "jest-runtime": "^28.1.3", - "jest-snapshot": "^28.1.3", - "jest-util": "^28.1.3", - "jest-validate": "^28.1.3", - "jest-watcher": "^28.1.3", - "micromatch": "^4.0.4", - "pretty-format": "^28.1.3", - "rimraf": "^3.0.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/html-validator/node_modules/@jest/environment": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-28.1.3.tgz", - "integrity": "sha512-1bf40cMFTEkKyEf585R9Iz1WayDjHoHqvts0XFYEqyKM3cFWDpeMoqKKTAF9LSYQModPUlh8FKptoM2YcMWAXA==", - "optional": true, - "peer": true, - "dependencies": { - "@jest/fake-timers": "^28.1.3", - "@jest/types": "^28.1.3", - "@types/node": "*", - "jest-mock": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/@jest/expect": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-28.1.3.tgz", - "integrity": "sha512-lzc8CpUbSoE4dqT0U+g1qODQjBRHPpCPXissXD4mS9+sWQdmmpeJ9zSH1rS1HEkrsMN0fb7nKrJ9giAR1d3wBw==", - "optional": true, - "peer": true, - "dependencies": { - "expect": "^28.1.3", - "jest-snapshot": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/@jest/expect-utils": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-28.1.3.tgz", - "integrity": "sha512-wvbi9LUrHJLn3NlDW6wF2hvIMtd4JUl2QNVrjq+IBSHirgfrR3o9RnVtxzdEGO2n9JyIWwHnLfby5KzqBGg2YA==", - "optional": true, - "peer": true, - "dependencies": { - "jest-get-type": "^28.0.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/@jest/fake-timers": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-28.1.3.tgz", - "integrity": "sha512-D/wOkL2POHv52h+ok5Oj/1gOG9HSywdoPtFsRCUmlCILXNn5eIWmcnd3DIiWlJnpGvQtmajqBP95Ei0EimxfLw==", - "optional": true, - "peer": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@sinonjs/fake-timers": "^9.1.2", - "@types/node": "*", - "jest-message-util": "^28.1.3", - "jest-mock": "^28.1.3", - "jest-util": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/@jest/globals": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-28.1.3.tgz", - "integrity": "sha512-XFU4P4phyryCXu1pbcqMO0GSQcYe1IsalYCDzRNyhetyeyxMcIxa11qPNDpVNLeretItNqEmYYQn1UYz/5x1NA==", - "optional": true, - "peer": true, - "dependencies": { - "@jest/environment": "^28.1.3", - "@jest/expect": "^28.1.3", - "@jest/types": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/@jest/reporters": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-28.1.3.tgz", - "integrity": "sha512-JuAy7wkxQZVNU/V6g9xKzCGC5LVXx9FDcABKsSXp5MiKPEE2144a/vXTEDoyzjUpZKfVwp08Wqg5A4WfTMAzjg==", - "optional": true, - "peer": true, - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^28.1.3", - "@jest/test-result": "^28.1.3", - "@jest/transform": "^28.1.3", - "@jest/types": "^28.1.3", - "@jridgewell/trace-mapping": "^0.3.13", - "@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": "^5.1.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^28.1.3", - "jest-util": "^28.1.3", - "jest-worker": "^28.1.3", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "terminal-link": "^2.0.0", - "v8-to-istanbul": "^9.0.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/html-validator/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==", - "optional": true, - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/html-validator/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", - "optional": true, - "peer": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/html-validator/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==", - "optional": true, - "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/html-validator/node_modules/@jest/schemas": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz", - "integrity": "sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==", - "optional": true, - "peer": true, - "dependencies": { - "@sinclair/typebox": "^0.24.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/@jest/source-map": { - "version": "28.1.2", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-28.1.2.tgz", - "integrity": "sha512-cV8Lx3BeStJb8ipPHnqVw/IM2VCMWO3crWZzYodSIkxXnRcXJipCdx1JCK0K5MsJJouZQTH73mzf4vgxRaH9ww==", - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.13", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/@jest/test-result": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-28.1.3.tgz", - "integrity": "sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg==", - "optional": true, - "peer": true, - "dependencies": { - "@jest/console": "^28.1.3", - "@jest/types": "^28.1.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/@jest/test-sequencer": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-28.1.3.tgz", - "integrity": "sha512-NIMPEqqa59MWnDi1kvXXpYbqsfQmSJsIbnd85mdVGkiDfQ9WQQTXOLsvISUfonmnBT+w85WEgneCigEEdHDFxw==", - "optional": true, - "peer": true, - "dependencies": { - "@jest/test-result": "^28.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.3", - "slash": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/@jest/transform": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-28.1.3.tgz", - "integrity": "sha512-u5dT5di+oFI6hfcLOHGTAfmUxFRrjK+vnaP0kkVow9Md/M7V/MxqQMOz/VV25UZO8pzeA9PjfTpOu6BDuwSPQA==", - "optional": true, - "peer": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^28.1.3", - "@jridgewell/trace-mapping": "^0.3.13", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.3", - "jest-regex-util": "^28.0.2", - "jest-util": "^28.1.3", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/@jest/types": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-28.1.3.tgz", - "integrity": "sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==", - "optional": true, - "peer": true, - "dependencies": { - "@jest/schemas": "^28.1.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": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/@sinclair/typebox": { - "version": "0.24.51", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", - "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==", - "optional": true, - "peer": true - }, - "node_modules/html-validator/node_modules/@sinonjs/commons": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", - "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", - "optional": true, - "peer": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/html-validator/node_modules/@sinonjs/fake-timers": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", - "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", - "optional": true, - "peer": true, - "dependencies": { - "@sinonjs/commons": "^1.7.0" - } - }, - "node_modules/html-validator/node_modules/babel-jest": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-28.1.3.tgz", - "integrity": "sha512-epUaPOEWMk3cWX0M/sPvCHHCe9fMFAa/9hXEgKP8nFfNl/jlGkE9ucq9NqkZGXLDduCJYS0UvSlPUwC0S+rH6Q==", - "optional": true, - "peer": true, - "dependencies": { - "@jest/transform": "^28.1.3", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^28.1.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/html-validator/node_modules/babel-plugin-jest-hoist": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-28.1.3.tgz", - "integrity": "sha512-Ys3tUKAmfnkRUpPdpa98eYrAR0nV+sSFUZZEGuQ2EbFd1y4SOLtD5QDNHAq+bb9a+bbXvYQC4b+ID/THIMcU6Q==", - "optional": true, - "peer": true, - "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": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/babel-preset-jest": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-28.1.3.tgz", - "integrity": "sha512-L+fupJvlWAHbQfn74coNX3zf60LXMJsezNvvx8eIh7iOR1luJ1poxYgQk1F8PYtNq/6QODDHCqsSnTFSWC491A==", - "optional": true, - "peer": true, - "dependencies": { - "babel-plugin-jest-hoist": "^28.1.3", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/html-validator/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "optional": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/html-validator/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "optional": true, - "peer": true, - "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/html-validator/node_modules/chalk/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==", - "optional": true, - "peer": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/html-validator/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==", - "optional": true, - "peer": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/html-validator/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==", - "optional": true, - "peer": true - }, - "node_modules/html-validator/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "optional": true, - "peer": true - }, - "node_modules/html-validator/node_modules/dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", - "optional": true, - "peer": true - }, - "node_modules/html-validator/node_modules/diff-sequences": { - "version": "28.1.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.1.1.tgz", - "integrity": "sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==", - "optional": true, - "peer": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/emittery": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", - "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==", - "optional": true, - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/html-validator/node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "optional": true, - "peer": true, - "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/html-validator/node_modules/expect": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/expect/-/expect-28.1.3.tgz", - "integrity": "sha512-eEh0xn8HlsuOBxFgIss+2mX85VAS4Qy3OSkjV7rlBWljtA4oWH37glVGyOZSZvErDT/yBywZdPGwCXuTvSG85g==", - "optional": true, - "peer": true, - "dependencies": { - "@jest/expect-utils": "^28.1.3", - "jest-get-type": "^28.0.2", - "jest-matcher-utils": "^28.1.3", - "jest-message-util": "^28.1.3", - "jest-util": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/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==", - "optional": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/html-validator/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/html-validator/node_modules/html-validate": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/html-validate/-/html-validate-7.0.0.tgz", - "integrity": "sha512-Sq+ZQQSnkabgjplF2qNjbA4RdTkOP/99V2Q7gP2rr4BLUEEjlpfJQx3DeHtpzfBJmKFA/NzT7wb2qrzGCpcilA==", - "workspaces": [ - "tests/integration/*" - ], - "dependencies": { - "@babel/code-frame": "^7.10.0", - "@html-validate/stylish": "^2.0.0", - "@sidvind/better-ajv-errors": "^1.1.1", - "acorn-walk": "^8.0.0", - "ajv": "^8.0.0", - "deepmerge": "^4.2.0", - "espree": "^9.0.0", - "glob": "^8.0.0", - "ignore": "^5.0.0", - "kleur": "^4.1.0", - "minimist": "^1.2.0", - "prompts": "^2.0.0", - "semver": "^7.0.0" - }, - "bin": { - "html-validate": "bin/html-validate.js" - }, - "engines": { - "node": ">= 14.0" - }, - "peerDependencies": { - "jest": "^25.1 || ^26 || ^27.1 || ^28", - "jest-diff": "^25.1 || ^26 || ^27.1 || ^28", - "jest-snapshot": "^25.1 || ^26 || ^27.1 || ^28" - }, - "peerDependenciesMeta": { - "jest": { - "optional": true - }, - "jest-diff": { - "optional": true - }, - "jest-snapshot": { - "optional": true - } - } - }, - "node_modules/html-validator/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==", - "optional": true, - "peer": true, - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/html-validator/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==", - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/html-validator/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==", - "optional": true, - "peer": true, - "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/html-validator/node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "optional": true, - "peer": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/html-validator/node_modules/jest": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest/-/jest-28.1.3.tgz", - "integrity": "sha512-N4GT5on8UkZgH0O5LUavMRV1EDEhNTL0KEfRmDIeZHSV7p2XgLoY9t9VDUgL6o+yfdgYHVxuz81G8oB9VG5uyA==", - "optional": true, - "peer": true, - "dependencies": { - "@jest/core": "^28.1.3", - "@jest/types": "^28.1.3", - "import-local": "^3.0.2", - "jest-cli": "^28.1.3" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/html-validator/node_modules/jest-changed-files": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-28.1.3.tgz", - "integrity": "sha512-esaOfUWJXk2nfZt9SPyC8gA1kNfdKLkQWyzsMlqq8msYSlNKfmZxfRgZn4Cd4MGVUF+7v6dBs0d5TOAKa7iIiA==", - "optional": true, - "peer": true, - "dependencies": { - "execa": "^5.0.0", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/jest-circus": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-28.1.3.tgz", - "integrity": "sha512-cZ+eS5zc79MBwt+IhQhiEp0OeBddpc1n8MBo1nMB8A7oPMKEO+Sre+wHaLJexQUj9Ya/8NOBY0RESUgYjB6fow==", - "optional": true, - "peer": true, - "dependencies": { - "@jest/environment": "^28.1.3", - "@jest/expect": "^28.1.3", - "@jest/test-result": "^28.1.3", - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^0.7.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^28.1.3", - "jest-matcher-utils": "^28.1.3", - "jest-message-util": "^28.1.3", - "jest-runtime": "^28.1.3", - "jest-snapshot": "^28.1.3", - "jest-util": "^28.1.3", - "p-limit": "^3.1.0", - "pretty-format": "^28.1.3", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/jest-cli": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-28.1.3.tgz", - "integrity": "sha512-roY3kvrv57Azn1yPgdTebPAXvdR2xfezaKKYzVxZ6It/5NCxzJym6tUI5P1zkdWhfUYkxEI9uZWcQdaFLo8mJQ==", - "optional": true, - "peer": true, - "dependencies": { - "@jest/core": "^28.1.3", - "@jest/test-result": "^28.1.3", - "@jest/types": "^28.1.3", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "import-local": "^3.0.2", - "jest-config": "^28.1.3", - "jest-util": "^28.1.3", - "jest-validate": "^28.1.3", - "prompts": "^2.0.1", - "yargs": "^17.3.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/html-validator/node_modules/jest-config": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-28.1.3.tgz", - "integrity": "sha512-MG3INjByJ0J4AsNBm7T3hsuxKQqFIiRo/AUqb1q9LRKI5UU6Aar9JHbr9Ivn1TVwfUD9KirRoM/T6u8XlcQPHQ==", - "optional": true, - "peer": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^28.1.3", - "@jest/types": "^28.1.3", - "babel-jest": "^28.1.3", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^28.1.3", - "jest-environment-node": "^28.1.3", - "jest-get-type": "^28.0.2", - "jest-regex-util": "^28.0.2", - "jest-resolve": "^28.1.3", - "jest-runner": "^28.1.3", - "jest-util": "^28.1.3", - "jest-validate": "^28.1.3", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^28.1.3", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/html-validator/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==", - "optional": true, - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/html-validator/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", - "optional": true, - "peer": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/html-validator/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==", - "optional": true, - "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/html-validator/node_modules/jest-diff": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.3.tgz", - "integrity": "sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==", - "optional": true, - "peer": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^28.1.1", - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/jest-docblock": { - "version": "28.1.1", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-28.1.1.tgz", - "integrity": "sha512-3wayBVNiOYx0cwAbl9rwm5kKFP8yHH3d/fkEaL02NPTkDojPtheGB7HZSFY4wzX+DxyrvhXz0KSCVksmCknCuA==", - "optional": true, - "peer": true, - "dependencies": { - "detect-newline": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/jest-each": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-28.1.3.tgz", - "integrity": "sha512-arT1z4sg2yABU5uogObVPvSlSMQlDA48owx07BDPAiasW0yYpYHYOo4HHLz9q0BVzDVU4hILFjzJw0So9aCL/g==", - "optional": true, - "peer": true, - "dependencies": { - "@jest/types": "^28.1.3", - "chalk": "^4.0.0", - "jest-get-type": "^28.0.2", - "jest-util": "^28.1.3", - "pretty-format": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/jest-environment-node": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-28.1.3.tgz", - "integrity": "sha512-ugP6XOhEpjAEhGYvp5Xj989ns5cB1K6ZdjBYuS30umT4CQEETaxSiPcZ/E1kFktX4GkrcM4qu07IIlDYX1gp+A==", - "optional": true, - "peer": true, - "dependencies": { - "@jest/environment": "^28.1.3", - "@jest/fake-timers": "^28.1.3", - "@jest/types": "^28.1.3", - "@types/node": "*", - "jest-mock": "^28.1.3", - "jest-util": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/jest-get-type": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz", - "integrity": "sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==", - "optional": true, - "peer": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/jest-haste-map": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-28.1.3.tgz", - "integrity": "sha512-3S+RQWDXccXDKSWnkHa/dPwt+2qwA8CJzR61w3FoYCvoo3Pn8tvGcysmMF0Bj0EX5RYvAI2EIvC57OmotfdtKA==", - "optional": true, - "peer": true, - "dependencies": { - "@jest/types": "^28.1.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": "^28.0.2", - "jest-util": "^28.1.3", - "jest-worker": "^28.1.3", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/html-validator/node_modules/jest-leak-detector": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-28.1.3.tgz", - "integrity": "sha512-WFVJhnQsiKtDEo5lG2mM0v40QWnBM+zMdHHyJs8AWZ7J0QZJS59MsyKeJHWhpBZBH32S48FOVvGyOFT1h0DlqA==", - "optional": true, - "peer": true, - "dependencies": { - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/jest-matcher-utils": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.3.tgz", - "integrity": "sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw==", - "optional": true, - "peer": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^28.1.3", - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/jest-message-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz", - "integrity": "sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==", - "optional": true, - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^28.1.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^28.1.3", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/jest-mock": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-28.1.3.tgz", - "integrity": "sha512-o3J2jr6dMMWYVH4Lh/NKmDXdosrsJgi4AviS8oXLujcjpCMBb1FMsblDnOXKZKfSiHLxYub1eS0IHuRXsio9eA==", - "optional": true, - "peer": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/jest-regex-util": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", - "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", - "optional": true, - "peer": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/jest-resolve": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-28.1.3.tgz", - "integrity": "sha512-Z1W3tTjE6QaNI90qo/BJpfnvpxtaFTFw5CDgwpyE/Kz8U/06N1Hjf4ia9quUhCh39qIGWF1ZuxFiBiJQwSEYKQ==", - "optional": true, - "peer": true, - "dependencies": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.3", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^28.1.3", - "jest-validate": "^28.1.3", - "resolve": "^1.20.0", - "resolve.exports": "^1.1.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/jest-resolve-dependencies": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-28.1.3.tgz", - "integrity": "sha512-qa0QO2Q0XzQoNPouMbCc7Bvtsem8eQgVPNkwn9LnS+R2n8DaVDPL/U1gngC0LTl1RYXJU0uJa2BMC2DbTfFrHA==", - "optional": true, - "peer": true, - "dependencies": { - "jest-regex-util": "^28.0.2", - "jest-snapshot": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/jest-runner": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-28.1.3.tgz", - "integrity": "sha512-GkMw4D/0USd62OVO0oEgjn23TM+YJa2U2Wu5zz9xsQB1MxWKDOlrnykPxnMsN0tnJllfLPinHTka61u0QhaxBA==", - "optional": true, - "peer": true, - "dependencies": { - "@jest/console": "^28.1.3", - "@jest/environment": "^28.1.3", - "@jest/test-result": "^28.1.3", - "@jest/transform": "^28.1.3", - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.10.2", - "graceful-fs": "^4.2.9", - "jest-docblock": "^28.1.1", - "jest-environment-node": "^28.1.3", - "jest-haste-map": "^28.1.3", - "jest-leak-detector": "^28.1.3", - "jest-message-util": "^28.1.3", - "jest-resolve": "^28.1.3", - "jest-runtime": "^28.1.3", - "jest-util": "^28.1.3", - "jest-watcher": "^28.1.3", - "jest-worker": "^28.1.3", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/jest-runtime": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-28.1.3.tgz", - "integrity": "sha512-NU+881ScBQQLc1JHG5eJGU7Ui3kLKrmwCPPtYsJtBykixrM2OhVQlpMmFWJjMyDfdkGgBMNjXCGB/ebzsgNGQw==", - "optional": true, - "peer": true, - "dependencies": { - "@jest/environment": "^28.1.3", - "@jest/fake-timers": "^28.1.3", - "@jest/globals": "^28.1.3", - "@jest/source-map": "^28.1.2", - "@jest/test-result": "^28.1.3", - "@jest/transform": "^28.1.3", - "@jest/types": "^28.1.3", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "execa": "^5.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.3", - "jest-message-util": "^28.1.3", - "jest-mock": "^28.1.3", - "jest-regex-util": "^28.0.2", - "jest-resolve": "^28.1.3", - "jest-snapshot": "^28.1.3", - "jest-util": "^28.1.3", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/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==", - "optional": true, - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/html-validator/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", - "optional": true, - "peer": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/html-validator/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==", - "optional": true, - "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/html-validator/node_modules/jest-snapshot": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-28.1.3.tgz", - "integrity": "sha512-4lzMgtiNlc3DU/8lZfmqxN3AYD6GGLbl+72rdBpXvcV+whX7mDrREzkPdp2RnmfIiWBg1YbuFSkXduF2JcafJg==", - "optional": true, - "peer": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^28.1.3", - "@jest/transform": "^28.1.3", - "@jest/types": "^28.1.3", - "@types/babel__traverse": "^7.0.6", - "@types/prettier": "^2.1.5", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^28.1.3", - "graceful-fs": "^4.2.9", - "jest-diff": "^28.1.3", - "jest-get-type": "^28.0.2", - "jest-haste-map": "^28.1.3", - "jest-matcher-utils": "^28.1.3", - "jest-message-util": "^28.1.3", - "jest-util": "^28.1.3", - "natural-compare": "^1.4.0", - "pretty-format": "^28.1.3", - "semver": "^7.3.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/jest-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", - "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", - "optional": true, - "peer": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/jest-validate": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-28.1.3.tgz", - "integrity": "sha512-SZbOGBWEsaTxBGCOpsRWlXlvNkvTkY0XxRfh7zYmvd8uL5Qzyg0CHAXiXKROflh801quA6+/DsT4ODDthOC/OA==", - "optional": true, - "peer": true, - "dependencies": { - "@jest/types": "^28.1.3", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^28.0.2", - "leven": "^3.1.0", - "pretty-format": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/jest-watcher": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-28.1.3.tgz", - "integrity": "sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g==", - "optional": true, - "peer": true, - "dependencies": { - "@jest/test-result": "^28.1.3", - "@jest/types": "^28.1.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.10.2", - "jest-util": "^28.1.3", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/jest-worker": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz", - "integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==", - "optional": true, - "peer": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/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==", - "optional": true, - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/html-validator/node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/html-validator/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==", - "optional": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/html-validator/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/html-validator/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==", - "optional": true, - "peer": true, - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/html-validator/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "optional": true, - "peer": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/html-validator/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "optional": true, - "peer": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/html-validator/node_modules/pretty-format": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz", - "integrity": "sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==", - "optional": true, - "peer": true, - "dependencies": { - "@jest/schemas": "^28.1.3", - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/html-validator/node_modules/resolve.exports": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.1.tgz", - "integrity": "sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ==", - "optional": true, - "peer": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/html-validator/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==", - "optional": true, - "peer": true - }, - "node_modules/html-validator/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==", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/html-validator/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==", - "optional": true, - "peer": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/html-validator/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==", - "optional": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" - } - }, - "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/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=16.17.0" - } - }, - "node_modules/husky": { - "version": "9.1.4", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.4.tgz", - "integrity": "sha512-bho94YyReb4JV7LYWRWxZ/xr6TtOTt8cMfmQ39MQYJ7f/YE268s3GdghGwi+y4zAeqewE5zYLvuhV0M0ijsDEA==", - "dev": true, - "license": "MIT", - "bin": { - "husky": "bin.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, - "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.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", - "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-fresh/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/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "devOptional": 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/import-meta-resolve": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", - "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "devOptional": 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.", - "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/ini": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", - "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "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/inquirer/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/inquirer/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/inquirer/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/inquirer/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ioredis": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.1.tgz", - "integrity": "sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==", - "dependencies": { - "@ioredis/commands": "^1.1.1", - "cluster-key-slot": "^1.1.0", - "debug": "^4.3.4", - "denque": "^2.1.0", - "lodash.defaults": "^4.2.0", - "lodash.isarguments": "^3.1.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0", - "standard-as-callback": "^2.1.0" - }, - "engines": { - "node": ">=12.22.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ioredis" - } - }, - "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-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", - "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", - "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, - "license": "MIT", - "optional": true, - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-expression": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", - "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "acorn": "^7.1.1", - "object-assign": "^4.1.1" - } - }, - "node_modules/is-expression/node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true, - "license": "MIT", - "optional": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "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": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "devOptional": 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-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-text-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-2.0.0.tgz", - "integrity": "sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "text-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "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==", - "devOptional": 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==", - "devOptional": 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-report/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==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "devOptional": 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==", - "devOptional": 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==", - "devOptional": 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": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "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/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/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/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jake/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jake/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/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-changed-files/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/jest-changed-files/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/jest-changed-files/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/jest-changed-files/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/jest-changed-files/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/jest-changed-files/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/jest-changed-files/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/jest-changed-files/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/jest-changed-files/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/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-circus/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-circus/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-circus/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-circus/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/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-cli/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-cli/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-cli/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-cli/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/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/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/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/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-config/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-config/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/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-diff/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-diff/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/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-each/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-each/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-each/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-each/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/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-matcher-utils/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-matcher-utils/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-matcher-utils/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/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-message-util/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-message-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-message-util/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-message-util/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/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==", - "devOptional": 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-resolve/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-resolve/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-resolve/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-resolve/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/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/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-runner/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-runner/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-runner/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/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/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/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/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-runtime/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-runtime/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/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-snapshot/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-snapshot/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-snapshot/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/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/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-util/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-util/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/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/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/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-validate/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-validate/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-validate/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/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-watcher/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-watcher/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-watcher/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-watcher/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/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": "1.21.6", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", - "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "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-beautify": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz", - "integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "config-chain": "^1.1.13", - "editorconfig": "^1.0.4", - "glob": "^10.3.3", - "js-cookie": "^3.0.5", - "nopt": "^7.2.0" - }, - "bin": { - "css-beautify": "js/bin/css-beautify.js", - "html-beautify": "js/bin/html-beautify.js", - "js-beautify": "js/bin/js-beautify.js" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/js-beautify/node_modules/abbrev": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", - "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", - "dev": true, - "license": "ISC", - "optional": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/js-beautify/node_modules/nopt": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", - "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "abbrev": "^2.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/js-cookie": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", - "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/js-stringify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", - "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==", - "dev": true, - "license": "MIT", - "optional": true - }, - "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==", - "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": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "devOptional": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/json-bigint": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", - "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.0.0" - } - }, - "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==", - "devOptional": 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==", - "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==", - "devOptional": 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/jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", - "dev": true, - "engines": [ - "node >= 0.2.0" - ], - "license": "MIT" - }, - "node_modules/JSONStream": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", - "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", - "dev": true, - "license": "(MIT OR Apache-2.0)", - "dependencies": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" - }, - "bin": { - "JSONStream": "bin.js" - }, - "engines": { - "node": "*" - } - }, - "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/jsonwebtoken/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/jsonwebtoken/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/jstransformer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", - "integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "is-promise": "^2.0.0", - "promise": "^7.0.1" - } - }, - "node_modules/juice": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/juice/-/juice-10.0.0.tgz", - "integrity": "sha512-9f68xmhGrnIi6DBkiiP3rUrQN33SEuaKu1+njX6VgMP+jwZAsnT33WIzlrWICL9matkhYu3OyrqSUP55YTIdGg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "cheerio": "^1.0.0-rc.12", - "commander": "^6.1.0", - "mensch": "^0.3.4", - "slick": "^1.12.2", - "web-resource-inliner": "^6.0.1" - }, - "bin": { - "juice": "bin/juice" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/juice/node_modules/commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", - "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": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "license": "MIT", - "dependencies": { - "jwa": "^2.0.0", - "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==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/leac": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", - "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==", - "dev": true, - "license": "MIT", - "optional": true, - "funding": { - "url": "https://ko-fi.com/killymxi" - } - }, - "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==", - "devOptional": 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/libbase64": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-1.3.0.tgz", - "integrity": "sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/libmime": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.5.tgz", - "integrity": "sha512-nSlR1yRZ43L3cZCiWEw7ali3jY29Hz9CQQ96Oy+sSspYnIP5N54ucOPHqooBsXzwrX1pwn13VUE05q4WmzfaLg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "encoding-japanese": "2.1.0", - "iconv-lite": "0.6.3", - "libbase64": "1.3.0", - "libqp": "2.1.0" - } - }, - "node_modules/libmime/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==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/libphonenumber-js": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.5.tgz", - "integrity": "sha512-TwHR5BZxGRODtAfz03szucAkjT5OArXr+94SMtAM2pYXIlQNVMrxvb6uSCbnaJJV6QXEyICk7+l6QPgn72WHhg==", - "license": "MIT" - }, - "node_modules/libqp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/libqp/-/libqp-2.1.0.tgz", - "integrity": "sha512-O6O6/fsG5jiUVbvdgT7YX3xY3uIadR6wEZ7+vy9u7PKHAlSEB6blvC1o5pHBjgsi95Uo0aiBBdkyFecj6jtb7A==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/lilconfig": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", - "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "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==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "uc.micro": "^2.0.0" - } - }, - "node_modules/lint-staged": { - "version": "15.2.7", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.7.tgz", - "integrity": "sha512-+FdVbbCZ+yoh7E/RosSdqKJyUM2OEjTciH0TFNkawKgvFp1zbGlEC39RADg+xKBG1R4mhoH2j85myBQZ5wR+lw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "~5.3.0", - "commander": "~12.1.0", - "debug": "~4.3.4", - "execa": "~8.0.1", - "lilconfig": "~3.1.1", - "listr2": "~8.2.1", - "micromatch": "~4.0.7", - "pidtree": "~0.6.0", - "string-argv": "~0.3.2", - "yaml": "~2.4.2" - }, - "bin": { - "lint-staged": "bin/lint-staged.js" - }, - "engines": { - "node": ">=18.12.0" - }, - "funding": { - "url": "https://opencollective.com/lint-staged" - } - }, - "node_modules/lint-staged/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/liquidjs": { - "version": "10.16.1", - "resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-10.16.1.tgz", - "integrity": "sha512-1JFL/Y7ONoajrfwav37yuz5yQHU3+Pgz1XWsg9E/2T8Fp65KalNfMF8QZ3+tNETqGUIB66waOSLOi64niYZE9A==", - "dev": true, - "optional": true, - "dependencies": { - "commander": "^10.0.0" - }, - "bin": { - "liquid": "bin/liquid.js", - "liquidjs": "bin/liquid.js" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/liquidjs" - } - }, - "node_modules/liquidjs/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "dev": true, - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/listr2": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.4.tgz", - "integrity": "sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^4.0.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/listr2/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/listr2/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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/listr2/node_modules/emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", - "dev": true, - "license": "MIT" - }, - "node_modules/listr2/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/listr2/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==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/listr2/node_modules/wrap-ansi": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", - "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "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": "7.2.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^6.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/locter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/locter/-/locter-2.1.0.tgz", - "integrity": "sha512-QUPPtb6CQ3hOacDZq2kc6KMzYn9z6r9B2RtFJTBD9nqxmyQJVYnTNZNqY6Z5NcJfwsGEgJLddnfFpofg7EJMDg==", - "license": "MIT", - "dependencies": { - "destr": "^2.0.3", - "ebec": "^2.3.0", - "fast-glob": "^3.3.2", - "flat": "^5.0.2", - "jiti": "^1.21.0", - "yaml": "^2.4.1" - } - }, - "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.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" - }, - "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.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" - }, - "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.kebabcase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", - "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", - "dev": true, - "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.mergewith": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", - "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", - "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/lodash.snakecase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", - "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.startcase": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", - "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.upperfirst": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", - "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", - "dev": true, - "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/log-symbols/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/log-symbols/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/log-symbols/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/log-symbols/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/ansi-escapes": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", - "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/log-update/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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/log-update/node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-update/node_modules/is-fullwidth-code-point": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", - "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", - "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/log-update/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/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==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", - "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/lower-case": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", - "integrity": "sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==", - "dev": true, - "license": "MIT", - "optional": true - }, - "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==", - "devOptional": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/luxon": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", - "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", - "engines": { - "node": ">=12" - } - }, - "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/mailparser": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.7.1.tgz", - "integrity": "sha512-RCnBhy5q8XtB3mXzxcAfT1huNqN93HTYYyL6XawlIKycfxM/rXPg9tXoZ7D46+SgCS1zxKzw+BayDQSvncSTTw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "encoding-japanese": "2.1.0", - "he": "1.2.0", - "html-to-text": "9.0.5", - "iconv-lite": "0.6.3", - "libmime": "5.3.5", - "linkify-it": "5.0.0", - "mailsplit": "5.4.0", - "nodemailer": "6.9.13", - "punycode.js": "2.3.1", - "tlds": "1.252.0" - } - }, - "node_modules/mailparser/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==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mailparser/node_modules/nodemailer": { - "version": "6.9.13", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.13.tgz", - "integrity": "sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==", - "dev": true, - "license": "MIT-0", - "optional": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/mailsplit": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/mailsplit/-/mailsplit-5.4.0.tgz", - "integrity": "sha512-wnYxX5D5qymGIPYLwnp6h8n1+6P6vz/MJn5AzGjZ8pwICWssL+CCQjWBIToOVHASmATot4ktvlLo6CyLfOXWYA==", - "dev": true, - "license": "(MIT OR EUPL-1.1+)", - "optional": true, - "dependencies": { - "libbase64": "1.2.1", - "libmime": "5.2.0", - "libqp": "2.0.1" - } - }, - "node_modules/mailsplit/node_modules/encoding-japanese": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.0.0.tgz", - "integrity": "sha512-++P0RhebUC8MJAwJOsT93dT+5oc5oPImp1HubZpAuCZ5kTLnhuuBhKHj2jJeO/Gj93idPBWmIuQ9QWMe5rX3pQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/mailsplit/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==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mailsplit/node_modules/libbase64": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-1.2.1.tgz", - "integrity": "sha512-l+nePcPbIG1fNlqMzrh68MLkX/gTxk/+vdvAb388Ssi7UuUN31MI44w4Yf33mM3Cm4xDfw48mdf3rkdHszLNew==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/mailsplit/node_modules/libmime": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.2.0.tgz", - "integrity": "sha512-X2U5Wx0YmK0rXFbk67ASMeqYIkZ6E5vY7pNWRKtnNzqjvdYYG8xtPDpCnuUEnPU9vlgNev+JoSrcaKSUaNvfsw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "encoding-japanese": "2.0.0", - "iconv-lite": "0.6.3", - "libbase64": "1.2.1", - "libqp": "2.0.1" - } - }, - "node_modules/mailsplit/node_modules/libqp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/libqp/-/libqp-2.0.1.tgz", - "integrity": "sha512-Ka0eC5LkF3IPNQHJmYBWljJsw0UvM6j+QdKRbWyCdTmYwvIDE6a7bCm0UkTAL/K+3KXK5qXT/ClcInU01OpdLg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "license": "MIT", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "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==", - "devOptional": 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/mensch": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.4.tgz", - "integrity": "sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/meow": { - "version": "12.1.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", - "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16.10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", - "license": "MIT" - }, - "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==", - "devOptional": 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.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", - "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": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "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/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, - "node_modules/mjml": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml/-/mjml-4.15.3.tgz", - "integrity": "sha512-bW2WpJxm6HS+S3Yu6tq1DUPFoTxU9sPviUSmnL7Ua+oVO3WA5ILFWqvujUlz+oeuM+HCwEyMiP5xvKNPENVjYA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "mjml-cli": "4.15.3", - "mjml-core": "4.15.3", - "mjml-migrate": "4.15.3", - "mjml-preset-core": "4.15.3", - "mjml-validator": "4.15.3" - }, - "bin": { - "mjml": "bin/mjml" - } - }, - "node_modules/mjml-accordion": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-accordion/-/mjml-accordion-4.15.3.tgz", - "integrity": "sha512-LPNVSj1LyUVYT9G1gWwSw3GSuDzDsQCu0tPB2uDsq4VesYNnU6v3iLCQidMiR6azmIt13OEozG700ygAUuA6Ng==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "4.15.3" - } - }, - "node_modules/mjml-body": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-body/-/mjml-body-4.15.3.tgz", - "integrity": "sha512-7pfUOVPtmb0wC+oUOn4xBsAw4eT5DyD6xqaxj/kssu6RrFXOXgJaVnDPAI9AzIvXJ/5as9QrqRGYAddehwWpHQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "4.15.3" - } - }, - "node_modules/mjml-button": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-button/-/mjml-button-4.15.3.tgz", - "integrity": "sha512-79qwn9AgdGjJR1vLnrcm2rq2AsAZkKC5JPwffTMG+Nja6zGYpTDZFZ56ekHWr/r1b5WxkukcPj2PdevUug8c+Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "4.15.3" - } - }, - "node_modules/mjml-carousel": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-carousel/-/mjml-carousel-4.15.3.tgz", - "integrity": "sha512-3ju6I4l7uUhPRrJfN3yK9AMsfHvrYbRkcJ1GRphFHzUj37B2J6qJOQUpzA547Y4aeh69TSb7HFVf1t12ejQxVw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "4.15.3" - } - }, - "node_modules/mjml-cli": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-cli/-/mjml-cli-4.15.3.tgz", - "integrity": "sha512-+V2TDw3tXUVEptFvLSerz125C2ogYl8klIBRY1m5BHd4JvGVf3yhx8N3PngByCzA6PGcv/eydGQN+wy34SHf0Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "chokidar": "^3.0.0", - "glob": "^10.3.10", - "html-minifier": "^4.0.0", - "js-beautify": "^1.6.14", - "lodash": "^4.17.21", - "minimatch": "^9.0.3", - "mjml-core": "4.15.3", - "mjml-migrate": "4.15.3", - "mjml-parser-xml": "4.15.3", - "mjml-validator": "4.15.3", - "yargs": "^17.7.2" - }, - "bin": { - "mjml-cli": "bin/mjml" - } - }, - "node_modules/mjml-column": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-column/-/mjml-column-4.15.3.tgz", - "integrity": "sha512-hYdEFdJGHPbZJSEysykrevEbB07yhJGSwfDZEYDSbhQQFjV2tXrEgYcFD5EneMaowjb55e3divSJxU4c5q4Qgw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "4.15.3" - } - }, - "node_modules/mjml-core": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-core/-/mjml-core-4.15.3.tgz", - "integrity": "sha512-Dmwk+2cgSD9L9GmTbEUNd8QxkTZtW9P7FN/ROZW/fGZD6Hq6/4TB0zEspg2Ow9eYjZXO2ofOJ3PaQEEShKV0kQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "cheerio": "1.0.0-rc.12", - "detect-node": "^2.0.4", - "html-minifier": "^4.0.0", - "js-beautify": "^1.6.14", - "juice": "^10.0.0", - "lodash": "^4.17.21", - "mjml-migrate": "4.15.3", - "mjml-parser-xml": "4.15.3", - "mjml-validator": "4.15.3" - } - }, - "node_modules/mjml-divider": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-divider/-/mjml-divider-4.15.3.tgz", - "integrity": "sha512-vh27LQ9FG/01y0b9ntfqm+GT5AjJnDSDY9hilss2ixIUh0FemvfGRfsGVeV5UBVPBKK7Ffhvfqc7Rciob9Spzw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "4.15.3" - } - }, - "node_modules/mjml-group": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-group/-/mjml-group-4.15.3.tgz", - "integrity": "sha512-HSu/rKnGZVKFq3ciT46vi1EOy+9mkB0HewO4+P6dP/Y0UerWkN6S3UK11Cxsj0cAp0vFwkPDCdOeEzRdpFEkzA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "4.15.3" - } - }, - "node_modules/mjml-head": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-head/-/mjml-head-4.15.3.tgz", - "integrity": "sha512-o3mRuuP/MB5fZycjD3KH/uXsnaPl7Oo8GtdbJTKtH1+O/3pz8GzGMkscTKa97l03DAG2EhGrzzLcU2A6eshwFw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "4.15.3" - } - }, - "node_modules/mjml-head-attributes": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-head-attributes/-/mjml-head-attributes-4.15.3.tgz", - "integrity": "sha512-2ISo0r5ZKwkrvJgDou9xVPxxtXMaETe2AsAA02L89LnbB2KC0N5myNsHV0sEysTw9+CfCmgjAb0GAI5QGpxKkQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "4.15.3" - } - }, - "node_modules/mjml-head-breakpoint": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-head-breakpoint/-/mjml-head-breakpoint-4.15.3.tgz", - "integrity": "sha512-Eo56FA5C2v6ucmWQL/JBJ2z641pLOom4k0wP6CMZI2utfyiJ+e2Uuinj1KTrgDcEvW4EtU9HrfAqLK9UosLZlg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "4.15.3" - } - }, - "node_modules/mjml-head-font": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-head-font/-/mjml-head-font-4.15.3.tgz", - "integrity": "sha512-CzV2aDPpiNIIgGPHNcBhgyedKY4SX3BJoTwOobSwZVIlEA6TAWB4Z9WwFUmQqZOgo1AkkiTHPZQvGcEhFFXH6g==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "4.15.3" - } - }, - "node_modules/mjml-head-html-attributes": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-head-html-attributes/-/mjml-head-html-attributes-4.15.3.tgz", - "integrity": "sha512-MDNDPMBOgXUZYdxhosyrA2kudiGO8aogT0/cODyi2Ed9o/1S7W+je11JUYskQbncqhWKGxNyaP4VWa+6+vUC/g==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "4.15.3" - } - }, - "node_modules/mjml-head-preview": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-head-preview/-/mjml-head-preview-4.15.3.tgz", - "integrity": "sha512-J2PxCefUVeFwsAExhrKo4lwxDevc5aKj888HBl/wN4EuWOoOg06iOGCxz4Omd8dqyFsrqvbBuPqRzQ+VycGmaA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "4.15.3" - } - }, - "node_modules/mjml-head-style": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-head-style/-/mjml-head-style-4.15.3.tgz", - "integrity": "sha512-9J+JuH+mKrQU65CaJ4KZegACUgNIlYmWQYx3VOBR/tyz+8kDYX7xBhKJCjQ1I4wj2Tvga3bykd89Oc2kFZ5WOw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "4.15.3" - } - }, - "node_modules/mjml-head-title": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-head-title/-/mjml-head-title-4.15.3.tgz", - "integrity": "sha512-IM59xRtsxID4DubQ0iLmoCGXguEe+9BFG4z6y2xQDrscIa4QY3KlfqgKGT69ojW+AVbXXJPEVqrAi4/eCsLItQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "4.15.3" - } - }, - "node_modules/mjml-hero": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-hero/-/mjml-hero-4.15.3.tgz", - "integrity": "sha512-9cLAPuc69yiuzNrMZIN58j+HMK1UWPaq2i3/Fg2ZpimfcGFKRcPGCbEVh0v+Pb6/J0+kf8yIO0leH20opu3AyQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "4.15.3" - } - }, - "node_modules/mjml-image": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-image/-/mjml-image-4.15.3.tgz", - "integrity": "sha512-g1OhSdofIytE9qaOGdTPmRIp7JsCtgO0zbsn1Fk6wQh2gEL55Z40j/VoghslWAWTgT2OHFdBKnMvWtN6U5+d2Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "4.15.3" - } - }, - "node_modules/mjml-migrate": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-migrate/-/mjml-migrate-4.15.3.tgz", - "integrity": "sha512-sr/+35RdxZroNQVegjpfRHJ5hda9XCgaS4mK2FGO+Mb1IUevKfeEPII3F/cHDpNwFeYH3kAgyqQ22ClhGLWNBA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "js-beautify": "^1.6.14", - "lodash": "^4.17.21", - "mjml-core": "4.15.3", - "mjml-parser-xml": "4.15.3", - "yargs": "^17.7.2" - }, - "bin": { - "migrate": "lib/cli.js" - } - }, - "node_modules/mjml-navbar": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-navbar/-/mjml-navbar-4.15.3.tgz", - "integrity": "sha512-VsKH/Jdlf8Yu3y7GpzQV5n7JMdpqvZvTSpF6UQXL0PWOm7k6+LX+sCZimOfpHJ+wCaaybpxokjWZ71mxOoCWoA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "4.15.3" - } - }, - "node_modules/mjml-parser-xml": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-parser-xml/-/mjml-parser-xml-4.15.3.tgz", - "integrity": "sha512-Tz0UX8/JVYICLjT+U8J1f/TFxIYVYjzZHeh4/Oyta0pLpRLeZlxEd71f3u3kdnulCKMP4i37pFRDmyLXAlEuLw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "detect-node": "2.1.0", - "htmlparser2": "^9.1.0", - "lodash": "^4.17.15" - } - }, - "node_modules/mjml-parser-xml/node_modules/htmlparser2": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", - "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.1.0", - "entities": "^4.5.0" - } - }, - "node_modules/mjml-preset-core": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-preset-core/-/mjml-preset-core-4.15.3.tgz", - "integrity": "sha512-1zZS8P4O0KweWUqNS655+oNnVMPQ1Rq1GaZq5S9JfwT1Vh/m516lSmiTW9oko6gGHytt5s6Yj6oOeu5Zm8FoLw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "mjml-accordion": "4.15.3", - "mjml-body": "4.15.3", - "mjml-button": "4.15.3", - "mjml-carousel": "4.15.3", - "mjml-column": "4.15.3", - "mjml-divider": "4.15.3", - "mjml-group": "4.15.3", - "mjml-head": "4.15.3", - "mjml-head-attributes": "4.15.3", - "mjml-head-breakpoint": "4.15.3", - "mjml-head-font": "4.15.3", - "mjml-head-html-attributes": "4.15.3", - "mjml-head-preview": "4.15.3", - "mjml-head-style": "4.15.3", - "mjml-head-title": "4.15.3", - "mjml-hero": "4.15.3", - "mjml-image": "4.15.3", - "mjml-navbar": "4.15.3", - "mjml-raw": "4.15.3", - "mjml-section": "4.15.3", - "mjml-social": "4.15.3", - "mjml-spacer": "4.15.3", - "mjml-table": "4.15.3", - "mjml-text": "4.15.3", - "mjml-wrapper": "4.15.3" - } - }, - "node_modules/mjml-raw": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-raw/-/mjml-raw-4.15.3.tgz", - "integrity": "sha512-IGyHheOYyRchBLiAEgw3UM11kFNmBSMupu2BDdejC6ZiDhEAdG+tyERlsCwDPYtXanvFpGWULIu3XlsUPc+RZw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "4.15.3" - } - }, - "node_modules/mjml-section": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-section/-/mjml-section-4.15.3.tgz", - "integrity": "sha512-JfVPRXH++Hd933gmQfG8JXXCBCR6fIzC3DwiYycvanL/aW1cEQ2EnebUfQkt5QzlYjOkJEH+JpccAsq3ln6FZQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "4.15.3" - } - }, - "node_modules/mjml-social": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-social/-/mjml-social-4.15.3.tgz", - "integrity": "sha512-7sD5FXrESOxpT9Z4Oh36bS6u/geuUrMP1aCg2sjyAwbPcF1aWa2k9OcatQfpRf6pJEhUZ18y6/WBBXmMVmSzXg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "4.15.3" - } - }, - "node_modules/mjml-spacer": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-spacer/-/mjml-spacer-4.15.3.tgz", - "integrity": "sha512-3B7Qj+17EgDdAtZ3NAdMyOwLTX1jfmJuY7gjyhS2HtcZAmppW+cxqHUBwCKfvSRgTQiccmEvtNxaQK+tfyrZqA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "4.15.3" - } - }, - "node_modules/mjml-table": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-table/-/mjml-table-4.15.3.tgz", - "integrity": "sha512-FLx7DcRKTdKdcOCbMyBaeudeHaHpwPveRrBm6WyQe3LXx6FfdmOh59i71/16LFQMgBOD3N4/UJkzxLzlTJzMqQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "4.15.3" - } - }, - "node_modules/mjml-text": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-text/-/mjml-text-4.15.3.tgz", - "integrity": "sha512-+C0hxCmw9kg0XzT6vhE5mFkK6y225nC8UEQcN94K0fBCjPKkM+HqZMwGX205fzdGRi+Bxa55b/VhrIVwdv+8vw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "4.15.3" - } - }, - "node_modules/mjml-validator": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-validator/-/mjml-validator-4.15.3.tgz", - "integrity": "sha512-Xb72KdqRwjv/qM2rJpV22syyP2N3cRQ9VVDrN6u2FSzLq02buFNxmSPJ7CKhat3PrUNdVHU75KZwOf/tz4UEhA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9" - } - }, - "node_modules/mjml-wrapper": { - "version": "4.15.3", - "resolved": "https://registry.npmjs.org/mjml-wrapper/-/mjml-wrapper-4.15.3.tgz", - "integrity": "sha512-ditsCijeHJrmBmObtJmQ18ddLxv5oPyMTdPU8Di8APOnD2zPk7Z4UAuJSl7HXB45oFiivr3MJf4koFzMUSZ6Gg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "4.15.3", - "mjml-section": "4.15.3" - } - }, - "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.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "license": "MIT" - }, - "node_modules/msgpackr": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.0.tgz", - "integrity": "sha512-I8qXuuALqJe5laEBYoFykChhSXLikZmUhccjGsPuSJ/7uPip2TJ7lwdIQwWSAi0jGZDXv4WOP8Qg65QZRuXxXw==", - "optionalDependencies": { - "msgpackr-extract": "^3.0.2" - } - }, - "node_modules/msgpackr-extract": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", - "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", - "hasInstallScript": true, - "optional": true, - "dependencies": { - "node-gyp-build-optional-packages": "5.2.2" - }, - "bin": { - "download-msgpackr-prebuilds": "bin/download-prebuilds.js" - }, - "optionalDependencies": { - "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" - } - }, - "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/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/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "devOptional": 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==", - "license": "MIT" - }, - "node_modules/nestjs-pino": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/nestjs-pino/-/nestjs-pino-4.1.0.tgz", - "integrity": "sha512-I6zcddauD2TNMRbsraEIxNUvHcz0El5QRUYH5eY1+pBzj7R17U+Yoyypoc+akVdSLWJ1r0kDYAZPy2mlhXv6vw==", - "hasInstallScript": true, - "license": "MIT", - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", - "pino-http": "^6.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0" - } - }, - "node_modules/nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/no-case": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", - "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "lower-case": "^1.1.1" - } - }, - "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": "5.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", - "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", - "license": "MIT" - }, - "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-optional-packages": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", - "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.1" - }, - "bin": { - "node-gyp-build-optional-packages": "bin.js", - "node-gyp-build-optional-packages-optional": "optional.js", - "node-gyp-build-optional-packages-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==", - "devOptional": 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==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/nodemailer": { - "version": "6.9.14", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.14.tgz", - "integrity": "sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==", - "license": "MIT-0", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "license": "ISC", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, - "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==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/oauth": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.0.tgz", - "integrity": "sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==", - "license": "MIT" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.values": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", - "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-exit-leak-free": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", - "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "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==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", - "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "is-docker": "^2.0.0", - "is-wsl": "^2.1.1" - }, - "engines": { - "node": ">=8" - }, - "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/ora/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ora/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/ora/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/ora/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/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-event": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz", - "integrity": "sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "p-timeout": "^3.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" - } - }, - "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==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate/node_modules/yocto-queue": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", - "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-timeout": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", - "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "p-finally": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "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==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/p-wait-for": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/p-wait-for/-/p-wait-for-3.2.0.tgz", - "integrity": "sha512-wpgERjNkLrBiFmkMEjuZJEWKKDrNfHCKA1OhyN1wg1FrLkULbviEy6py1AyJUgZ72YWFbZ38FIpnqvVqAlDUwA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "p-timeout": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/param-case": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", - "integrity": "sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "no-case": "^2.2.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==", - "devOptional": 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": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "entities": "^4.4.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", - "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "domhandler": "^5.0.2", - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parseley": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", - "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "leac": "^0.6.0", - "peberminta": "^0.9.0" - }, - "funding": { - "url": "https://ko-fi.com/killymxi" - } - }, - "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/pascal-case/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/pascal-case/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/passport": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", - "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", - "license": "MIT", - "dependencies": { - "passport-strategy": "1.x.x", - "pause": "0.0.1", - "utils-merge": "^1.0.1" - }, - "engines": { - "node": ">= 0.4.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jaredhanson" - } - }, - "node_modules/passport-google-oauth20": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", - "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", - "license": "MIT", - "dependencies": { - "passport-oauth2": "1.x.x" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/passport-jwt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", - "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", - "license": "MIT", - "dependencies": { - "jsonwebtoken": "^9.0.0", - "passport-strategy": "^1.0.0" - } - }, - "node_modules/passport-oauth2": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", - "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", - "license": "MIT", - "dependencies": { - "base64url": "3.x.x", - "oauth": "0.10.x", - "passport-strategy": "1.x.x", - "uid2": "0.0.x", - "utils-merge": "1.x.x" - }, - "engines": { - "node": ">= 0.4.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jaredhanson" - } - }, - "node_modules/passport-strategy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", - "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/path-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "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==", - "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==", - "devOptional": 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.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz", - "integrity": "sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==", - "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/pause": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", - "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" - }, - "node_modules/peberminta": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", - "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==", - "dev": true, - "license": "MIT", - "optional": true, - "funding": { - "url": "https://ko-fi.com/killymxi" - } - }, - "node_modules/pg": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz", - "integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==", - "license": "MIT", - "dependencies": { - "pg-connection-string": "^2.6.4", - "pg-pool": "^3.6.2", - "pg-protocol": "^1.6.1", - "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.6.4", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz", - "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==", - "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.6.2", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.2.tgz", - "integrity": "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==", - "license": "MIT", - "peerDependencies": { - "pg": ">=8.0" - } - }, - "node_modules/pg-protocol": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz", - "integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==", - "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.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "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/pidtree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", - "dev": true, - "license": "MIT", - "bin": { - "pidtree": "bin/pidtree.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/pino": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/pino/-/pino-9.3.2.tgz", - "integrity": "sha512-WtARBjgZ7LNEkrGWxMBN/jvlFiE17LTbBoH0konmBU684Kd0uIiDwBXlcTCW7iJnA6HfIKwUssS/2AC6cDEanw==", - "license": "MIT", - "peer": true, - "dependencies": { - "atomic-sleep": "^1.0.0", - "fast-redact": "^3.1.1", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^1.2.0", - "pino-std-serializers": "^7.0.0", - "process-warning": "^4.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", - "safe-stable-stringify": "^2.3.1", - "sonic-boom": "^4.0.1", - "thread-stream": "^3.0.0" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/pino-abstract-transport": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", - "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", - "license": "MIT", - "peer": true, - "dependencies": { - "readable-stream": "^4.0.0", - "split2": "^4.0.0" - } - }, - "node_modules/pino-abstract-transport/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", - "peer": true, - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/pino-abstract-transport/node_modules/readable-stream": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", - "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", - "license": "MIT", - "peer": true, - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/pino-abstract-transport/node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "peer": true, - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/pino-http": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-10.2.0.tgz", - "integrity": "sha512-am03BxnV3Ckx68OkbH0iZs3indsrH78wncQ6w1w51KroIbvJZNImBKX2X1wjdY8lSyaJ0UrX/dnO2DY3cTeCRw==", - "license": "MIT", - "peer": true, - "dependencies": { - "get-caller-file": "^2.0.5", - "pino": "^9.0.0", - "pino-std-serializers": "^7.0.0", - "process-warning": "^3.0.0" - } - }, - "node_modules/pino-std-serializers": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", - "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", - "license": "MIT", - "peer": true - }, - "node_modules/pino/node_modules/process-warning": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz", - "integrity": "sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==", - "license": "MIT", - "peer": true - }, - "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==", - "devOptional": 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==", - "devOptional": 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==", - "devOptional": 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==", - "devOptional": 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==", - "devOptional": 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==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/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==", - "devOptional": true, - "license": "MIT", - "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/possible-typed-array-names": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", - "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.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/preview-email": { - "version": "3.0.19", - "resolved": "https://registry.npmjs.org/preview-email/-/preview-email-3.0.19.tgz", - "integrity": "sha512-DBS3Nir18YtKc8loYCCOGitmiaQ0vTdahPoiXxwNweJDpmVZo+w3tppufOhoK0m8skpRxT56llYLs3VrORnmNQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ci-info": "^3.8.0", - "display-notification": "2.0.0", - "fixpack": "^4.0.0", - "get-port": "5.1.1", - "mailparser": "^3.6.4", - "nodemailer": "^6.9.2", - "open": "7", - "p-event": "4.2.0", - "p-wait-for": "3.2.0", - "pug": "^3.0.2", - "uuid": "^9.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/preview-email/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "optional": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6.0" - } - }, - "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/process-warning": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", - "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", - "license": "MIT", - "peer": true - }, - "node_modules/promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "asap": "~2.0.3" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", - "dev": true, - "license": "ISC", - "optional": true - }, - "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/pug": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.3.tgz", - "integrity": "sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "pug-code-gen": "^3.0.3", - "pug-filters": "^4.0.0", - "pug-lexer": "^5.0.1", - "pug-linker": "^4.0.0", - "pug-load": "^3.0.0", - "pug-parser": "^6.0.0", - "pug-runtime": "^3.0.1", - "pug-strip-comments": "^2.0.0" - } - }, - "node_modules/pug-attrs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz", - "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "constantinople": "^4.0.1", - "js-stringify": "^1.0.2", - "pug-runtime": "^3.0.0" - } - }, - "node_modules/pug-code-gen": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.3.tgz", - "integrity": "sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "constantinople": "^4.0.1", - "doctypes": "^1.1.0", - "js-stringify": "^1.0.2", - "pug-attrs": "^3.0.0", - "pug-error": "^2.1.0", - "pug-runtime": "^3.0.1", - "void-elements": "^3.1.0", - "with": "^7.0.0" - } - }, - "node_modules/pug-error": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz", - "integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/pug-filters": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz", - "integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "constantinople": "^4.0.1", - "jstransformer": "1.0.0", - "pug-error": "^2.0.0", - "pug-walk": "^2.0.0", - "resolve": "^1.15.1" - } - }, - "node_modules/pug-lexer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz", - "integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "character-parser": "^2.2.0", - "is-expression": "^4.0.0", - "pug-error": "^2.0.0" - } - }, - "node_modules/pug-linker": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz", - "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "pug-error": "^2.0.0", - "pug-walk": "^2.0.0" - } - }, - "node_modules/pug-load": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz", - "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "object-assign": "^4.1.1", - "pug-walk": "^2.0.0" - } - }, - "node_modules/pug-parser": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz", - "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "pug-error": "^2.0.0", - "token-stream": "1.0.0" - } - }, - "node_modules/pug-runtime": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz", - "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/pug-strip-comments": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz", - "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "pug-error": "^2.0.0" - } - }, - "node_modules/pug-walk": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", - "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "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/punycode.js": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", - "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", - "dev": true, - "license": "MIT", - "optional": true, - "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.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.4" - }, - "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/quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", - "license": "MIT", - "peer": true - }, - "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/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "optional": true, - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "devOptional": 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/real-require": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", - "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/redis-errors": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", - "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", - "engines": { - "node": ">=4" - } - }, - "node_modules/redis-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", - "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", - "dependencies": { - "redis-errors": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "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/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", - "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.6", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "set-function-name": "^2.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/relateurl": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.10" - } - }, - "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==", - "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==", - "devOptional": 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==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "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==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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/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/restore-cursor/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/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/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true, - "license": "MIT" - }, - "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", - "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==", - "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", - "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==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/run-applescript": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-3.2.0.tgz", - "integrity": "sha512-Ep0RsvAjnRcBX1p5vogbaBdAGu/8j/ewpvGqnQYunnLd9SM0vWcPJewPKNnWFggf0hF0pwIgwV5XK7qQ7UZ8Qg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "execa": "^0.10.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/run-applescript/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/run-applescript/node_modules/execa": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz", - "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "cross-spawn": "^6.0.0", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/run-applescript/node_modules/get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/run-applescript/node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/run-applescript/node_modules/npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "path-key": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/run-applescript/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/run-applescript/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/run-applescript/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/run-applescript/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/run-applescript/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", - "optional": true - }, - "node_modules/run-applescript/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "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-array-concat": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", - "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4", - "has-symbols": "^1.0.3", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-array-concat/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "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/safe-regex-test": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", - "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-regex": "^1.1.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-stable-stringify": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", - "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10" - } - }, - "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/selderee": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", - "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "parseley": "^0.12.0" - }, - "funding": { - "url": "https://ko-fi.com/killymxi" - } - }, - "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.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "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/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/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.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "license": "MIT", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC" - }, - "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/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "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==", - "license": "MIT" - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/slice-ansi/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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/slick": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", - "integrity": "sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==", - "dev": true, - "license": "MIT (http://mootools.net/license.txt)", - "optional": true, - "engines": { - "node": "*" - } - }, - "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/sonic-boom": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.0.1.tgz", - "integrity": "sha512-hTSD/6JMLyT4r9zeof6UtuBDpjJ9sO08/nmS5djaA9eozT9oOlNdpXSnzcgj4FTqpk3nkLrs61l4gip9r1HCrQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "atomic-sleep": "^1.0.0" - } - }, - "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/speakeasy": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/speakeasy/-/speakeasy-2.0.0.tgz", - "integrity": "sha512-lW2A2s5LKi8rwu77ewisuUOtlCydF/hmQSOJjpTqTj1gZLkNgTaYnyvfxy2WBr4T/h+9c4g8HIITfj83OkFQFw==", - "license": "MIT", - "dependencies": { - "base32.js": "0.0.1" - }, - "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==", - "devOptional": true, - "license": "BSD-3-Clause" - }, - "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==", - "devOptional": 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==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/standard-as-callback": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", - "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" - }, - "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.7.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", - "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", - "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-argv": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", - "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6.19" - } - }, - "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==", - "devOptional": 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/string-width-cjs/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/string-width/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/string.prototype.trim": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", - "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", - "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "devOptional": 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==", - "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==", - "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==", - "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-hyperlinks": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", - "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", - "optional": true, - "peer": true, - "dependencies": { - "has-flag": "^4.0.0", - "supports-color": "^7.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==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/swagger-ui-dist": { - "version": "5.17.14", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz", - "integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==", - "license": "Apache-2.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.1", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", - "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@pkgr/core": "^0.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts" - } - }, - "node_modules/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/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/tar/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, - "node_modules/terminal-link": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", - "optional": true, - "peer": true, - "dependencies": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/terser": { - "version": "5.31.3", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.3.tgz", - "integrity": "sha512-pAfYn3NIZLyZpa83ZKigvj6Rn9c/vd5KfYGX7cN1mnzqgDcxWvrU5ZtAfIKhEXz9nRecw4z3LXkjaq96/qZqAA==", - "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==", - "devOptional": 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==", - "devOptional": 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", - "devOptional": 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==", - "devOptional": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/text-extensions": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", - "integrity": "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "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/thread-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", - "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", - "license": "MIT", - "peer": true, - "dependencies": { - "real-require": "^0.2.0" - } - }, - "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/tlds": { - "version": "1.252.0", - "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.252.0.tgz", - "integrity": "sha512-GA16+8HXvqtfEnw/DTcwB0UU354QE1n3+wh08oFjr6Znl7ZLAeUgYzCcK+/CCrOyE0vnHR8/pu3XXG3vDijXpQ==", - "dev": true, - "license": "MIT", - "optional": true, - "bin": { - "tlds": "bin.js" - } - }, - "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==", - "devOptional": true, - "license": "BSD-3-Clause" - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "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/token-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", - "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "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.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", - "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, - "node_modules/ts-jest": { - "version": "29.2.3", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.3.tgz", - "integrity": "sha512-yCcfVdiBFngVz9/keHin9EnsrQtQtEu3nRykNy9RVp+FiPFFbPJ3Sg6Qg4+TkmH0vMP5qsTKgXSsk80HRwvdgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bs-logger": "0.x", - "ejs": "^3.1.10", - "fast-json-stable-stringify": "2.x", - "jest-util": "^29.0.0", - "json5": "^2.2.3", - "lodash.memoize": "4.x", - "make-error": "1.x", - "semver": "^7.5.3", - "yargs-parser": "^21.0.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-loader/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ts-loader/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/ts-loader/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/ts-loader/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/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/ts-node-dev": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", - "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.1", - "dynamic-dedupe": "^0.3.0", - "minimist": "^1.2.6", - "mkdirp": "^1.0.4", - "resolve": "^1.0.0", - "rimraf": "^2.6.1", - "source-map-support": "^0.5.12", - "tree-kill": "^1.2.2", - "ts-node": "^10.4.0", - "tsconfig": "^7.0.0" - }, - "bin": { - "ts-node-dev": "lib/bin.js", - "tsnd": "lib/bin.js" - }, - "engines": { - "node": ">=0.8.0" - }, - "peerDependencies": { - "node-notifier": "*", - "typescript": "*" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/ts-node-dev/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/ts-node-dev/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/ts-node-dev/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/ts-node-dev/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ts-node-dev/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/tsconfig": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", - "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/strip-bom": "^3.0.0", - "@types/strip-json-comments": "0.0.30", - "strip-bom": "^3.0.0", - "strip-json-comments": "^2.0.0" - } - }, - "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-webpack-plugin/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/tsconfig-paths-webpack-plugin/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/tsconfig-paths-webpack-plugin/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/tsconfig-paths-webpack-plugin/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/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/tsconfig/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/tsconfig/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "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==", - "devOptional": 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/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", - "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", - "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", - "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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.0", - "resolved": "https://registry.npmjs.org/typeorm-extension/-/typeorm-extension-3.6.0.tgz", - "integrity": "sha512-v2mothqX5/0nxK4eGrk+h2tR/+kpyeLJhWVviDOvPJ5zE8AgJyrhOX4lj0qpa+a/BokhJjusSSXF+aRX9DQNtQ==", - "license": "MIT", - "dependencies": { - "@faker-js/faker": "^8.4.1", - "consola": "^3.2.3", - "envix": "^1.5.0", - "locter": "^2.1.0", - "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/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/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/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/typeorm/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/typeorm/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/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/typeorm/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/types-joi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/types-joi/-/types-joi-2.1.0.tgz", - "integrity": "sha512-wdvZWNhDx9syXdes3V+YH0KLRNiwGsg7itbjL27truN1Av3YvnJDc3HGs9kbTpfzi3vV2q0scM2y6iSkm9nriQ==", - "license": "ISC" - }, - "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/uglify-js": { - "version": "3.19.1", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.1.tgz", - "integrity": "sha512-y/2wiW+ceTYR2TSSptAhfnEtpLaQ4Ups5zrjB2d3kuVxHj16j/QJwPl5PvuGy9uARb39J0+iKxcRPvtpsx4A4A==", - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "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/uid2": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", - "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", - "license": "MIT" - }, - "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "license": "MIT" - }, - "node_modules/unicorn-magic": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", - "devOptional": 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.1.2", - "picocolors": "^1.0.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/upper-case": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", - "integrity": "sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==", - "dev": true, - "license": "MIT", - "optional": true - }, - "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": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", - "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==", - "devOptional": 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/valid-data-url": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz", - "integrity": "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/valid-url": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", - "integrity": "sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA==" - }, - "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/void-elements": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", - "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "makeerror": "1.0.12" - } - }, - "node_modules/watchpack": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", - "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", - "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/web-resource-inliner": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-6.0.1.tgz", - "integrity": "sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-colors": "^4.1.1", - "escape-goat": "^3.0.0", - "htmlparser2": "^5.0.0", - "mime": "^2.4.6", - "node-fetch": "^2.6.0", - "valid-data-url": "^3.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/web-resource-inliner/node_modules/dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/web-resource-inliner/node_modules/dom-serializer/node_modules/domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "dependencies": { - "domelementtype": "^2.2.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/web-resource-inliner/node_modules/domhandler": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", - "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "dependencies": { - "domelementtype": "^2.0.1" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/web-resource-inliner/node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/web-resource-inliner/node_modules/domutils/node_modules/domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "dependencies": { - "domelementtype": "^2.2.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/web-resource-inliner/node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/web-resource-inliner/node_modules/htmlparser2": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-5.0.1.tgz", - "integrity": "sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^3.3.0", - "domutils": "^2.4.2", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/fb55/htmlparser2?sponsor=1" - } - }, - "node_modules/web-resource-inliner/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", - "optional": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "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.93.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", - "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.7.1", - "acorn-import-attributes": "^1.9.5", - "browserslist": "^4.21.10", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.0", - "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", - "peer": true, - "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", - "peer": true, - "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/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "license": "ISC", - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "node_modules/with": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", - "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/parser": "^7.9.6", - "@babel/types": "^7.9.6", - "assert-never": "^1.2.1", - "babel-walk": "3.0.0-canary-5" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "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/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "license": "MIT" - }, - "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/wrap-ansi-cjs/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/wrap-ansi-cjs/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/wrap-ansi-cjs/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/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "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==", - "devOptional": 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==", - "devOptional": 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==", - "devOptional": true, - "license": "ISC" - }, - "node_modules/yaml": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", - "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", - "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==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/package.json b/package.json index 723e4b40e..6e7c1e539 100644 --- a/package.json +++ b/package.json @@ -29,32 +29,43 @@ "premigration:run": "npm run build", "migration:run": "npx typeorm migration:run -d dist/src/database/data-source", "migration:revert": "npx typeorm migration:revert -d dist/src/database/data-source", - "seed": "ts-node src/seed.ts" + "seed": "ts-node src/seed.ts", + "postinstall": "npm install --platform=linux --arch=x64 sharp" }, "dependencies": { "@css-inline/css-inline": "^0.14.1", + "@css-inline/css-inline-linux-x64-gnu": "^0.14.1", + "@faker-js/faker": "^8.4.1", + "@google/generative-ai": "^0.17.0", "@nestjs-modules/mailer": "^2.0.2", + "@nestjs/axios": "^3.0.2", "@nestjs/bull": "^10.2.0", - "@faker-js/faker": "^8.4.1", "@nestjs/config": "^3.2.3", "@nestjs/core": "^10.0.0", "@nestjs/jwt": "^10.2.0", "@nestjs/passport": "^10.0.3", - "@nestjs/platform-express": "^10.3.9", + "@nestjs/platform-express": "^10.3.10", + "@nestjs/serve-static": "^4.0.2", "@nestjs/swagger": "^7.4.0", "@nestjs/typeorm": "^10.0.2", "@types/nodemailer": "^6.4.15", "@types/speakeasy": "^2.0.10", + "@vitalets/google-translate-api": "^9.2.0", "bcrypt": "^5.1.1", "bcryptjs": "^2.4.3", - "bull": "^4.15.1", + "bull": "^4.16.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", - "google-auth-library": "^9.12.0", + "csv-writer": "^1.6.0", + "date-fns": "^3.6.0", + "file-type-mime": "^0.4.3", + "google-auth-library": "^9.13.0", "handlebars": "^4.7.8", "html-validator": "^6.0.1", "ioredis": "^5.4.1", "joi": "^17.6.0", + "multer": "^1.4.5-lts.1", + "nestjs-form-data": "^1.9.91", "nestjs-pino": "^4.1.0", "nodemailer": "^6.9.14", "passport": "^0.7.0", @@ -63,12 +74,14 @@ "pg": "^8.12.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", + "sharp": "^0.33.5", "speakeasy": "^2.0.0", "supertest": "^7.0.0", "typeorm": "^0.3.20", "typeorm-extension": "^3.5.1", "types-joi": "^2.1.0", - "uuid": "^10.0.0" + "uuid": "^10.0.0", + "xlsx": "^0.18.5" }, "devDependencies": { "@commitlint/cli": "^19.3.0", @@ -81,6 +94,7 @@ "@types/bcrypt": "^5.0.2", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", + "@types/multer": "^1.4.11", "@types/node": "^20.3.1", "@types/passport-google-oauth2": "^0.1.8", "@types/passport-jwt": "^4.0.1", diff --git a/qa/compress_send.js b/qa/compress_send.js new file mode 100644 index 000000000..47bd2fa15 --- /dev/null +++ b/qa/compress_send.js @@ -0,0 +1,204 @@ +const fs = require('fs'); +const path = require('path'); +const json = require('big-json'); +const axios = require('axios'); + +const { pipeline } = require('stream'); + +const readStream = fs.createReadStream('./result.json'); +const parseStream = json.createParseStream(); + +parseStream.on('data', async function (result) { + const tests = () => { + const Authentication = []; + const Settings = []; + const Avatars = []; + const Blogs = []; + const Rooms = []; + const FAQs = []; + const ContactUs = []; + const Default = []; + const Payment = []; + const Game = []; + const AboutUs = []; + + result.collection.item?.map(cat => { + const category = cat.name; + const subCategories = []; + if (cat.name === 'Authentication' || cat.name === 'Settings') { + cat.item.forEach(element => { + element.item.forEach(el => { + subCategories.push(el.name); + }); + }); + } else { + cat.item.forEach(element => { + subCategories.push(element.name); + }); + } + + testPassesSummary.forEach(test => { + if (subCategories.includes(test.name)) { + switch (category) { + case 'Authentication': + Authentication.push(test); + break; + case 'Settings': + Settings.push(test); + break; + case 'Avatars': + Avatars.push(test); + break; + case 'Blogs': + Blogs.push(test); + break; + case 'Rooms': + Rooms.push(test); + break; + case 'FAQs': + FAQs.push(test); + break; + case 'Contact Us': + ContactUs.push(test); + break; + case 'Default': + Default.push(test); + break; + case 'Payment': + Payment.push(test); + break; + case 'Game': + Game.push(test); + break; + case 'About Us': + AboutUs.push(test); + break; + default: + break; + } + } + }); + }); + return { + AboutUs, + Authentication, + Avatars, + Blogs, + ContactUs, + Default, + FAQs, + Game, + Payment, + Rooms, + Settings, + }; + }; + const testPassesSummary = result.run.executions.map(execution => ({ + name: execution.item.name, + assertions: execution.assertions?.map(assertion => ({ + assertion: assertion.assertion, + error: assertion.error ? assertion.error.message : null, + })), + requestUrl: execution.request.url.path.join('/'), + host: execution.request.url.host.join('.'), + responseTime: execution.response.responseTime, + statusCode: execution.response.code, + status: execution.response.status, + })); + + // Create the summary object + const summary = { + last_checked: new Date(), + stats: result.run.stats, + tests: tests(), + }; + const parsedData = parseNewmanResults(summary); + await sendApiStatus(parsedData); +}); + +pipeline(readStream, parseStream, err => { + if (err) { + console.error('Pipeline failed.', err); + } else { + console.log('Pipeline succeeded.'); + } +}); + +const parseNewmanResults = summary => { + const endpoints = [ + 'Authentication', + 'Settings', + 'Avatars', + 'Blogs', + 'Rooms', + 'FAQs', + 'ContactUs', + 'Default', + 'Payment', + 'Game', + 'AboutUs', + ]; + + const finalData = []; + let assertions = 0; + endpoints.forEach(name => { + let responseTimeSum = 0; + let count = 0; + const api_group = `${name} API`; + let status = 'operational'; + let requests = []; + + if (summary.tests[name]) { + summary.tests[name].forEach(request => { + const eachRequest = { + requestName: request.name, + requestUrl: request.requestUrl, + statusCode: request.statusCode, + status: request.status, + responseTime: request.responseTime, + errors: [], + }; + count++; + responseTimeSum += request.responseTime; + request.assertions?.forEach(assertion => { + assertions++; + if (assertion.error) { + status = 'down'; + eachRequest.errors.push(assertion.error); + } + }); + requests.push(eachRequest); + }); + const details = status === 'down' ? 'Some tests failed' : 'All tests passed'; + finalData.push({ + api_group, + status, + details, + requests, + }); + } else { + finalData.push({ + api_group, + status: 'No tests available', + details: 'No data', + requests, + }); + } + }); + return finalData; +}; + +async function sendApiStatus(apiStatusData) { + try { + const response = await axios.post(`${process.env.API_URL}api-status`, apiStatusData); + console.log(response?.data); + if (response.status === 201) { + console.log(`Successfully sent data `); + } else { + console.error(`Failed to send data `); + } + } catch (error) { + console.error(`Error sending data:`, error.message); + console.log(error); + } +} diff --git a/qa/index.js b/qa/index.js new file mode 100644 index 000000000..0dc3641bf --- /dev/null +++ b/qa/index.js @@ -0,0 +1,80 @@ +const { spawn } = require('child_process'); +const fs = require('fs'); +const path = require('path'); +require('dotenv').config(); + +const resultFilePath = path.resolve(__dirname, 'boilerplate_report.json'); + +const postmanApiKey = process.env.POSTMAN_API_KEY; + +const runTasks = async () => { + try { + console.log('Generating test reports...'); + + // Spawn a new process for running Newman tests + const newmanProcess = spawn('npx', [ + 'newman', + 'run', + `https://api.getpostman.com/collections/37678338-3145218a-98a5-49f6-87be-18ec1ca6e0db?apikey=${postmanApiKey}`, + '-e', + `https://api.getpostman.com/environments/37678338-5b07d664-877b-40d0-ae8f-fddc791af401?apikey=${postmanApiKey}`, + '--reporters', + 'cli,json', + '--reporter-json-export', + resultFilePath, + ]); + + // Handle stdout + newmanProcess.stdout.on('data', data => { + console.log(`stdout: ${data}`); + }); + + // Handle stderr + newmanProcess.stderr.on('data', data => { + console.error(`stderr: ${data}`); + }); + + // Handle process close + newmanProcess.on('close', code => { + if (code !== 0) { + console.warn(`Newman process exited with code ${code}. There may be test failures.`); + } + console.log('Finished running tests and generating reports'); + + // Spawn a new process for compressing the report and making API requests + console.log('Running test result compression and API request...'); + + const compressProcess = spawn('node', ['compress_send.js']); + + compressProcess.stdout.on('data', data => { + console.log(`stdout: ${data}`); + }); + + compressProcess.stderr.on('data', data => { + console.error(`stderr: ${data}`); + }); + + compressProcess.on('close', code => { + if (code !== 0) { + console.error(`Compression process exited with code ${code}`); + return; + } + console.log('Finished compressing test results and making API requests'); + + // Remove the result.json file after successful compression + fs.unlink(resultFilePath, err => { + if (err) { + console.error(`Error removing result.json: ${err.message}`); + return; + } + console.log('Successfully removed result.json'); + }); + }); + }); + } catch (error) { + console.error(`Error executing commands: ${error.message}`); + console.log(error.stack); + } +}; + +runTasks(); diff --git a/qa/test.sh b/qa/test.sh new file mode 100644 index 000000000..f9176307e --- /dev/null +++ b/qa/test.sh @@ -0,0 +1,5 @@ +npm install dotenv +npm install newman +npm install axios +npm install big-json +node ./index.js diff --git a/src/app.e2e.spec.ts b/src/app.e2e.spec.ts index 518e84b80..0c6b7511c 100644 --- a/src/app.e2e.spec.ts +++ b/src/app.e2e.spec.ts @@ -14,7 +14,7 @@ describe('Health Check Test', () => { it('should return healthy endpoint', async () => { const result = { message: 'This is a healthy endpoint', status_code: 200 }; - expect(await healthController.health()).toStrictEqual(result); + expect(await healthController.health()).toMatchObject(result); }); }); diff --git a/src/app.module.ts b/src/app.module.ts index ce1dc8b97..b134d4bdb 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,48 +1,60 @@ import { MailerModule } from '@nestjs-modules/mailer'; import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter'; +import { BullModule } from '@nestjs/bull'; import { Module, ValidationPipe } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { APP_PIPE } from '@nestjs/core'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { BullModule } from '@nestjs/bull'; import * as Joi from 'joi'; import { LoggerModule } from 'nestjs-pino'; +import authConfig from '../config/auth.config'; import serverConfig from '../config/server.config'; import dataSource from './database/data-source'; import { SeedingModule } from './database/seeding/seeding.module'; +import { AuthGuard } from './guards/auth.guard'; import HealthController from './health.controller'; import { AuthModule } from './modules/auth/auth.module'; -import { EmailService } from './modules/email/email.service'; -import { JobsModule } from './modules/jobs/jobs.module'; +import { BillingPlanModule } from './modules/billing-plans/billing-plan.module'; +import { BlogModule } from './modules/blogs/blogs.module'; +import { CommentsModule } from './modules/comments/comments.module'; +import { ContactUsModule } from './modules/contact-us/contact-us.module'; +import { RevenueModule } from './modules/dashboard/dashboard.module'; +import { EmailModule } from './modules/email/email.module'; +import { FaqModule } from './modules/faq/faq.module'; +import { FlutterwaveModule } from './modules/flutterwave/flutterwave.module'; +import { HelpCenterModule } from './modules/help-center/help-center.module'; import { InviteModule } from './modules/invite/invite.module'; +import { JobsModule } from './modules/jobs/jobs.module'; +import { NewsletterSubscriptionModule } from './modules/newsletter-subscription/newsletter-subscription.module'; +import { NotificationSettingsModule } from './modules/notification-settings/notification-settings.module'; +import { NotificationsModule } from './modules/notifications/notifications.module'; import { OrganisationsModule } from './modules/organisations/organisations.module'; import { OtpModule } from './modules/otp/otp.module'; -import authConfig from '../config/auth.config'; -import { AuthGuard } from './guards/auth.guard'; -import { EmailModule } from './modules/email/email.module'; -import { OtpService } from './modules/otp/otp.service'; +import { OrganisationPermissionsModule } from './modules/permissions/permissions.module'; import { ProductsModule } from './modules/products/products.module'; -import { BillingPlanModule } from './modules/billing-plans/billing-plan.module'; -import { NotificationSettingsModule } from './modules/notification-settings/notification-settings.module'; import { ProfileModule } from './modules/profile/profile.module'; +import { RoleModule } from './modules/role/role.module'; import { SqueezeModule } from './modules/squeeze/squeeze.module'; +import { SubscriptionsModule } from './modules/subscriptions/subscriptions.module'; +import { TeamsModule } from './modules/teams/teams.module'; import { TestimonialsModule } from './modules/testimonials/testimonials.module'; import { TimezonesModule } from './modules/timezones/timezones.module'; import { UserModule } from './modules/user/user.module'; +import { WaitlistModule } from './modules/waitlist/waitlist.module'; import ProbeController from './probe.controller'; import { RunTestsModule } from './run-tests/run-tests.module'; -import { ContactUsModule } from './modules/contact-us/contact-us.module'; -import { OrganisationPermissionsModule } from './modules/organisation-permissions/organisation-permissions.module'; -import { NotificationsModule } from './modules/notifications/notifications.module'; -import { WaitlistModule } from './modules/waitlist/waitlist.module'; -import { HelpCenterModule } from './modules/help-center/help-center.module'; -import { OrganisationRoleModule } from './modules/organisation-role/organisation-role.module'; -import { FaqModule } from './modules/faq/faq.module'; -import { NewsletterSubscriptionModule } from './modules/newsletter-subscription/newsletter-subscription.module'; -import { TeamsModule } from './modules/teams/teams.module'; +import { BlogCategoryModule } from './modules/blog-category/blog-category.module'; +import { ServeStaticModule } from '@nestjs/serve-static'; +import { join } from 'path'; +import { LanguageGuard } from './guards/language.guard'; +import { ApiStatusModule } from './modules/api-status/api-status.module'; @Module({ providers: [ + { + provide: 'APP_GUARD', + useClass: LanguageGuard, + }, { provide: 'CONFIG', useClass: ConfigService, @@ -92,6 +104,41 @@ import { TeamsModule } from './modules/teams/teams.module'; TestimonialsModule, EmailModule, InviteModule, + + MailerModule.forRootAsync({ + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => ({ + transport: { + host: configService.get('SMTP_HOST'), + port: configService.get('SMTP_PORT'), + auth: { + user: configService.get('SMTP_USER'), + pass: configService.get('SMTP_PASSWORD'), + }, + }, + defaults: { + from: `"Team Remote Bingo" <${configService.get('SMTP_USER')}>`, + }, + template: { + dir: process.cwd() + '/src/modules/email/templates', + adapter: new HandlebarsAdapter(), + options: { + strict: true, + }, + }, + }), + inject: [ConfigService], + }), + BullModule.forRootAsync({ + useFactory: () => ({ + redis: { + host: authConfig().redis.host, + port: +authConfig().redis.port, + password: authConfig().redis.password, + username: authConfig().redis.username, + }, + }), + }), OrganisationsModule, SqueezeModule, NotificationSettingsModule, @@ -103,7 +150,7 @@ import { TeamsModule } from './modules/teams/teams.module'; BillingPlanModule, JobsModule, ProfileModule, - OrganisationRoleModule, + RoleModule, OrganisationPermissionsModule, RunTestsModule, ContactUsModule, @@ -113,6 +160,20 @@ import { TeamsModule } from './modules/teams/teams.module'; WaitlistModule, NewsletterSubscriptionModule, TeamsModule, + FlutterwaveModule, + BlogModule, + CommentsModule, + SubscriptionsModule, + RevenueModule, + BlogCategoryModule, + ServeStaticModule.forRoot({ + rootPath: join(__dirname, 'uploads'), + serveRoot: '/uploads', + serveStaticOptions: { + index: false, + }, + }), + ApiStatusModule, ], controllers: [HealthController, ProbeController], }) diff --git a/src/database/data-source.ts b/src/database/data-source.ts index 1519c2bd5..b7c0a534c 100644 --- a/src/database/data-source.ts +++ b/src/database/data-source.ts @@ -1,28 +1,29 @@ -import { DataSource, DataSourceOptions } from 'typeorm'; -import { ConfigService } from '@nestjs/config'; -import * as dotenv from 'dotenv'; - -dotenv.config(); - -const isDevelopment = process.env.NODE_ENV === 'development'; - -const dataSource = new DataSource({ - type: process.env.DB_TYPE as 'postgres', - username: process.env.DB_USERNAME, - password: process.env.DB_PASSWORD, - host: process.env.DB_HOST, - database: process.env.DB_DATABASE, - entities: [process.env.DB_ENTITIES], - migrations: [process.env.DB_MIGRATIONS], - synchronize: isDevelopment, - migrationsTableName: 'migrations', - ssl: process.env.DB_SSL === 'true', -}); -export async function initializeDataSource() { - if (!dataSource.isInitialized) { - await dataSource.initialize(); - } - return dataSource; -} - -export default dataSource; +import { DataSource, DataSourceOptions } from 'typeorm'; +import { ConfigService } from '@nestjs/config'; +import * as dotenv from 'dotenv'; + +dotenv.config(); + +const isDevelopment = process.env.NODE_ENV === 'development'; + +const dataSource = new DataSource({ + type: process.env.DB_TYPE as 'postgres', + username: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD, + host: process.env.DB_HOST, + port: +process.env.DB_PORT, + database: process.env.DB_NAME, + entities: [process.env.DB_ENTITIES], + migrations: [process.env.DB_MIGRATIONS], + synchronize: isDevelopment, + migrationsTableName: 'migrations', + ssl: process.env.DB_SSL === 'true', +}); +export async function initializeDataSource() { + if (!dataSource.isInitialized) { + await dataSource.initialize(); + } + return dataSource; +} + +export default dataSource; diff --git a/src/database/seeding/seeding.controller.ts b/src/database/seeding/seeding.controller.ts index a89977e72..ea1164805 100644 --- a/src/database/seeding/seeding.controller.ts +++ b/src/database/seeding/seeding.controller.ts @@ -1,10 +1,11 @@ import { Body, Controller, Get, Post } from '@nestjs/common'; -import { SeedingService } from './seeding.service'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { skipAuth } from '../../helpers/skipAuth'; import { CreateAdminDto } from './dto/admin.dto'; -import { User } from '../../modules/user/entities/user.entity'; import { CreateAdminResponseDto } from './dto/create-admin-response.dto'; +import { SeedingService } from './seeding.service'; +@ApiTags('Seed') @skipAuth() @Controller('seed') export class SeedingController { @@ -25,4 +26,10 @@ export class SeedingController { async seedSuperAdmin(@Body() adminDetails: CreateAdminDto): Promise { return this.seedingService.createSuperAdmin(adminDetails); } + + @Post('transactions') + @ApiOperation({ summary: 'Seed transactions' }) + async seedTransactions() { + return this.seedingService.seedTransactions(); + } } diff --git a/src/database/seeding/seeding.service.ts b/src/database/seeding/seeding.service.ts index 8e45b702d..ef06e7cef 100644 --- a/src/database/seeding/seeding.service.ts +++ b/src/database/seeding/seeding.service.ts @@ -7,19 +7,26 @@ import { UnauthorizedException, } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import { User, UserType } from '../../modules/user/entities/user.entity'; -import { Organisation } from '../../modules/organisations/entities/organisations.entity'; +import { v4 as uuidv4 } from 'uuid'; +import { ADMIN_CREATED, INVALID_ADMIN_SECRET, SERVER_ERROR } from '../../helpers/SystemMessages'; +import { Cart } from '../../modules/dashboard/entities/cart.entity'; +import { OrderItem } from '../../modules/dashboard/entities/order-items.entity'; +import { Order } from '../../modules/dashboard/entities/order.entity'; +import { Transaction } from '../../modules/dashboard/entities/transaction.entity'; import { Invite } from '../../modules/invite/entities/invite.entity'; -import { Product, ProductSizeType } from '../../modules/products/entities/product.entity'; +import { Notification } from '../../modules/notifications/entities/notifications.entity'; +import { Organisation } from '../../modules/organisations/entities/organisations.entity'; +import { DefaultPermissions } from '../../modules/permissions/entities/default-permissions.entity'; +import { PermissionCategory } from '../../modules/permissions/helpers/PermissionCategory'; import { ProductCategory } from '../../modules/product-category/entities/product-category.entity'; -import { DefaultPermissions } from '../../modules/organisation-permissions/entities/default-permissions.entity'; -import { PermissionCategory } from '../../modules/organisation-permissions/helpers/PermissionCategory'; +import { Product, ProductSizeType } from '../../modules/products/entities/product.entity'; + import { Profile } from '../../modules/profile/entities/profile.entity'; -import { Notification } from '../../modules/notifications/entities/notifications.entity'; -import { v4 as uuidv4 } from 'uuid'; +import { Role } from '../../modules/role/entities/role.entity'; +import { User } from '../../modules/user/entities/user.entity'; import { CreateAdminDto } from './dto/admin.dto'; -import { ADMIN_CREATED, INVALID_ADMIN_SECRET, SERVER_ERROR } from '../../helpers/SystemMessages'; import { CreateAdminResponseDto } from './dto/create-admin-response.dto'; +import { OrganisationUserRole } from '../../modules/role/entities/organisation-user-role.entity'; @Injectable() export class SeedingService { @@ -34,9 +41,12 @@ export class SeedingService { const categoryRepository = this.dataSource.getRepository(ProductCategory); const defaultPermissionRepository = this.dataSource.getRepository(DefaultPermissions); const notificationRepository = this.dataSource.getRepository(Notification); + const defaultRoleRepository = this.dataSource.getRepository(Role); + const organisationUserRoleRepository = this.dataSource.getRepository(OrganisationUserRole); try { const existingPermissions = await defaultPermissionRepository.count(); + const existingRoles = await defaultRoleRepository.count(); //Populate the database with default permissions if none exits else stop execution if (existingPermissions <= 0) { @@ -50,6 +60,18 @@ export class SeedingService { await defaultPermissionRepository.save(defaultPermissions); } + //Populate the database with default Roles if none exits else stop execution + // if (existingRoles <= 0) { + // const defaultRoles = Object.values(RoleCategory).map(name => + // defaultRoleRepository.create({ + // description: RoleCategoryDescriptions[name], + // }) + // ); + + // // Save all default roles to the database + // await defaultRoleRepository.save(defaultRoles); + // } + const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); @@ -61,23 +83,55 @@ export class SeedingService { } try { - const u1 = userRepository.create({ - first_name: 'John', - last_name: 'Smith', - email: 'john.smith@example.com', - password: 'password', + const roles1 = defaultRoleRepository.create({ + name: 'super-admin', + description: '', }); - const u2 = userRepository.create({ - first_name: 'Jane', - last_name: 'Smith', - email: 'jane.smith@example.com', - password: 'password', + const roles2 = defaultRoleRepository.create({ + name: 'admin', + description: '', }); - await userRepository.save([u1, u2]); + await defaultRoleRepository.save([roles1, roles2]); + const savedRoles = await defaultRoleRepository.find(); + + if (savedRoles.length !== 2) { + throw new Error('Failed to create all roles'); + } + + const u1 = new User(); + const userObject1 = { + first_name: 'Alpha', + last_name: 'Smith', + email: 'user1@admin.com', + password: 'Password@1', + }; + Object.assign(u1, userObject1); + await userRepository.save(u1); + + const u2 = new User(); + const userObject2 = { + first_name: 'Admin', + last_name: 'User', + email: 'user2@admin.com', + password: 'Password@2', + }; + Object.assign(u2, userObject2); + await userRepository.save(u2); + + const u3 = new User(); + const userObject = { + first_name: 'Jane', + last_name: 'Smith', + email: 'user3@admin.com', + password: 'Password@3', + }; + Object.assign(u3, userObject); + await userRepository.save(u3); const savedUsers = await userRepository.find(); - if (savedUsers.length !== 2) { + + if (savedUsers.length !== 3) { throw new Error('Failed to create all users'); } @@ -102,6 +156,26 @@ export class SeedingService { await userRepository.save(savedUsers); + const usr_org_rol1 = organisationUserRoleRepository.create({ + userId: savedUsers[0].id, + roleId: savedRoles[0].id, + }); + const usr_org_rol2 = organisationUserRoleRepository.create({ + userId: savedUsers[1].id, + roleId: savedRoles[0].id, + }); + const usr_org_rol3 = organisationUserRoleRepository.create({ + userId: savedUsers[2].id, + roleId: savedRoles[0].id, + }); + + await organisationUserRoleRepository.save([usr_org_rol1, usr_org_rol2, usr_org_rol3]); + const savedUsrRole = await profileRepository.find(); + + if (savedUsrRole.length !== 3) { + throw new Error('Failed to create all User Org roles'); + } + const or1 = organisationRepository.create({ name: 'Org 1', description: 'Description 1', @@ -112,7 +186,6 @@ export class SeedingService { state: 'state1', address: 'address1', owner: savedUsers[0], - creator: savedUsers[0], isDeleted: false, }); @@ -126,7 +199,6 @@ export class SeedingService { state: 'state2', address: 'address2', owner: savedUsers[0], - creator: savedUsers[0], isDeleted: false, }); @@ -158,6 +230,7 @@ export class SeedingService { name: 'Product 1', description: 'Description for Product 1', size: ProductSizeType.STANDARD, + category: 'electricity', quantity: 1, price: 500, org: or1, @@ -166,6 +239,7 @@ export class SeedingService { name: 'Product 2', description: 'Description for Product 2', size: ProductSizeType.LARGE, + category: 'electricity', quantity: 2, price: 50, org: or2, @@ -174,6 +248,7 @@ export class SeedingService { name: 'Product 2', description: 'Description for Product 2', size: ProductSizeType.STANDARD, + category: 'electricity', quantity: 2, price: 50, org: or1, @@ -182,6 +257,7 @@ export class SeedingService { name: 'Product 2', description: 'Description for Product 2', size: ProductSizeType.SMALL, + category: 'clothing', quantity: 2, price: 50, org: or2, @@ -274,7 +350,7 @@ export class SeedingService { const { ADMIN_SECRET } = process.env; if (secret !== ADMIN_SECRET) throw new UnauthorizedException(INVALID_ADMIN_SECRET); - user.user_type = UserType.SUPER_ADMIN; + // user.user_type = UserType.SUPER_ADMIN; const admin = await userRepository.save(user); return { status: 201, message: ADMIN_CREATED, data: admin }; } catch (error) { @@ -283,4 +359,148 @@ export class SeedingService { throw new InternalServerErrorException(SERVER_ERROR); } } + + async seedTransactions() { + const cartRepository = this.dataSource.getRepository(Cart); + const orderRepository = this.dataSource.getRepository(Order); + const orderItemRepository = this.dataSource.getRepository(OrderItem); + const transactionRepository = this.dataSource.getRepository(Transaction); + const userRepository = this.dataSource.getRepository(User); + const productRepository = this.dataSource.getRepository(Product); + + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + const savedUsers = await userRepository.find(); + const savedProducts = await productRepository.find(); + + const orders = [ + orderRepository.create({ + user: savedUsers[0], + total_price: 1000, + }), + orderRepository.create({ + user: savedUsers[1], + total_price: 1500, + }), + orderRepository.create({ + user: savedUsers[0], + total_price: 750, + }), + orderRepository.create({ + user: savedUsers[1], + total_price: 1250, + }), + orderRepository.create({ + user: savedUsers[0], + total_price: 2000, + }), + ]; + + await orderRepository.save(orders); + + const orderItems = [ + orderItemRepository.create({ + order: orders[0], + product: savedProducts[0], + quantity: 2, + total_price: 500, + }), + orderItemRepository.create({ + order: orders[1], + product: savedProducts[1], + quantity: 3, + total_price: 1500, + }), + orderItemRepository.create({ + order: orders[2], + product: savedProducts[2], + quantity: 1, + total_price: 750, + }), + orderItemRepository.create({ + order: orders[3], + product: savedProducts[0], + quantity: 5, + total_price: 1250, + }), + orderItemRepository.create({ + order: orders[4], + product: savedProducts[1], + quantity: 4, + total_price: 2000, + }), + ]; + + await orderItemRepository.save(orderItems); + + const carts = [ + cartRepository.create({ + user: savedUsers[0], + product: savedProducts[0], + quantity: 1, + }), + cartRepository.create({ + user: savedUsers[1], + product: savedProducts[1], + quantity: 2, + }), + cartRepository.create({ + user: savedUsers[0], + product: savedProducts[2], + quantity: 1, + }), + cartRepository.create({ + user: savedUsers[1], + product: savedProducts[0], + quantity: 3, + }), + cartRepository.create({ + user: savedUsers[0], + product: savedProducts[1], + quantity: 2, + }), + ]; + + await cartRepository.save(carts); + + const currentMonth = new Date().getMonth(); + const currentYear = new Date().getFullYear(); + + const currentMonthDate = (day: number) => new Date(currentYear, currentMonth, day); + const previousMonthDate = (day: number) => new Date(currentYear, currentMonth - 1, day); + + const transactions = [ + transactionRepository.create({ + order: orders[0], + amount: 1000, + date: currentMonthDate(1), + }), + transactionRepository.create({ + order: orders[1], + amount: 1500, + date: currentMonthDate(10), + }), + transactionRepository.create({ + order: orders[2], + amount: 750, + date: currentMonthDate(20), + }), + transactionRepository.create({ + order: orders[3], + amount: 1250, + date: previousMonthDate(1), + }), + transactionRepository.create({ + order: orders[4], + amount: 2000, + date: previousMonthDate(15), + }), + ]; + + await transactionRepository.save(transactions); + + await queryRunner.commitTransaction(); + } } diff --git a/src/guards/authorization.guard.ts b/src/guards/authorization.guard.ts index ad3f4c377..b55bbef7b 100644 --- a/src/guards/authorization.guard.ts +++ b/src/guards/authorization.guard.ts @@ -2,41 +2,53 @@ import { Injectable, CanActivate, ExecutionContext, HttpStatus } from '@nestjs/c import { Reflector } from '@nestjs/core'; import { Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; -import { UserType } from '../modules/user/entities/user.entity'; +import { User } from '../modules/user/entities/user.entity'; import * as SYS_MSG from '../helpers/SystemMessages'; import { Organisation } from './../modules/organisations/entities/organisations.entity'; import { CustomHttpException } from '../helpers/custom-http-filter'; +import { OrganisationUserRole } from '../modules/role/entities/organisation-user-role.entity'; +import { Role } from '../modules/role/entities/role.entity'; @Injectable() export class OwnershipGuard implements CanActivate { constructor( private reflector: Reflector, @InjectRepository(Organisation) - private readonly organisationRepository: Repository + private readonly organisationRepository: Repository, + @InjectRepository(User) + private readonly userRepository: Repository, + @InjectRepository(OrganisationUserRole) + private readonly organisationMembersRole: Repository, + @InjectRepository(Role) + private readonly userRoleManager: Repository ) {} async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); - const user = request.user; - const organisationId = request.params.id; + const userId = request.user.sub; + const organisationId = request.params.orgId || request.params.org_id || request.params.id; + + const adminUserRole = await this.organisationMembersRole.find({ where: { userId }, relations: ['role'] }); + if (adminUserRole.length) { + const roles = adminUserRole.map(instance => instance.role.name); + if (roles.includes('super-admin')) { + return true; + } + } - if (user.user_type === UserType.SUPER_ADMIN) { - return true; + if (!organisationId) { + throw new CustomHttpException('Invalid Organisation', HttpStatus.BAD_REQUEST); } const organisation = await this.organisationRepository.findOne({ where: { id: organisationId }, - relations: ['owner', 'creator'], + relations: ['owner'], }); if (!organisation) { throw new CustomHttpException(SYS_MSG.ORG_NOT_FOUND, HttpStatus.NOT_FOUND); } - if ( - organisation.owner.id === user.sub || - organisation.creator.id === user.sub || - organisation.owner.id === user.id || - organisation.creator.id === user.id - ) { + + if (organisation.owner.id === userId) { return true; } throw new CustomHttpException(SYS_MSG.NOT_ORG_OWNER, HttpStatus.FORBIDDEN); diff --git a/src/guards/language.guard.ts b/src/guards/language.guard.ts new file mode 100644 index 000000000..6c6701bda --- /dev/null +++ b/src/guards/language.guard.ts @@ -0,0 +1,14 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; + +@Injectable() +export class LanguageGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest(); + const language = request.headers['accept-language'] || 'en'; + request.language = language; + return true; + } +} diff --git a/src/guards/member.guard.ts b/src/guards/member.guard.ts new file mode 100644 index 000000000..8f50e8978 --- /dev/null +++ b/src/guards/member.guard.ts @@ -0,0 +1,64 @@ +import { Injectable, CanActivate, ExecutionContext, HttpStatus } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { Repository } from 'typeorm'; +import { InjectRepository } from '@nestjs/typeorm'; +import * as SYS_MSG from '../helpers/SystemMessages'; +import { Organisation } from './../modules/organisations/entities/organisations.entity'; +import { CustomHttpException } from '../helpers/custom-http-filter'; +import { PermissionCategory } from '../modules/permissions/helpers/PermissionCategory'; +import { PERMISSIONS_KEY } from './permission.decorator'; +import { OrganisationUserRole } from '../modules/role/entities/organisation-user-role.entity'; +import { Role } from '../modules/role/entities/role.entity'; + +@Injectable() +export class MembershipGuard implements CanActivate { + constructor( + private reflector: Reflector, + @InjectRepository(Organisation) + private readonly organisationRepository: Repository, + @InjectRepository(OrganisationUserRole) + private readonly organisationMembersRole: Repository, + @InjectRepository(Role) + private readonly userRoleManager: Repository + ) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const currentUserId = request.user.sub; + + const organisationId = request.params.org_id; + const adminRole = await this.userRoleManager.findOne({ + where: { name: 'member' }, + relations: ['permissions'], + }); + const requiredPermissions = adminRole.permissions.map(permission => permission.title); + + const organisation = await this.organisationRepository.findOne({ + where: { id: organisationId }, + relations: ['owner'], + }); + + if (!organisation) { + throw new CustomHttpException(SYS_MSG.ORG_NOT_FOUND, HttpStatus.NOT_FOUND); + } + + if (organisation.owner.id === currentUserId) return true; + + const userRole = ( + await this.organisationMembersRole.findOne({ + where: { organisationId: organisation.id, userId: currentUserId }, + relations: ['role', 'role.permissions'], + }) + ).role; + + const userHasPerms = userRole.permissions.some(permission => { + return requiredPermissions.includes(permission.title); + }); + + if (userHasPerms) { + return true; + } else { + throw new CustomHttpException(SYS_MSG.FORBIDDEN_ACTION, HttpStatus.FORBIDDEN); + } + } +} diff --git a/src/guards/permission.decorator.ts b/src/guards/permission.decorator.ts new file mode 100644 index 000000000..7d009a780 --- /dev/null +++ b/src/guards/permission.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common'; +import { PermissionCategory } from '../modules/permissions/helpers/PermissionCategory'; +export const PERMISSIONS_KEY = 'permissions'; +export const Permissions = (...permissions: PermissionCategory[]) => SetMetadata(PERMISSIONS_KEY, permissions); diff --git a/src/guards/super-admin.guard.ts b/src/guards/super-admin.guard.ts index e6cef8edc..abb75e46a 100644 --- a/src/guards/super-admin.guard.ts +++ b/src/guards/super-admin.guard.ts @@ -4,21 +4,43 @@ import { User, UserType } from '../modules/user/entities/user.entity'; import { Repository } from 'typeorm'; import * as SYS_MSG from '../helpers/SystemMessages'; import { CustomHttpException } from '../helpers/custom-http-filter'; +import { Organisation } from '../modules/organisations/entities/organisations.entity'; +import { OrganisationUserRole } from '../modules/role/entities/organisation-user-role.entity'; +import { Role } from '../modules/role/entities/role.entity'; @Injectable() export class SuperAdminGuard implements CanActivate { constructor( @InjectRepository(User) - private readonly userRepository: Repository + private readonly userRepository: Repository, + @InjectRepository(Organisation) + private readonly organisationRepository: Repository, + @InjectRepository(OrganisationUserRole) + private readonly organisationMembersRole: Repository, + @InjectRepository(Role) + private readonly userRoleManager: Repository ) {} async canActivate(context: ExecutionContext): Promise { - const { sub } = context.switchToHttp().getRequest().user; - const user = await this.userRepository.findOne({ where: { id: sub } }); + const request = context.switchToHttp().getRequest(); + const currentUserId = request.user.sub; - if (user.user_type !== UserType.SUPER_ADMIN) - throw new CustomHttpException(SYS_MSG.FORBIDDEN_ACTION, HttpStatus.FORBIDDEN); + const adminRole = await this.userRoleManager.findOne({ + where: { name: 'super-admin' }, + relations: ['permissions'], + }); + if (!adminRole) { + throw new CustomHttpException('Admin Role does not exist', HttpStatus.BAD_REQUEST); + } + + const userRole = await this.organisationMembersRole.find({ + where: { userId: currentUserId, roleId: adminRole.id }, + }); + + if (!userRole.length) { + throw new CustomHttpException('Access denied', HttpStatus.FORBIDDEN); + } return true; } } diff --git a/src/health.controller.ts b/src/health.controller.ts index 90c3676e3..7b7ad9460 100644 --- a/src/health.controller.ts +++ b/src/health.controller.ts @@ -1,5 +1,6 @@ import { Controller, Get } from '@nestjs/common'; import { skipAuth } from './helpers/skipAuth'; +import * as os from 'os'; @Controller() export default class HealthController { @@ -24,6 +25,22 @@ export default class HealthController { @skipAuth() @Get('health') public health() { - return { status_code: 200, message: 'This is a healthy endpoint' }; + const networkInterfaces = os.networkInterfaces(); + let localIpAddress = 'Not available'; + + // Iterate over network interfaces to find the first non-internal IPv4 address + for (const interfaceKey in networkInterfaces) { + const interfaceDetails = networkInterfaces[interfaceKey]; + for (const detail of interfaceDetails) { + if (detail.family === 'IPv4' && !detail.internal) { + localIpAddress = detail.address; + break; + } + } + if (localIpAddress !== 'Not available') break; + } + + return { status_code: 200, message: 'This is a healthy endpoint', ip: localIpAddress }; } } + diff --git a/src/helpers/SystemMessages.ts b/src/helpers/SystemMessages.ts index 533594739..64b9f74fd 100644 --- a/src/helpers/SystemMessages.ts +++ b/src/helpers/SystemMessages.ts @@ -6,9 +6,12 @@ export const USER_ACCOUNT_EXIST = 'Account with the specified email exists'; export const USER_ACCOUNT_DOES_NOT_EXIST = "Account with the specified email doesn't exist"; export const UNAUTHENTICATED_MESSAGE = 'User is currently unauthorized, kindly authenticate to continue'; export const TWO_FACTOR_VERIFIED_SUCCESSFULLY = '2FA verified and enabled'; +export const ANALYTICS_FETCHED_SUCCESSFULLY = 'Admin Analytics fetched successfully'; +export const DASHBOARD_FETCHED_SUCCESSFULLY = 'Admin Dashboard retrieved successfully'; export const INCORRECT_TOTP_CODE = 'Incorrect totp code'; export const USER_NOT_ENABLED_2FA = 'Two factor Auth not initiated. Visit api/auth/2fa/enable'; export const USER_NOT_FOUND = 'User not found!'; +export const TOTAL_PRODUCTS_FETCHED_SUCCESSFULLY = 'Total Products fetched successfully'; export const INVALID_PASSWORD = 'Invalid password'; export const TWO_FA_INITIATED = '2FA setup initiated'; export const TWO_FA_ENABLED = '2FA is already enabled'; @@ -16,6 +19,7 @@ export const BAD_REQUEST = 'Bad Request'; export const LANGUAGE_CREATED_SUCCESSFULLY = 'Language Created Successfully'; export const OK = 'Success'; export const LANGUAGE_ALREADY_EXISTS = 'Language already exits'; +export const WORK_IN_PROGRESS = 'Work in progress'; export const FETCH_LANGUAGE_FAILURE = 'Failed to fetch language'; export const UNAUTHORISED_TOKEN = 'Invalid token or email'; export const TIMEZONE_CREATED_SUCCESSFULLY = 'Timezone created Successfully'; @@ -27,6 +31,7 @@ export const LOGIN_SUCCESSFUL = 'Login successful'; export const LOGIN_ERROR = 'An error occurred during login'; export const EMAIL_SENT = 'Email sent successfully'; export const ENABLE_2FA_ERROR = 'Error occured enabling 2fa'; +export const ALREADY_ENABLED_2FA = '2FA already enabled on your account'; export const SIGN_IN_OTP_SENT = 'Sign-in token sent to email'; export const WRONG_PARAMETERS = 'permission_list must be an object with keys from PermissionCategory and boolean values'; @@ -38,3 +43,71 @@ export const ORG_NOT_FOUND = 'Organisation not found'; export const NOT_ORG_OWNER = 'You do not have permission to update this organisation'; export const PASSWORD_UPDATED = 'Password updated successfully'; export const REQUEST_SUCCESSFUL = 'Request completed successfully'; +export const PAYMENT_NOTFOUND = 'Payment plan not found'; +export const PRODUCT_NOT_FOUND = 'Product not found!'; +export const COMMENT_CREATED = 'Comment added successfully'; +export const ORG_UPDATE = 'Organisation updated successfully'; +export const ORG_MEMBER_NOT_FOUND = 'Member not found'; +export const ORG_MEMBER_DOES_NOT_BELONG = 'Member does not belong to the specified organisation'; +export const ROLE_NOT_FOUND = 'Role not found in the organization'; +export const BLOG_FETCHED_SUCCESSFUL = 'Blog fetched successfully'; +export const BLOG_NOT_FOUND = 'Blog not found'; +export const JOB_NOT_FOUND = 'Job not found'; +export const JOB_DELETION_SUCCESSFUL = 'Job details deleted successfully'; +export const JOB_LISTING_RETRIEVAL_SUCCESSFUL = 'Jobs listing fetched successfully'; +export const JOB_CREATION_SUCCESSFUL = 'Job listing created successfully'; +export const NO_USER_ORGS = 'User has no organisations'; +export const DEADLINE_PASSED = 'Job application deadline passed'; +export const EMAIL_TEMPLATES = { + TEMPLATE_UPDATED_SUCCESSFULLY: 'Template updated successfully', + INVALID_HTML_FORMAT: 'Invalid HTML format', + TEMPLATE_NOT_FOUND: 'Template not found', +}; +export const EXISTING_ROLE = 'A role with this name already exists in the organisation'; +export const ROLE_CREATION_FAILED = 'Failed to create organisation role'; +export const ROLE_FETCHED_SUCCESSFULLY = 'Roles fetched successfully'; +export const ROLE_CREATED_SUCCESSFULLY = 'Role created successfully'; + +export const RESOURCE_NOT_FOUND = resource => { + return `${resource} does not exist`; +}; +export const INVALID_UUID_FORMAT = 'Invalid UUID format'; +export const MEMBER_ALREADY_EXISTS = 'User already added to organization'; +export const MEMBER_ALREADY_SUCCESSFULLY = 'Member added successfully'; +export const MEMBER_NOT_ADDED = 'Failed to add member to the organisation'; +export const USER_NOT_REGISTERED = 'User not found, register to continue'; +export const CATEGORY_NOT_FOUND = 'Organization category not found'; +export const INVITE_ACCEPTED = 'Invite already accepted'; +export const INVALID_INVITE = 'Invalid invite link'; +export const INVITE_NOT_FOUND = 'Invite link not found'; +export const BLOG_DELETED = 'Blog post has been successfully deleted'; +export const NO_USER_TESTIMONIALS = 'User has no testimonials'; +export const USER_TESTIMONIALS_FETCHED = 'User testimonials retrieved successfully'; +export const INVALID_ORG_ID = 'Provide a valid organization Id'; +export const INVALID_USER_ID = 'Provide a valid user Id'; +export const INVALID_PRODUCT_ID = 'Provide a valid user Id'; +export const REVENUE_FETCHED_SUCCESSFULLY = 'Revenue Fetched'; +export const QUESTION_ALREADY_EXISTS = 'This question already exists.'; +export const ROLE_ALREADY_EXISTS = 'A role with this name already exists in the organisation'; +export const NO_FILE_FOUND = 'No file uploaded.'; +export const PROFILE_NOT_FOUND = 'Profile not found'; +export const PROFILE_PIC_ERROR = 'Error deleting previous profile picture:'; +export const PROFILE_PIC_NOT_FOUND = 'Previous profile picture pic not found'; +export const ERROR_DIRECTORY = 'Error creating uploads directory:'; +export const DIRECTORY_CREATED = 'Uploads directory created at:'; +export const PICTURE_UPDATED = 'Profile picture updated successfully'; +export const FILE_SAVE_ERROR = 'Error saving file to disk'; +export const FILE_EXCEEDS_SIZE = resource => { + return `File size exceeds ${resource} MB limit`; +}; +export const INVALID_FILE_TYPE = resource => { + return `Invalid file type. Allowed types: ${resource}`; +}; +export const INQUIRY_SENT = 'Inquiry sent successfully'; +export const BILLING_PLAN_ALREADY_EXISTS = 'Billing plan already exists'; +export const BILLING_PLAN_CREATED = 'Billing plan successfully created'; +export const USER_ALREADY_WAITLISTED = 'User already on the waitlist'; +export const TOPIC_NOT_FOUND = `Help center topic with ID not found`; +export const TOPIC_UPDATE_SUCCESS = 'Topic updated successfully'; +export const TOPIC_DELETED = 'Topic deleted successfully'; +export const BILLING_PLAN_NOT_FOUND = 'Billing plan not found'; diff --git a/src/helpers/app-constants.ts b/src/helpers/app-constants.ts new file mode 100644 index 000000000..659946f63 --- /dev/null +++ b/src/helpers/app-constants.ts @@ -0,0 +1,6 @@ +import * as path from 'path'; + +export const MAX_PROFILE_PICTURE_SIZE = 2 * 1024 * 1024; +export const VALID_UPLOADS_MIME_TYPES = ['image/jpeg', 'image/png']; +export const BASE_URL = "https://staging.api-nestjs.boilerplate.hng.tech"; +export const PROFILE_PHOTO_UPLOADS = path.join(__dirname, '..', 'uploads') \ No newline at end of file diff --git a/src/helpers/contactHelper.ts b/src/helpers/contactHelper.ts new file mode 100644 index 000000000..159fc24ae --- /dev/null +++ b/src/helpers/contactHelper.ts @@ -0,0 +1,2 @@ +export const COMPANYEMAIL = 'amal_salam@yahoo.com'; +export const SUBJECT = 'New Contact Inquiry'; diff --git a/src/helpers/custom-http-filter.ts b/src/helpers/custom-http-filter.ts index 53eb4f750..5969d180c 100644 --- a/src/helpers/custom-http-filter.ts +++ b/src/helpers/custom-http-filter.ts @@ -7,19 +7,19 @@ export class CustomHttpException extends HttpException { getResponse(): any { const response = super.getResponse(); - const status = this.getStatus(); + const status_code = this.getStatus(); if (typeof response === 'object' && response !== null) { const res = response as Record; return { message: res.message || 'An error occurred', - status, + status_code, }; } return { message: response, - status, + status_code, }; } } diff --git a/src/modules/email/email_storage.service.ts b/src/helpers/fileHelpers.ts similarity index 63% rename from src/modules/email/email_storage.service.ts rename to src/helpers/fileHelpers.ts index bf368c0d9..b05f884ff 100644 --- a/src/modules/email/email_storage.service.ts +++ b/src/helpers/fileHelpers.ts @@ -1,34 +1,25 @@ import * as fs from 'fs'; import { promisify } from 'util'; -/** Check if a file exists at a given path.*/ export const checkIfFileOrDirectoryExists = (path: string): boolean => { return fs.existsSync(path); }; -/** Gets file data from a given path via a promise interface.*/ export const getFile = async (path: string, encoding?: BufferEncoding): Promise => { const readFile = promisify(fs.readFile); - return encoding ? readFile(path, { encoding }) : readFile(path); }; -/** Writes a file at a given path via a promise interface */ export const createFile = async (path: string, fileName: string, data: string): Promise => { if (!checkIfFileOrDirectoryExists(path)) { - fs.mkdirSync(path); + fs.mkdirSync(path, { recursive: true }); } - console.log(`Creating file at ${path}/${fileName}`); - const writeFile = promisify(fs.writeFile); - - return await writeFile(`${path}/${fileName}`, data, 'utf8'); + await writeFile(`${path}/${fileName}`, data, 'utf8'); }; -/**Delete file at the given path via a promise interface*/ export const deleteFile = async (path: string): Promise => { const unlink = promisify(fs.unlink); - - return await unlink(path); + await unlink(path); }; diff --git a/src/helpers/http-exception-filter.ts b/src/helpers/http-exception-filter.ts new file mode 100644 index 000000000..d66931a20 --- /dev/null +++ b/src/helpers/http-exception-filter.ts @@ -0,0 +1,43 @@ +import { + ExceptionFilter, + Catch, + ArgumentsHost, + HttpException, + HttpStatus, + InternalServerErrorException, +} from '@nestjs/common'; +import { Response } from 'express'; + +export interface ExceptionResponse { + statusCode: number; + message: string | string[]; + error: string; +} + +@Catch() +export class HttpExceptionFilter implements ExceptionFilter { + catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; + + const exceptionResponse = + exception instanceof HttpException + ? exception.getResponse() + : { + message: (exception as InternalServerErrorException).message || 'Internal server error', + error: 'Internal Server Error', + }; + + const errorMessage = + typeof exceptionResponse === 'string' ? exceptionResponse : (exceptionResponse as ExceptionResponse).message; + + const error = typeof exceptionResponse === 'string' ? '' : (exceptionResponse as ExceptionResponse).error; + + response.status(status).json({ + status, + error: error, + message: errorMessage, + }); + } +} diff --git a/src/modules/api-status/api-status.controller.ts b/src/modules/api-status/api-status.controller.ts new file mode 100644 index 000000000..8fca699b0 --- /dev/null +++ b/src/modules/api-status/api-status.controller.ts @@ -0,0 +1,21 @@ +import { Controller, Get, Post, Body } from '@nestjs/common'; +import { ApiStatusService } from './api-status.service'; +import { CreateApiStatusDto } from './dto/create-api-status.dto'; +import { skipAuth } from '../../helpers/skipAuth'; + +@Controller('api-status') +export class ApiStatusController { + constructor(private readonly apiStatusService: ApiStatusService) {} + + @skipAuth() + @Post() + create(@Body() createApiStatusDto: CreateApiStatusDto[]) { + return this.apiStatusService.create(createApiStatusDto); + } + + @skipAuth() + @Get() + findAll() { + return this.apiStatusService.findAll(); + } +} diff --git a/src/modules/api-status/api-status.module.ts b/src/modules/api-status/api-status.module.ts new file mode 100644 index 000000000..a6512e2dc --- /dev/null +++ b/src/modules/api-status/api-status.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { ApiStatusService } from './api-status.service'; +import { ApiStatusController } from './api-status.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ApiHealth } from './entities/api-status.entity'; +import { Request } from './entities/request.entity'; + +@Module({ + controllers: [ApiStatusController], + imports: [TypeOrmModule.forFeature([ApiHealth, Request])], + providers: [ApiStatusService], +}) +export class ApiStatusModule {} diff --git a/src/modules/api-status/api-status.service.ts b/src/modules/api-status/api-status.service.ts new file mode 100644 index 000000000..e125a4676 --- /dev/null +++ b/src/modules/api-status/api-status.service.ts @@ -0,0 +1,86 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { CreateApiStatusDto } from './dto/create-api-status.dto'; +import { ApiHealth, ApiStatus } from './entities/api-status.entity'; +import { Request } from './entities/request.entity'; + +@Injectable() +export class ApiStatusService { + constructor( + @InjectRepository(ApiHealth) + private readonly apiHealthRepository: Repository, + @InjectRepository(Request) + private readonly requestRepository: Repository + ) {} + + async create(apiHealthDto: CreateApiStatusDto[]) { + const apiHealthList = []; + + for (const eachApiStatus of apiHealthDto) { + const apiHealth = await this.apiHealthRepository.findOne({ + where: { api_group: eachApiStatus.api_group }, + }); + + if (!apiHealth) { + const apiRequestList = []; + const savedApiHealth = await this.apiHealthRepository.save(eachApiStatus); + await Promise.all( + eachApiStatus.requests.map(async request => { + const savedRequest = await this.requestRepository.save({ + ...request, + api_health: savedApiHealth, + updated_at: new Date(), + }); + apiRequestList.push(savedRequest); + return savedRequest; + }) + ); + + const savedHealth = await this.apiHealthRepository.findOne({ + where: { id: apiHealth.id }, + }); + + savedHealth.requests = apiRequestList; + await this.apiHealthRepository.save(savedHealth); + + apiHealthList.push(apiHealth); + } else { + const apiRequestList = []; + + await this.requestRepository.clear(); + + await Promise.all( + eachApiStatus.requests.map(async request => { + const savedRequest = await this.requestRepository.save(request); + apiRequestList.push(savedRequest); + return savedRequest; + }) + ); + + Object.assign(apiHealth, ApiStatus); + apiHealth.requests = apiRequestList; + apiHealth.updated_at = new Date(); + apiHealth.lastChecked = new Date(); + await this.apiHealthRepository.save(apiHealth); + apiHealthList.push(apiHealth); + } + } + + return { + message: `Status Added Successfully`, + data: apiHealthList, + }; + } + + async findAll() { + const apiHealthData = await this.apiHealthRepository.find({ + relations: ['requests'], + }); + + return { + message: `Health Status Retrieved Successfully`, + data: apiHealthData, + }; + } +} diff --git a/src/modules/api-status/dto/create-api-status.dto.ts b/src/modules/api-status/dto/create-api-status.dto.ts new file mode 100644 index 000000000..f91cfa58c --- /dev/null +++ b/src/modules/api-status/dto/create-api-status.dto.ts @@ -0,0 +1,19 @@ +import { IsString, IsArray, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; +import { CreateRequestDto } from './create-request.dto'; + +export class CreateApiStatusDto { + @IsString() + api_group: string; + + @IsString() + status: string; + + @IsString() + details: string; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CreateRequestDto) + requests: CreateRequestDto[]; +} diff --git a/src/modules/api-status/dto/create-request.dto.ts b/src/modules/api-status/dto/create-request.dto.ts new file mode 100644 index 000000000..32ada4ce3 --- /dev/null +++ b/src/modules/api-status/dto/create-request.dto.ts @@ -0,0 +1,22 @@ +import { IsString, IsOptional, IsArray, IsInt } from 'class-validator'; + +export class CreateRequestDto { + @IsString() + requestName: string; + + @IsString() + requestUrl: string; + + @IsOptional() + @IsString() + status?: string; + + @IsInt() + responseTime: number; + + @IsInt() + statusCode: number; + + @IsArray() + errors: string[]; +} diff --git a/src/modules/api-status/entities/api-status.entity.ts b/src/modules/api-status/entities/api-status.entity.ts new file mode 100644 index 000000000..9d087d1ee --- /dev/null +++ b/src/modules/api-status/entities/api-status.entity.ts @@ -0,0 +1,31 @@ +import { AbstractBaseEntity } from '../../../entities/base.entity'; +import { Entity, Column, OneToMany } from 'typeorm'; +import { Request } from './request.entity'; + +export enum ApiStatus { + OPERATIONAL = 'operational', + DEGRADED = 'degraded', + DOWN = 'down', +} + +@Entity('api_health') +export class ApiHealth extends AbstractBaseEntity { + @Column() + api_group: string; + + @Column({ + type: 'enum', + enum: ApiStatus, + default: ApiStatus.OPERATIONAL, + }) + status: string; + + @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) + lastChecked: Date; + + @Column() + details: string; + + @OneToMany(() => Request, request => request.api_health) + requests: Request[]; +} diff --git a/src/modules/api-status/entities/request.entity.ts b/src/modules/api-status/entities/request.entity.ts new file mode 100644 index 000000000..b17336f7c --- /dev/null +++ b/src/modules/api-status/entities/request.entity.ts @@ -0,0 +1,27 @@ +import { AbstractBaseEntity } from '../../../entities/base.entity'; +import { Entity, Column, ManyToOne } from 'typeorm'; +import { ApiHealth } from './api-status.entity'; + +@Entity('request') +export class Request extends AbstractBaseEntity { + @Column() + requestName: string; + + @Column({ nullable: true }) + status: string; + + @Column() + requestUrl: string; + + @Column() + responseTime: number; + + @Column() + statusCode: number; + + @Column('simple-array', { nullable: true }) + errors: string[]; + + @ManyToOne(() => ApiHealth, apiHealth => apiHealth.requests) + api_health: ApiHealth; +} diff --git a/src/modules/api-status/tests/api-status.service.spec.ts b/src/modules/api-status/tests/api-status.service.spec.ts new file mode 100644 index 000000000..6342a8d03 --- /dev/null +++ b/src/modules/api-status/tests/api-status.service.spec.ts @@ -0,0 +1,131 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ApiStatusService } from '../api-status.service'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { ApiHealth } from '../entities/api-status.entity'; +import { Repository } from 'typeorm'; +import { CreateApiStatusDto } from '../dto/create-api-status.dto'; +import { Request } from '../entities/request.entity'; + +describe('ApiStatusService', () => { + let service: ApiStatusService; + let apiHealthRepository: Repository; + let requestRepository: Repository; + + const mockApiHealthRepository = () => ({ + findOne: jest.fn(), + save: jest.fn(), + find: jest.fn(), + }); + + const mockRequestRepository = () => ({ + save: jest.fn(), + clear: jest.fn(), + delete: jest.fn(), + }); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ApiStatusService, + { + provide: getRepositoryToken(ApiHealth), + useValue: mockApiHealthRepository(), + }, + { + provide: getRepositoryToken(Request), + useValue: mockRequestRepository(), + }, + ], + }).compile(); + + service = module.get(ApiStatusService); + apiHealthRepository = module.get>(getRepositoryToken(ApiHealth)); + requestRepository = module.get>(getRepositoryToken(Request)); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('create', () => { + it('should create new ApiHealth and Requests', async () => { + const createApiStatusDto: CreateApiStatusDto[] = [ + { + api_group: 'Blogs API 2', + status: 'operational', + details: 'All tests passed', + requests: [], + }, + ]; + + const savedApiHealth = new ApiHealth(); + savedApiHealth.requests = []; + + const savedRequest: Request = { + id: '1', + requestName: 'Blog', + requestUrl: '/api/v1/blog', + responseTime: 2000, + errors: ['Hello'], + status: 'Bad Request', + statusCode: 400, + created_at: new Date(), + updated_at: new Date(), + api_health: savedApiHealth, + }; + + jest.spyOn(apiHealthRepository, 'findOne').mockResolvedValue(savedApiHealth); + jest.spyOn(apiHealthRepository, 'save').mockResolvedValue(savedApiHealth); + jest.spyOn(requestRepository, 'save').mockResolvedValue(savedRequest); + + const result = await service.create(createApiStatusDto); + + expect(apiHealthRepository.findOne).toHaveBeenCalled(); + expect(apiHealthRepository.save).toHaveBeenCalled(); + expect(result).toEqual({ + message: 'Status Added Successfully', + data: [savedApiHealth], + }); + }); + + describe('create', () => { + describe('findAll', () => { + it('should return all ApiHealth records with their requests', async () => { + const apiHealthData: ApiHealth[] = [ + { + id: '1', + api_group: 'Test', + status: 'operational', + details: 'All Tests passed', + requests: [], + created_at: new Date(), + updated_at: new Date(), + lastChecked: new Date(), + }, + { + id: '2', + api_group: 'Test', + status: 'operational', + details: 'All Tests passed', + requests: [], + created_at: new Date(), + updated_at: new Date(), + lastChecked: new Date(), + }, + ]; + + jest.spyOn(apiHealthRepository, 'find').mockResolvedValue(apiHealthData); + + const result = await service.findAll(); + + expect(apiHealthRepository.find).toHaveBeenCalledWith({ + relations: ['requests'], + }); + expect(result).toEqual({ + message: 'Health Status Retrieved Successfully', + data: apiHealthData, + }); + }); + }); + }); + }); +}); diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 8493848bb..f57a3294c 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -1,6 +1,27 @@ -import { ApiBearerAuth, ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { + ApiBadRequestResponse, + ApiBearerAuth, + ApiBody, + ApiOperation, + ApiResponse, + ApiTags, + ApiUnauthorizedResponse, +} from '@nestjs/swagger'; import * as SYS_MSG from '../../helpers/SystemMessages'; -import { Body, Controller, HttpCode, HttpStatus, Post, Req, Request, Res, UseGuards, Get, Patch } from '@nestjs/common'; +import { + Body, + Controller, + HttpCode, + HttpStatus, + Post, + Req, + Request, + Res, + UseGuards, + Get, + Patch, + Query, +} from '@nestjs/common'; import { CreateUserDTO } from './dto/create-user.dto'; import { skipAuth } from '../../helpers/skipAuth'; import AuthenticationService from './auth.service'; @@ -18,7 +39,13 @@ import { SuccessCreateUserResponse, } from '../user/dto/user-response.dto'; import { ChangePasswordDto } from './dto/change-password.dto'; +import { AuthResponseDto } from './dto/auth-response.dto'; +import { GoogleAuthPayloadDto } from './dto/google-auth.dto'; +import { GenericAuthResponseDto } from './dto/generic-reponse.dto'; import { UpdatePasswordDto } from './dto/updatePasswordDto'; +import { LoginErrorResponseDto } from './dto/login-error-dto'; +import { UpdateUserPasswordResponseDTO } from './dto/update-user-password.dto'; +import { CustomHttpException } from '../../helpers/custom-http-filter'; @ApiTags('Authentication') @Controller('auth') @@ -28,41 +55,29 @@ export default class RegistrationController { @skipAuth() @ApiOperation({ summary: 'User Registration' }) @ApiResponse({ status: 201, description: 'Register a new user', type: SuccessCreateUserResponse }) - @ApiResponse({ status: 400, description: 'Bad request', type: ErrorCreateUserResponse }) + @ApiResponse({ status: 400, description: 'User already exists', type: ErrorCreateUserResponse }) @Post('register') @HttpCode(201) public async register(@Body() body: CreateUserDTO): Promise { return this.authService.createNewUser(body); } - @ApiBearerAuth() - @ApiOperation({ summary: 'Verify two factor authentication code' }) - @ApiBody({ - description: 'Enable two factor authentication', - type: Verify2FADto, - }) - @ApiResponse({ - status: HttpStatus.OK, - description: SYS_MSG.TWO_FACTOR_VERIFIED_SUCCESSFULLY, - }) - @ApiResponse({ - status: HttpStatus.BAD_REQUEST, - description: SYS_MSG.INCORRECT_TOTP_CODE, - }) @Post('2fa/verify') verify2fa(@Body() verify2faDto: Verify2FADto, @Req() req) { return this.authService.verify2fa(verify2faDto, req.user.sub); } + @skipAuth() + @HttpCode(200) + @Post('forgot-password') + @ApiBody({ type: ForgotPasswordDto }) @ApiOperation({ summary: 'Generate forgot password reset token' }) @ApiResponse({ status: 200, description: 'The forgot password reset token generated successfully', type: ForgotPasswordResponseDto, }) - @skipAuth() - @HttpCode(200) - @Post('forgot-password') + @ApiBadRequestResponse({ description: SYS_MSG.USER_ACCOUNT_DOES_NOT_EXIST }) async forgotPassword(@Body() forgotPasswordDto: ForgotPasswordDto): Promise { return this.authService.forgotPassword(forgotPasswordDto); } @@ -70,17 +85,19 @@ export default class RegistrationController { @skipAuth() @Post('login') @ApiOperation({ summary: 'Login a user' }) + @ApiBody({ type: LoginDto }) @ApiResponse({ status: 200, description: 'Login successful', type: LoginResponseDto }) - @ApiResponse({ status: 401, description: 'Unauthorized' }) + @ApiUnauthorizedResponse({ description: 'Invalid credentials', type: LoginErrorResponseDto }) @HttpCode(200) async login(@Body() loginDto: LoginDto): Promise { return this.authService.loginUser(loginDto); } @skipAuth() - @ApiOperation({ summary: 'Email verification' }) - @ApiResponse({ status: 200, description: 'Verify token sent to the user mail', type: OtpDto }) - @ApiResponse({ status: 400, description: 'Bad request' }) + @ApiOperation({ summary: 'Verify magic link otp' }) + @ApiBody({ type: AuthResponseDto }) + @ApiResponse({ status: 200, description: 'successfully verifies otp and logs in user' }) + @ApiResponse({ status: 401, description: SYS_MSG.UNAUTHORISED_TOKEN }) @HttpCode(200) @Post('verify-otp') public async verifyEmail(@Body() body: OtpDto): Promise { @@ -88,13 +105,6 @@ export default class RegistrationController { } @Post('2fa/enable') - @ApiBody({ - description: 'Enable two factor authentication', - type: Enable2FADto, - }) - @ApiBearerAuth() - @ApiResponse({ status: 200, description: SYS_MSG.TWO_FA_INITIATED }) - @ApiResponse({ status: 400, description: SYS_MSG.BAD_REQUEST }) @HttpCode(200) public async enable2FA(@Body() body: Enable2FADto, @Req() request: Request): Promise { const { password } = body; @@ -103,10 +113,11 @@ export default class RegistrationController { } @skipAuth() - @ApiOperation({ summary: 'Google Authentication' }) - @ApiResponse({ status: 200, description: 'Verify Payload sent from google', type: OtpDto }) - @ApiResponse({ status: 400, description: 'Bad request' }) @Post('google') + @ApiOperation({ summary: 'Google Authentication' }) + @ApiBody({ type: GoogleAuthPayloadDto }) + @ApiResponse({ status: 200, description: 'Verify Payload sent from google', type: AuthResponseDto }) + @ApiBadRequestResponse({ description: 'Google authentication failed' }) @HttpCode(200) async googleAuth(@Body() body: GoogleAuthPayload) { return this.authService.googleAuth(body); @@ -118,8 +129,8 @@ export default class RegistrationController { type: RequestVerificationToken, }) @ApiOperation({ summary: 'Request Verification Token' }) - @ApiResponse({ status: 200, description: 'Verification Token sent to mail' }) - @ApiResponse({ status: 400, description: 'Bad request' }) + @ApiResponse({ status: 200, description: 'Verification Token sent to mail', type: GenericAuthResponseDto }) + @ApiResponse({ status: 400, description: 'Bad request', type: CustomHttpException }) @HttpCode(200) @Post('request/token') async requestVerificationToken(@Body() body: { email: string }) { @@ -131,13 +142,15 @@ export default class RegistrationController { @Post('magic-link') @HttpCode(200) @ApiOperation({ summary: 'Request Signin Token' }) - @ApiResponse({ status: 200, description: 'Sign-in token sent to email', type: RequestSigninTokenDto }) + @ApiBody({ type: RequestSigninTokenDto }) + @ApiResponse({ status: 200, description: 'Sign-in token sent to email', type: GenericAuthResponseDto }) @ApiResponse({ status: 400, description: 'Bad request' }) public async signInToken(@Body() body: RequestSigninTokenDto) { return await this.authService.requestSignInToken(body); } @ApiBearerAuth() + @ApiBody({ type: ChangePasswordDto }) @ApiOperation({ summary: 'Change user password' }) @ApiResponse({ status: 200, description: 'Password changed successfully' }) @ApiResponse({ status: 400, description: 'Bad request' }) @@ -154,6 +167,7 @@ export default class RegistrationController { @Post('magic-link/verify') @HttpCode(200) @ApiOperation({ summary: 'Verify Signin Token' }) + @ApiBody({ type: OtpDto }) @ApiResponse({ status: 200, description: 'Sign-in successful', type: OtpDto }) @ApiResponse({ status: 401, description: 'Unauthorized' }) public async verifySignInToken(@Body() body: OtpDto) { @@ -163,7 +177,8 @@ export default class RegistrationController { @skipAuth() @ApiBearerAuth() @ApiOperation({ summary: 'Verify Otp and change user password' }) - @ApiResponse({ status: 200, description: 'Password changed successfully' }) + @ApiBody({ type: UpdatePasswordDto }) + @ApiResponse({ status: 200, description: 'Password changed successfully', type: UpdateUserPasswordResponseDTO }) @ApiResponse({ status: 400, description: 'Bad request' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) @Patch('password-reset') diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 6990af5b1..ed8cb3972 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -13,9 +13,12 @@ import { EmailModule } from '../email/email.module'; import { OtpService } from '../otp/otp.service'; import { EmailService } from '../email/email.service'; import { Otp } from '../otp/entities/otp.entity'; -import { GoogleStrategy } from './strategies/google.strategy'; -import { GoogleAuthService } from './google-auth.service'; import { Profile } from '../profile/entities/profile.entity'; +import { OrganisationsService } from '../organisations/organisations.service'; +import { Organisation } from '../organisations/entities/organisations.entity'; +import { OrganisationUserRole } from '../role/entities/organisation-user-role.entity'; +import { Role } from '../role/entities/role.entity'; +import { ProfileService } from '../profile/profile.service'; @Module({ controllers: [RegistrationController], @@ -25,11 +28,11 @@ import { Profile } from '../profile/entities/profile.entity'; UserService, OtpService, EmailService, - GoogleStrategy, - GoogleAuthService, + OrganisationsService, + ProfileService, ], imports: [ - TypeOrmModule.forFeature([User, Otp, Profile]), + TypeOrmModule.forFeature([User, Otp, Profile, Organisation, OrganisationUserRole, Role]), PassportModule, OtpModule, EmailModule, diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 38cee7a5f..16303230a 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -1,5 +1,5 @@ import { HttpStatus, Injectable, InternalServerErrorException } from '@nestjs/common'; -import * as bcrypt from 'bcryptjs'; +import * as bcrypt from 'bcrypt'; import * as speakeasy from 'speakeasy'; import * as SYS_MSG from '../../helpers/SystemMessages'; import { JwtService } from '@nestjs/jwt'; @@ -13,12 +13,13 @@ import { ForgotPasswordDto } from './dto/forgot-password.dto'; import { LoginDto } from './dto/login.dto'; import { RequestSigninTokenDto } from './dto/request-signin-token.dto'; import { OtpDto } from '../otp/dto/otp.dto'; -import { GoogleAuthService } from './google-auth.service'; import GoogleAuthPayload from './interfaces/GoogleAuthPayloadInterface'; -import { GoogleVerificationPayloadInterface } from './interfaces/GoogleVerificationPayloadInterface'; -import { SendEmailDto } from '../email/dto/email.dto'; import { CustomHttpException } from '../../helpers/custom-http-filter'; import { UpdatePasswordDto } from './dto/updatePasswordDto'; +import { TokenPayload } from 'google-auth-library'; +import { OrganisationsService } from '../organisations/organisations.service'; +import { ProfileService } from '../profile/profile.service'; +import { UpdateProfileDto } from '../profile/dto/update-profile.dto'; @Injectable() export default class AuthenticationService { @@ -27,12 +28,13 @@ export default class AuthenticationService { private jwtService: JwtService, private otpService: OtpService, private emailService: EmailService, - private googleAuthService: GoogleAuthService + private organisationService: OrganisationsService, + private profileService: ProfileService ) {} - async createNewUser(creatUserDto: CreateUserDTO) { + async createNewUser(createUserDto: CreateUserDTO) { const userExists = await this.userService.getUserRecord({ - identifier: creatUserDto.email, + identifier: createUserDto.email, identifierType: 'email', }); @@ -40,17 +42,35 @@ export default class AuthenticationService { throw new CustomHttpException(SYS_MSG.USER_ACCOUNT_EXIST, HttpStatus.BAD_REQUEST); } - await this.userService.createUser(creatUserDto); + await this.userService.createUser(createUserDto); - const user = await this.userService.getUserRecord({ identifier: creatUserDto.email, identifierType: 'email' }); + const user = await this.userService.getUserRecord({ identifier: createUserDto.email, identifierType: 'email' }); if (!user) { throw new CustomHttpException(SYS_MSG.FAILED_TO_CREATE_USER, HttpStatus.BAD_REQUEST); } + const newOrganisationPayload = { + name: `${user.first_name}'s Organisation`, + description: '', + email: user.email, + industry: '', + type: '', + country: '', + address: '', + state: '', + }; - await this.otpService.createOtp(user.id); + const newOrganisation = await this.organisationService.create(newOrganisationPayload, user.id); - const access_token = this.jwtService.sign({ id: user.id, sub: user.id, email: user.email }); + const userOranisations = await this.organisationService.getAllUserOrganisations(user.id); + const isSuperAdmin = userOranisations.map(instance => instance.user_role).includes('super-admin'); + const token = (await this.otpService.createOtp(user.id)).token; + + const access_token = this.jwtService.sign({ + id: user.id, + sub: user.id, + email: user.email, + }); const responsePayload = { user: { @@ -59,8 +79,9 @@ export default class AuthenticationService { last_name: user.last_name, email: user.email, avatar_url: user.profile.profile_pic_url, - role: user.user_type, + is_superadmin: isSuperAdmin, }, + oranisations: userOranisations, }; return { @@ -77,12 +98,7 @@ export default class AuthenticationService { } const token = (await this.otpService.createOtp(user.id)).token; - const emailData = new SendEmailDto(); - emailData.to = dto.email; - emailData.subject = 'Reset Password'; - emailData.template = 'reset-password'; - emailData.context = { link: `${process.env.BASE_URL}/auth/reset-password`, email: dto.email, token: token }; - await this.emailService.sendEmail(emailData); + await this.emailService.sendForgotPasswordMail(user.email, `${process.env.FRONTEND_URL}/reset-password`, token); return { message: SYS_MSG.EMAIL_SENT, @@ -97,7 +113,7 @@ export default class AuthenticationService { const user = await this.otpService.retrieveUserAndOtp(exists.id, otp); - return this.userService.updateUser(user.id, { password: newPassword }, user); + // return this.userService.updateUser(user.id, { password: newPassword }, user); } async changePassword(user_id: string, oldPassword: string, newPassword: string) { @@ -145,9 +161,9 @@ export default class AuthenticationService { if (!isMatch) { throw new CustomHttpException(SYS_MSG.INVALID_CREDENTIALS, HttpStatus.UNAUTHORIZED); } - + const userOranisations = await this.organisationService.getAllUserOrganisations(user.id); const access_token = this.jwtService.sign({ id: user.id, sub: user.id }); - + const isSuperAdmin = userOranisations.map(instance => instance.user_role).includes('super-admin'); const responsePayload = { access_token, data: { @@ -156,9 +172,10 @@ export default class AuthenticationService { first_name: user.first_name, last_name: user.last_name, email: user.email, - role: user.user_type, avatar_url: user.profile && user.profile.profile_pic_url ? user.profile.profile_pic_url : null, + is_superadmin: isSuperAdmin, }, + organisations: userOranisations, }, }; @@ -190,6 +207,10 @@ export default class AuthenticationService { throw new InternalServerErrorException(SYS_MSG.ENABLE_2FA_ERROR); } + if (user.is_2fa_enabled) { + throw new CustomHttpException(SYS_MSG.ALREADY_ENABLED_2FA, HttpStatus.BAD_REQUEST); + } + const secret = speakeasy.generateSecret({ length: 32 }); const payload = { secret: secret.base32, @@ -265,7 +286,21 @@ export default class AuthenticationService { async googleAuth(googleAuthPayload: GoogleAuthPayload) { const idToken = googleAuthPayload.id_token; - const verifyTokenResponse: GoogleVerificationPayloadInterface = await this.googleAuthService.verifyToken(idToken); + + if (!idToken) { + throw new CustomHttpException(SYS_MSG.INVALID_CREDENTIALS, HttpStatus.UNAUTHORIZED); + } + + const request = await fetch(`https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=${idToken}`); + + if (request.status === 400) { + throw new CustomHttpException(SYS_MSG.INVALID_CREDENTIALS, HttpStatus.UNAUTHORIZED); + } + if (request.status === 500) { + throw new CustomHttpException(SYS_MSG.SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR); + } + const verifyTokenResponse: TokenPayload = await request.json(); + const userEmail = verifyTokenResponse.email; const userExists = await this.userService.getUserRecord({ identifier: userEmail, identifierType: 'email' }); @@ -275,9 +310,13 @@ export default class AuthenticationService { first_name: verifyTokenResponse.given_name || '', last_name: verifyTokenResponse?.family_name || '', password: '', + profile_pic_url: verifyTokenResponse?.picture || '', }; return await this.createUserGoogle(userCreationPayload); } + + const userOranisations = await this.organisationService.getAllUserOrganisations(userExists.id); + const isSuperAdmin = userOranisations.map(instance => instance.user_role).includes('super-admin'); const accessToken = this.jwtService.sign({ sub: userExists.id, id: userExists.id, @@ -285,22 +324,48 @@ export default class AuthenticationService { first_name: userExists.first_name, last_name: userExists.last_name, }); + + if (!userExists.profile.profile_pic_url || userExists.profile.profile_pic_url !== verifyTokenResponse.picture) { + const updateDto = new UpdateProfileDto(); + updateDto.profile_pic_url = verifyTokenResponse.picture; + await this.profileService.updateProfile(userExists.profile.id, updateDto); + } + return { message: SYS_MSG.LOGIN_SUCCESSFUL, access_token: accessToken, - user: { - id: userExists.id, - email: userExists.email, - first_name: userExists.first_name, - last_name: userExists.last_name, - fullname: userExists.first_name + ' ' + userExists.last_name, - role: '', + data: { + user: { + id: userExists.id, + email: userExists.email, + first_name: userExists.first_name, + last_name: userExists.last_name, + avatar_url: userExists.profile.profile_pic_url, + is_superadmin: isSuperAdmin, + }, + organisations: userOranisations, }, }; } public async createUserGoogle(userPayload: CreateUserDTO) { const newUser = await this.userService.createUser(userPayload); + const newOrganisationPaload = { + name: `${newUser.first_name}'s Organisation`, + description: '', + email: newUser.email, + industry: '', + type: '', + country: '', + address: '', + state: '', + }; + + await this.organisationService.create(newOrganisationPaload, newUser.id); + + const userOranisations = await this.organisationService.getAllUserOrganisations(newUser.id); + const isSuperAdmin = userOranisations.map(instance => instance.user_role).includes('super-admin'); + const accessToken = await this.jwtService.sign({ sub: newUser.id, id: newUser.id, @@ -308,17 +373,26 @@ export default class AuthenticationService { first_name: userPayload.first_name, last_name: userPayload.last_name, }); + if (userPayload.profile_pic_url) { + const updateDto = new UpdateProfileDto(); + updateDto.profile_pic_url = userPayload.profile_pic_url; + await this.profileService.updateProfile(newUser.profile.id, updateDto); + } return { + status_code: HttpStatus.CREATED, message: SYS_MSG.USER_CREATED, access_token: accessToken, - user: { - id: newUser.id, - email: newUser.email, - first_name: newUser.first_name, - last_name: newUser.last_name, - fullname: newUser.first_name + ' ' + newUser.last_name, - role: '', + data: { + user: { + id: newUser.id, + email: newUser.email, + first_name: newUser.first_name, + last_name: newUser.last_name, + is_superadmin: isSuperAdmin, + avatar_url: newUser.profile.profile_pic_url, + }, + organisations: userOranisations, }, }; } @@ -340,12 +414,7 @@ export default class AuthenticationService { const otp = await this.otpService.createOtp(user.id); - const emailData = new SendEmailDto(); - emailData.to = user.email; - emailData.subject = 'Login with OTP'; - emailData.template = 'login-otp'; - emailData.context = { token: otp.token, email: user.email }; - await this.emailService.sendEmail(emailData); + await this.emailService.sendLoginOtp(user.email, otp.token); return { message: SYS_MSG.SIGN_IN_OTP_SENT, @@ -379,7 +448,7 @@ export default class AuthenticationService { return { message: SYS_MSG.LOGIN_SUCCESSFUL, access_token: accessToken, - user: responsePayload, + data: responsePayload, }; } } diff --git a/src/modules/auth/dto/auth-response.dto.ts b/src/modules/auth/dto/auth-response.dto.ts new file mode 100644 index 000000000..db5f510cf --- /dev/null +++ b/src/modules/auth/dto/auth-response.dto.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class AuthResponseDto { + @ApiProperty({ + description: 'Status message of the authentication response', + example: 'Authentication successful', + }) + message: string; + + @ApiProperty({ + description: 'Access token for authentication', + example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', + }) + access_token: string; + + @ApiProperty({ + description: 'Additional data containing user object', + type: 'object', + }) + data: object; +} diff --git a/src/modules/auth/dto/change-password.dto.ts b/src/modules/auth/dto/change-password.dto.ts index 69798af37..63456e30e 100644 --- a/src/modules/auth/dto/change-password.dto.ts +++ b/src/modules/auth/dto/change-password.dto.ts @@ -1,11 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsString, IsStrongPassword, MinLength } from 'class-validator'; export class ChangePasswordDto { + @ApiProperty({ + description: 'The current password of the user', + example: 'OldPassword123!', + }) @IsNotEmpty() @IsString() @MinLength(8) oldPassword: string; + @ApiProperty({ + description: 'The new password to set for the user. Must meet strong password criteria.', + example: 'NewPassword123!', + }) @IsNotEmpty() @IsString() @MinLength(8) diff --git a/src/modules/auth/dto/create-user.dto.ts b/src/modules/auth/dto/create-user.dto.ts index f1adacf7a..beaadc611 100644 --- a/src/modules/auth/dto/create-user.dto.ts +++ b/src/modules/auth/dto/create-user.dto.ts @@ -2,17 +2,45 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEmail, IsNotEmpty, IsOptional, IsString, IsStrongPassword, MinLength } from 'class-validator'; export class CreateUserDTO { + @ApiProperty({ + description: 'The email address of the user', + example: 'user@example.com', + }) @IsEmail() email: string; + @ApiProperty({ + description: 'The first name of the user', + example: 'John', + }) @IsNotEmpty() @IsString() first_name: string; + @ApiProperty({ + description: 'The last name of the user', + example: 'Doe', + }) @IsNotEmpty() @IsString() last_name: string; + @ApiProperty({ + description: 'The URL for the user profile picture', + example: 'https://example.com/profile-pic.jpg', + required: false, + }) + @IsOptional() + @IsString() + profile_pic_url?: string; + + @ApiProperty({ + description: + 'The password for the user account.\ + It must contain at least one uppercase letter, one lowercase letter,\ + one number, and one special character.', + example: 'P@ssw0rd!', + }) @MinLength(8) @IsNotEmpty() @IsStrongPassword( @@ -24,6 +52,11 @@ export class CreateUserDTO { ) password: string; + @ApiProperty({ + description: 'An optional admin secret for elevated permissions', + example: 'admin123', + required: false, + }) @IsOptional() @IsString() admin_secret?: string; diff --git a/src/modules/auth/dto/generic-reponse.dto.ts b/src/modules/auth/dto/generic-reponse.dto.ts new file mode 100644 index 000000000..e2a0a9a9b --- /dev/null +++ b/src/modules/auth/dto/generic-reponse.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class GenericAuthResponseDto { + @ApiProperty({ + description: 'Status message indicating the result of the operation', + example: 'Verification token sent to mail', + }) + message: string; +} diff --git a/src/modules/auth/dto/google-auth.dto.ts b/src/modules/auth/dto/google-auth.dto.ts new file mode 100644 index 000000000..bec97ffea --- /dev/null +++ b/src/modules/auth/dto/google-auth.dto.ts @@ -0,0 +1,63 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class GoogleAuthPayloadDto { + @ApiProperty({ + description: 'Access token provided by Google', + example: 'ya29.a0AfH6SMBb4JG...', + }) + access_token: string; + + @ApiProperty({ + description: 'Expiration time in seconds for the access token', + example: 3599, + }) + expires_in: number; + + @ApiProperty({ + description: 'Refresh token provided by Google', + example: '1//09gJ...', + }) + refresh_token: string; + + @ApiProperty({ + description: 'Scope of the access token', + example: 'https://www.googleapis.com/auth/userinfo.profile', + }) + scope: string; + + @ApiProperty({ + description: 'Type of the token provided', + example: 'Bearer', + }) + token_type: string; + + @ApiProperty({ + description: 'ID token provided by Google', + example: 'eyJhbGciOiJSUzI1NiIs...', + }) + id_token: string; + + @ApiProperty({ + description: 'Expiration time in epoch format', + example: 1629716100, + }) + expires_at: number; + + @ApiProperty({ + description: 'Provider of the authentication service', + example: 'google', + }) + provider: string; + + @ApiProperty({ + description: 'Type of the authentication', + example: 'oauth', + }) + type: string; + + @ApiProperty({ + description: 'Provider account ID', + example: '1234567890', + }) + providerAccountId: string; +} diff --git a/src/modules/auth/dto/login-error-dto.ts b/src/modules/auth/dto/login-error-dto.ts index d3c6b3eb2..7b89bc36f 100644 --- a/src/modules/auth/dto/login-error-dto.ts +++ b/src/modules/auth/dto/login-error-dto.ts @@ -1,4 +1,15 @@ +import { ApiProperty } from '@nestjs/swagger'; + export class LoginErrorResponseDto { + @ApiProperty({ + description: 'Error message providing details about the login failure', + example: 'Invalid credentials provided', + }) message: string; + + @ApiProperty({ + description: 'HTTP status code indicating the type of error', + example: 401, + }) status_code: number; } diff --git a/src/modules/auth/dto/login-response.dto.ts b/src/modules/auth/dto/login-response.dto.ts index d1116d331..042a22e0b 100644 --- a/src/modules/auth/dto/login-response.dto.ts +++ b/src/modules/auth/dto/login-response.dto.ts @@ -1,12 +1,55 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class UserDto { + @ApiProperty({ + description: 'Unique identifier for the user', + example: '12345', + }) + id: string; + + @ApiProperty({ + description: 'First name of the user', + example: 'John', + }) + first_name: string; + + @ApiProperty({ + description: 'Last name of the user', + example: 'Doe', + }) + last_name: string; + + @ApiProperty({ + description: 'Email address of the user', + example: 'john.doe@example.com', + }) + email: string; +} + +export class DataDto { + @ApiProperty({ + description: 'User details', + type: UserDto, + }) + user: UserDto; +} + export class LoginResponseDto { + @ApiProperty({ + description: 'Status message of the login response', + example: 'Login successful', + }) message: string; - data: { - user: { - id: string; - first_name: string; - last_name: string; - email: string; - }; - }; + + @ApiProperty({ + description: 'Data object containing user information and other relevant data', + type: DataDto, + }) + data: DataDto; + + @ApiProperty({ + description: 'Access token for authentication', + example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', + }) access_token: string; } diff --git a/src/modules/auth/dto/request-signin-token.dto.ts b/src/modules/auth/dto/request-signin-token.dto.ts index 39e439002..7a82d7211 100644 --- a/src/modules/auth/dto/request-signin-token.dto.ts +++ b/src/modules/auth/dto/request-signin-token.dto.ts @@ -1,6 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; export class RequestSigninTokenDto { + @ApiProperty({ + description: 'The email address of the user requesting a sign-in token', + example: 'user@example.com', + }) @IsNotEmpty() @IsString() @IsEmail() diff --git a/src/modules/auth/dto/update-user-password.dto.ts b/src/modules/auth/dto/update-user-password.dto.ts new file mode 100644 index 000000000..56672adf1 --- /dev/null +++ b/src/modules/auth/dto/update-user-password.dto.ts @@ -0,0 +1,41 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class UserDto { + @ApiProperty({ + description: 'Unique identifier for the user', + example: '12345', + }) + id: string; + + @ApiProperty({ + description: 'Name of the user', + example: 'John Doe', + }) + name: string; + + @ApiProperty({ + description: 'Phone number of the user', + example: '+1234567890', + }) + phone_number: string; +} + +export class UpdateUserPasswordResponseDTO { + @ApiProperty({ + description: 'Status of the password update operation', + example: 'success', + }) + status: string; + + @ApiProperty({ + description: 'Message providing additional information about the password update', + example: 'Password updated successfully', + }) + message: string; + + @ApiProperty({ + description: 'Details of the updated user', + type: UserDto, + }) + user: UserDto; +} diff --git a/src/modules/auth/dto/verify-2fa.dto.ts b/src/modules/auth/dto/verify-2fa.dto.ts index 8971817e7..877fe425e 100644 --- a/src/modules/auth/dto/verify-2fa.dto.ts +++ b/src/modules/auth/dto/verify-2fa.dto.ts @@ -2,7 +2,10 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsString } from 'class-validator'; export class Verify2FADto { - @ApiProperty() + @ApiProperty({ + description: 'The email address of the user', + example: 'user@example.com', + }) @IsString() @IsNotEmpty() totp_code: string; diff --git a/src/modules/auth/google-auth.service.ts b/src/modules/auth/google-auth.service.ts deleted file mode 100644 index 291d443c5..000000000 --- a/src/modules/auth/google-auth.service.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; -import authConfig from '../../../config/auth.config'; -import { OAuth2Client } from 'google-auth-library'; - -@Injectable() -export class GoogleAuthService { - private clientId = authConfig().google.clientID; - private client = new OAuth2Client(this.clientId); - - async verifyToken(token: string): Promise { - try { - const ticket = await this.client.verifyIdToken({ - idToken: token, - audience: this.clientId, - }); - - const payload = ticket.getPayload(); - - return payload; - } catch (error) { - console.log(error); - throw new BadRequestException('Invalid Google token'); - } - } -} diff --git a/src/modules/auth/interfaces/GoogleAuthPayloadInterface.ts b/src/modules/auth/interfaces/GoogleAuthPayloadInterface.ts index a475149a2..94dcd9386 100644 --- a/src/modules/auth/interfaces/GoogleAuthPayloadInterface.ts +++ b/src/modules/auth/interfaces/GoogleAuthPayloadInterface.ts @@ -1,21 +1,3 @@ export default interface GoogleAuthPayload { - access_token: string; - - expires_in: number; - - refresh_token: string; - - scope: string; - - token_type: string; - id_token: string; - - expires_at: number; - - provider: string; - - type: string; - - providerAccountId: string; } diff --git a/src/modules/auth/interfaces/GoogleVerificationPayloadInterface.ts b/src/modules/auth/interfaces/GoogleVerificationPayloadInterface.ts deleted file mode 100644 index e51e00e26..000000000 --- a/src/modules/auth/interfaces/GoogleVerificationPayloadInterface.ts +++ /dev/null @@ -1,27 +0,0 @@ -export interface GoogleVerificationPayloadInterface { - iss: string; - - azp: string; - - aud: string; - - sub: string; - - email: string; - - email_verified: true; - - at_hash: string; - - name: string; - - picture: string; - - given_name: string; - - family_name: string; - - iat: number; - - exp: number; -} diff --git a/src/modules/auth/strategies/google.strategy.ts b/src/modules/auth/strategies/google.strategy.ts deleted file mode 100644 index 1ad1b992a..000000000 --- a/src/modules/auth/strategies/google.strategy.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { ConfigType } from '@nestjs/config'; -import { PassportStrategy } from '@nestjs/passport'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import config from '../../../../config/auth.config'; -import { User } from '../../user/entities/user.entity'; -import { Strategy, VerifyCallback } from 'passport-google-oauth20'; - -@Injectable() -export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { - constructor(@InjectRepository(User) private userRepository: Repository) { - super({ - clientID: config().google.clientID, - clientSecret: config().google.clientSecret, - callbackURL: config().google.callbackURL, - scope: ['profile', 'email'], - }); - } - - async validate(accessToken: string, refreshToken: string, profile: any, done: VerifyCallback): Promise { - const { id, name, emails, photos } = profile; - - const user = { - first_name: name.givenName, - last_name: name.familyName, - email: emails[0].value, - password: '', - is_active: true, - secret: '', - is_2fa_enabled: false, - }; - try { - let existingUser = await this.userRepository.findOne({ - where: { email: user.email }, - }); - - if (!existingUser) { - existingUser = this.userRepository.create(user); - await this.userRepository.save(existingUser); - } - - return done(null, existingUser); - } catch (error) { - return done(error, false); - } - } -} diff --git a/src/modules/auth/tests/auth.service.spec.ts b/src/modules/auth/tests/auth.service.spec.ts index 11204a6ca..0e28eb19c 100644 --- a/src/modules/auth/tests/auth.service.spec.ts +++ b/src/modules/auth/tests/auth.service.spec.ts @@ -13,25 +13,26 @@ import { User, UserType } from '../../user/entities/user.entity'; import { Otp } from '../../otp/entities/otp.entity'; import UserResponseDTO from '../../user/dto/user-response.dto'; import { LoginDto } from '../dto/login.dto'; -import { GoogleAuthService } from '../google-auth.service'; import { Profile } from '../../profile/entities/profile.entity'; import { CustomHttpException } from '../../../helpers/custom-http-filter'; +import { OrganisationsService } from '../../../modules/organisations/organisations.service'; +import { ProfileService } from '../../profile/profile.service'; jest.mock('speakeasy'); describe('AuthenticationService', () => { let service: AuthenticationService; let userServiceMock: jest.Mocked; + let profileServiceMock: jest.Mocked; let jwtServiceMock: jest.Mocked; let otpServiceMock: jest.Mocked; let emailServiceMock: jest.Mocked; - let googleAuthServiceMock: jest.Mocked; + let organisationServiceMock: jest.Mocked; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ AuthenticationService, - { provide: UserService, useValue: { @@ -41,9 +42,9 @@ describe('AuthenticationService', () => { }, }, { - provide: GoogleAuthService, + provide: ProfileService, useValue: { - verifyToken: jest.fn(), + updateProfile: jest.fn(), }, }, { @@ -58,6 +59,13 @@ describe('AuthenticationService', () => { createOtp: jest.fn().mockResolvedValue({ token: 999987 }), }, }, + { + provide: OrganisationsService, + useValue: { + create: jest.fn(), + getAllUserOrganisations: jest.fn(), + }, + }, { provide: EmailService, useValue: { @@ -71,10 +79,11 @@ describe('AuthenticationService', () => { service = module.get(AuthenticationService); userServiceMock = module.get(UserService) as jest.Mocked; + profileServiceMock = module.get(ProfileService) as jest.Mocked; jwtServiceMock = module.get(JwtService) as jest.Mocked; otpServiceMock = module.get(OtpService) as jest.Mocked; emailServiceMock = module.get(EmailService) as jest.Mocked; - googleAuthServiceMock = module.get(GoogleAuthService) as jest.Mocked; + organisationServiceMock = module.get(OrganisationsService) as jest.Mocked; }); afterEach(() => { @@ -100,7 +109,6 @@ describe('AuthenticationService', () => { first_name: createUserDto.first_name, last_name: createUserDto.last_name, created_at: new Date(), - user_type: UserType.USER, is_active: true, attempts_left: 3, time_left: 0, @@ -111,8 +119,43 @@ describe('AuthenticationService', () => { it('should create a new user successfully', async () => { userServiceMock.getUserRecord.mockResolvedValueOnce(null); + userServiceMock.createUser.mockResolvedValueOnce(undefined); - userServiceMock.getUserRecord.mockResolvedValueOnce(mockUser as User); + + userServiceMock.getUserRecord.mockResolvedValueOnce({ + id: '1', + first_name: 'John', + last_name: 'Doe', + email: 'test@example.com', + profile: { + profile_pic_url: 'some_url', + }, + } as User); + + organisationServiceMock.create.mockResolvedValueOnce({ + id: 'e12973d1-cbc3-45f8-ba13-14991e4490fa', + name: "John's Organisation", + description: '', + email: 'test@example.com', + industry: '', + type: '', + country: '', + address: '', + state: '', + owner_id: 'user-id', + created_at: new Date(), + updated_at: new Date(), + }); + + organisationServiceMock.getAllUserOrganisations.mockResolvedValueOnce([ + { + organisation_id: 'e12973d1-cbc3-45f8-ba13-14991e4490fa', + name: "John's Organisation", + user_role: 'admin', + is_owner: true, + }, + ]); + jwtServiceMock.sign.mockReturnValueOnce('mocked_token'); const result = await service.createNewUser(createUserDto); @@ -122,13 +165,21 @@ describe('AuthenticationService', () => { access_token: 'mocked_token', data: { user: { - avatar_url: 'some_url', - email: 'test@example.com', - first_name: 'John', id: '1', + first_name: 'John', last_name: 'Doe', - role: 'vendor', + email: 'test@example.com', + is_superadmin: false, + avatar_url: 'some_url', }, + oranisations: [ + { + organisation_id: 'e12973d1-cbc3-45f8-ba13-14991e4490fa', + name: "John's Organisation", + user_role: 'admin', + is_owner: true, + }, + ], }, }); }); @@ -161,7 +212,6 @@ describe('AuthenticationService', () => { attempts_left: 2, created_at: new Date(), updated_at: new Date(), - user_type: UserType.USER, profile: { profile_pic_url: 'profile_url', } as Profile, @@ -169,6 +219,14 @@ describe('AuthenticationService', () => { jest.spyOn(userServiceMock, 'getUserRecord').mockResolvedValue(user); jest.spyOn(bcrypt, 'compare').mockImplementation(() => Promise.resolve(true)); + organisationServiceMock.getAllUserOrganisations.mockResolvedValueOnce([ + { + organisation_id: 'e12973d1-cbc3-45f8-ba13-14991e4490fa', + name: "Test's Organisation", + user_role: 'admin', + is_owner: true, + }, + ]); jwtServiceMock.sign.mockReturnValue('jwt_token'); const result = await service.loginUser(loginDto); @@ -183,8 +241,16 @@ describe('AuthenticationService', () => { last_name: 'User', email: 'test@example.com', avatar_url: 'profile_url', - role: 'vendor', + is_superadmin: false, }, + organisations: [ + { + organisation_id: 'e12973d1-cbc3-45f8-ba13-14991e4490fa', + name: "Test's Organisation", + user_role: 'admin', + is_owner: true, + }, + ], }, }); }); @@ -194,7 +260,7 @@ describe('AuthenticationService', () => { userServiceMock.getUserRecord.mockResolvedValue(null); - expect(service.loginUser(loginDto)).rejects.toThrow(CustomHttpException); + await expect(service.loginUser(loginDto)).rejects.toThrow(CustomHttpException); }); it('should throw an unauthorized error for invalid password', async () => { @@ -213,12 +279,12 @@ describe('AuthenticationService', () => { userServiceMock.getUserRecord.mockResolvedValue(user); jest.spyOn(bcrypt, 'compare').mockImplementation(() => Promise.resolve(false)); - expect(service.loginUser(loginDto)).rejects.toThrow(CustomHttpException); + await expect(service.loginUser(loginDto)).rejects.toThrow(CustomHttpException); }); }); describe('verify2fa', () => { - it('should throw error if totp code is incorrect', () => { + it('should throw error if totp code is incorrect', async () => { const verify2faDto: Verify2FADto = { totp_code: '12345' }; const userId = 'some-uuid-here'; @@ -237,7 +303,7 @@ describe('AuthenticationService', () => { jest.spyOn(userServiceMock, 'getUserRecord').mockResolvedValueOnce(user); (speakeasy.totp.verify as jest.Mock).mockReturnValue(false); - expect(service.verify2fa(verify2faDto, userId)).rejects.toThrow(CustomHttpException); + await expect(service.verify2fa(verify2faDto, userId)).rejects.toThrow(CustomHttpException); }); it('should enable 2fa if successful', async () => { @@ -334,11 +400,10 @@ describe('AuthenticationService', () => { const emailData = { to: email, subject: 'Reset Password', - template: 'reset-password', + template: 'Password-Reset-Complete-Template', context: { - link: 'http://example.com/auth/reset-password', - email: email, - token: '123456', + otp: '123456', + name: email, }, }; @@ -360,15 +425,10 @@ describe('AuthenticationService', () => { userServiceMock.getUserRecord.mockResolvedValueOnce(mockUser as User); otpServiceMock.createOtp.mockResolvedValueOnce(mockOtp); - emailServiceMock.sendEmail.mockResolvedValueOnce({ - status_code: HttpStatus.OK, - message: 'Email sent successfully', - }); const result = await service.forgotPassword({ email }); expect(result.message).toBe('Email sent successfully'); - expect(emailServiceMock.sendEmail).toHaveBeenCalledWith(emailData); }); it('should throw error if user not found', async () => { diff --git a/src/modules/billing-plans/billing-plan.controller.ts b/src/modules/billing-plans/billing-plan.controller.ts index 133a2b16b..dfde14eea 100644 --- a/src/modules/billing-plans/billing-plan.controller.ts +++ b/src/modules/billing-plans/billing-plan.controller.ts @@ -1,8 +1,29 @@ -import { Controller, Get, Param, Post } from '@nestjs/common'; -import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { + Controller, + Get, + Param, + Post, + Body, + UseGuards, + Patch, + ParseUUIDPipe, + Delete, + HttpCode, + HttpStatus, +} from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { SuperAdminGuard } from '../../guards/super-admin.guard'; import { BillingPlanService } from './billing-plan.service'; import { skipAuth } from '../../helpers/skipAuth'; import { BillingPlanDto } from './dto/billing-plan.dto'; +import { + createBillingPlanDocs, + deleteBillingPlanDocs, + getAllBillingPlansDocs, + getSingleBillingPlanDocs, + updateBillingPlanDocs, +} from './docs/billing-plan-docs'; +import { UpdateBillingPlanDto } from './dto/update-billing-plan.dto'; @ApiTags('Billing Plans') @Controller('billing-plans') @@ -10,29 +31,41 @@ export class BillingPlanController { constructor(private readonly billingPlanService: BillingPlanService) {} @Post('/') - @ApiOperation({ summary: 'Create billing plans' }) - @ApiResponse({ status: 201, description: 'Billing plans created successfully.', type: [BillingPlanDto] }) - @ApiResponse({ status: 200, description: 'Billing plans already exist in the database.', type: [BillingPlanDto] }) - async createBillingPlan() { - return this.billingPlanService.createBillingPlan(); + @createBillingPlanDocs() + @UseGuards(SuperAdminGuard) + async createBillingPlan(@Body() createBillingPlanDto: BillingPlanDto) { + return this.billingPlanService.createBillingPlan(createBillingPlanDto); } @skipAuth() + @getAllBillingPlansDocs() @Get('/') - @ApiOperation({ summary: 'Get all billing plans' }) - @ApiResponse({ status: 200, description: 'Billing plans retrieved successfully.', type: [BillingPlanDto] }) - @ApiResponse({ status: 404, description: 'No billing plans found.' }) async getAllBillingPlans() { return this.billingPlanService.getAllBillingPlans(); } @skipAuth() + @getSingleBillingPlanDocs() @Get('/:id') - @ApiOperation({ summary: 'Get single billing plan by ID' }) - @ApiResponse({ status: 200, description: 'Billing plan retrieved successfully', type: BillingPlanDto }) - @ApiResponse({ status: 400, description: 'Invalid billing plan ID' }) - @ApiResponse({ status: 404, description: 'Billing plan not found' }) async getSingleBillingPlan(@Param('id') id: string) { return this.billingPlanService.getSingleBillingPlan(id); } + + @UseGuards(SuperAdminGuard) + @updateBillingPlanDocs() + @Patch('/:id') + async updateBillingPlan( + @Param('id', new ParseUUIDPipe()) id: string, + @Body() updateBillingPlanDto: UpdateBillingPlanDto + ) { + return this.billingPlanService.updateBillingPlan(id, updateBillingPlanDto); + } + + @UseGuards(SuperAdminGuard) + @deleteBillingPlanDocs() + @Delete('/:id') + @HttpCode(HttpStatus.NO_CONTENT) + async deleteBillingPlan(@Param('id', ParseUUIDPipe) id: string) { + return this.billingPlanService.deleteBillingPlan(id); + } } diff --git a/src/modules/billing-plans/billing-plan.module.ts b/src/modules/billing-plans/billing-plan.module.ts index da915ec3f..3447d932d 100644 --- a/src/modules/billing-plans/billing-plan.module.ts +++ b/src/modules/billing-plans/billing-plan.module.ts @@ -3,9 +3,13 @@ import { BillingPlanService } from './billing-plan.service'; import { BillingPlanController } from './billing-plan.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { BillingPlan } from './entities/billing-plan.entity'; +import { User } from '../user/entities/user.entity'; +import { Organisation } from '../organisations/entities/organisations.entity'; +import { OrganisationUserRole } from '../role/entities/organisation-user-role.entity'; +import { Role } from '../role/entities/role.entity'; @Module({ - imports: [TypeOrmModule.forFeature([BillingPlan])], + imports: [TypeOrmModule.forFeature([BillingPlan, User, Organisation, OrganisationUserRole, Role])], controllers: [BillingPlanController], providers: [BillingPlanService], }) diff --git a/src/modules/billing-plans/billing-plan.service.ts b/src/modules/billing-plans/billing-plan.service.ts index c0d7fbc53..a948535fd 100644 --- a/src/modules/billing-plans/billing-plan.service.ts +++ b/src/modules/billing-plans/billing-plan.service.ts @@ -2,6 +2,11 @@ import { Injectable, HttpStatus, HttpException, BadRequestException, NotFoundExc import { Repository } from 'typeorm'; import { BillingPlan } from './entities/billing-plan.entity'; import { InjectRepository } from '@nestjs/typeorm'; +import { BillingPlanDto } from './dto/billing-plan.dto'; +import * as SYS_MSG from '../../helpers/SystemMessages'; +import { CustomHttpException } from '../../helpers/custom-http-filter'; +import { BillingPlanMapper } from './mapper/billing-plan.mapper'; +import { UpdateBillingPlanDto } from './dto/update-billing-plan.dto'; @Injectable() export class BillingPlanService { @@ -10,119 +15,70 @@ export class BillingPlanService { private readonly billingPlanRepository: Repository ) {} - async createBillingPlan() { - try { - const billingPlans = await this.billingPlanRepository.find(); - - if (billingPlans.length > 0) { - const plans = billingPlans.map(plan => ({ id: plan.id, name: plan.name, price: plan.price })); + async createBillingPlan(createBillingPlanDto: BillingPlanDto) { + const billingPlan = await this.billingPlanRepository.findOne({ + where: { + name: createBillingPlanDto.name, + }, + }); - return { - status_code: HttpStatus.OK, - message: 'Billing plans already exist in the database', - data: plans, - }; - } + if (billingPlan) { + throw new CustomHttpException(SYS_MSG.BILLING_PLAN_ALREADY_EXISTS, HttpStatus.BAD_REQUEST); + } - const newPlans = this.createBillingPlanEntities(); - const createdPlans = await this.billingPlanRepository.save(newPlans); - const plans = createdPlans.map(plan => ({ id: plan.id, name: plan.name, price: plan.price })); + const newPlan = this.billingPlanRepository.create(createBillingPlanDto); + const createdPlan = await this.billingPlanRepository.save(newPlan); + const plan = BillingPlanMapper.mapToResponseFormat(createdPlan); - return { - message: 'Billing plans created successfully', - data: plans, - }; - } catch (error) { - throw new HttpException( - { - message: `Internal server error: ${error.message}`, - status_code: HttpStatus.INTERNAL_SERVER_ERROR, - }, - HttpStatus.INTERNAL_SERVER_ERROR - ); - } + return { + message: SYS_MSG.BILLING_PLAN_CREATED, + data: plan, + }; } async getAllBillingPlans() { - try { - const allPlans = await this.billingPlanRepository.find(); - - if (allPlans.length === 0) { - throw new NotFoundException('No billing plans found'); - } - - const plans = allPlans.map(plan => ({ id: plan.id, name: plan.name, price: plan.price })); - - return { - message: 'Billing plans retrieved successfully', - data: plans, - }; - } catch (error) { - if (error instanceof NotFoundException || error instanceof BadRequestException) { - throw error; - } - - throw new HttpException( - { - message: `Internal server error: ${error.message}`, - status_code: HttpStatus.INTERNAL_SERVER_ERROR, - }, - HttpStatus.INTERNAL_SERVER_ERROR - ); + const allPlans = await this.billingPlanRepository.find(); + if (allPlans.length === 0) { + throw new NotFoundException('No billing plans found'); } + const plans = allPlans.map(plan => BillingPlanMapper.mapToResponseFormat(plan)); + + return { + message: 'Billing plans retrieved successfully', + data: plans, + }; } async getSingleBillingPlan(id: string) { - try { - if (!id) { - throw new BadRequestException('Invalid billing plan ID'); - } - - const billingPlan = await this.billingPlanRepository.findOneBy({ id }); - - if (!billingPlan) { - throw new NotFoundException('Billing plan not found'); - } - - const plan = { id: billingPlan.id, name: billingPlan.name, price: billingPlan.price }; - - return { - message: 'Billing plan retrieved successfully', - data: plan, - }; - } catch (error) { - if (error instanceof NotFoundException || error instanceof BadRequestException) { - throw error; - } + if (!id) { + throw new BadRequestException('Invalid billing plan ID'); + } + const billingPlan = await this.billingPlanRepository.findOneBy({ id }); - throw new HttpException( - { - message: `Internal server error: ${error.message}`, - status_code: HttpStatus.INTERNAL_SERVER_ERROR, - }, - HttpStatus.INTERNAL_SERVER_ERROR - ); + if (!billingPlan) { + throw new NotFoundException('Billing plan not found'); } + const plan = BillingPlanMapper.mapToResponseFormat(billingPlan); + return { + message: 'Billing plan retrieved successfully', + data: plan, + }; } - private createBillingPlanEntities() { - const freePlan = this.billingPlanRepository.create({ - name: 'Free', - price: 0, - }); - const basicPlan = this.billingPlanRepository.create({ - name: 'Basic', - price: 20, - }); - const advancedPlan = this.billingPlanRepository.create({ - name: 'Advanced', - price: 50, - }); - const premiumPlan = this.billingPlanRepository.create({ - name: 'Premium', - price: 100, - }); + async updateBillingPlan(id: string, updateBillingPlanDto: UpdateBillingPlanDto): Promise { + const billing_plan = await this.billingPlanRepository.findOneBy({ id }); + if (!billing_plan) { + throw new CustomHttpException(SYS_MSG.BILLING_PLAN_NOT_FOUND, HttpStatus.NOT_FOUND); + } + Object.assign(billing_plan, updateBillingPlanDto); + return await this.billingPlanRepository.save(billing_plan); + } - return [freePlan, basicPlan, advancedPlan, premiumPlan]; + async deleteBillingPlan(id: string): Promise { + const billing_plan = await this.billingPlanRepository.findOne({ where: { id: id } }); + if (!billing_plan) { + throw new CustomHttpException(SYS_MSG.BILLING_PLAN_NOT_FOUND, HttpStatus.NOT_FOUND); + } + await this.billingPlanRepository.delete(id); } } diff --git a/src/modules/billing-plans/docs/billing-plan-docs.ts b/src/modules/billing-plans/docs/billing-plan-docs.ts new file mode 100644 index 000000000..97faf5c4e --- /dev/null +++ b/src/modules/billing-plans/docs/billing-plan-docs.ts @@ -0,0 +1,47 @@ +import { applyDecorators } from '@nestjs/common'; +import { BillingPlanDto } from '../dto/billing-plan.dto'; +import { ApiBearerAuth, ApiBody, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { BillingPlan } from '../entities/billing-plan.entity'; + +export function createBillingPlanDocs() { + return applyDecorators( + ApiBearerAuth(), + ApiOperation({ summary: 'Create billing plans' }), + ApiBody({ type: BillingPlanDto }), + ApiResponse({ status: 201, description: 'Billing plan created successfully.', type: BillingPlanDto }), + ApiResponse({ status: 200, description: 'Billing plan already exists in the database.', type: [BillingPlanDto] }) + ); +} + +export function getAllBillingPlansDocs() { + return applyDecorators( + ApiOperation({ summary: 'Get all billing plans' }), + ApiResponse({ status: 200, description: 'Billing plans retrieved successfully.', type: [BillingPlanDto] }), + ApiResponse({ status: 404, description: 'No billing plans found.' }) + ); +} + +export function getSingleBillingPlanDocs() { + return applyDecorators( + ApiOperation({ summary: 'Get single billing plan by ID' }), + ApiResponse({ status: 200, description: 'Billing plan retrieved successfully', type: BillingPlanDto }), + ApiResponse({ status: 400, description: 'Invalid billing plan ID' }), + ApiResponse({ status: 404, description: 'Billing plan not found' }) + ); +} + +export function updateBillingPlanDocs() { + return applyDecorators( + ApiOperation({ summary: 'Update a billing plan by ID' }), + ApiResponse({ status: 200, description: 'Billing plan updated successfully.', type: BillingPlan }), + ApiResponse({ status: 404, description: 'Billing plan not found.' }) + ); +} + +export function deleteBillingPlanDocs() { + return applyDecorators( + ApiOperation({ summary: 'Delete a billing plan by ID' }), + ApiResponse({ status: 204, description: 'Billing plan deleted successfully.' }), + ApiResponse({ status: 404, description: 'Billing plan not found.' }) + ); +} diff --git a/src/modules/billing-plans/dto/billing-plan.dto.ts b/src/modules/billing-plans/dto/billing-plan.dto.ts index ddd5829de..20cad8604 100644 --- a/src/modules/billing-plans/dto/billing-plan.dto.ts +++ b/src/modules/billing-plans/dto/billing-plan.dto.ts @@ -1,13 +1,25 @@ import { ApiProperty } from '@nestjs/swagger'; -import { randomUUID } from 'crypto'; +import { IsString, IsOptional, IsNumberString, IsBoolean } from 'class-validator'; export class BillingPlanDto { - @ApiProperty({ example: randomUUID() }) - id: string; - @ApiProperty({ example: 'Free' }) + @IsString() name: string; + @ApiProperty({ example: 'Free' }) + @IsString() + @IsOptional() + description: string; + + @ApiProperty({ example: 'monthly' }) + @IsString() + frequency: string; + @ApiProperty({ example: 0 }) - price: number; + @IsNumberString() + amount: number; + + @ApiProperty({ example: 'true' }) + @IsBoolean() + is_active: boolean; } diff --git a/src/modules/billing-plans/dto/update-billing-plan.dto.ts b/src/modules/billing-plans/dto/update-billing-plan.dto.ts new file mode 100644 index 000000000..f24653c00 --- /dev/null +++ b/src/modules/billing-plans/dto/update-billing-plan.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { BillingPlanDto } from './billing-plan.dto'; + +export class UpdateBillingPlanDto extends PartialType(BillingPlanDto) {} diff --git a/src/modules/billing-plans/entities/billing-plan.entity.ts b/src/modules/billing-plans/entities/billing-plan.entity.ts index 07ad6d761..c53c69fa2 100644 --- a/src/modules/billing-plans/entities/billing-plan.entity.ts +++ b/src/modules/billing-plans/entities/billing-plan.entity.ts @@ -1,12 +1,20 @@ import { AbstractBaseEntity } from '../../../entities/base.entity'; - import { Column, Entity } from 'typeorm'; @Entity() export class BillingPlan extends AbstractBaseEntity { - @Column({ type: 'text', nullable: false }) + @Column({ type: 'text', nullable: false, unique: true }) name: string; - @Column({ type: 'int', nullable: false, default: 0 }) - price: number; + @Column({ type: 'text', nullable: true }) + description: string; + + @Column({ type: 'text', nullable: false }) + frequency: string; + + @Column({ default: 'true' }) + is_active: boolean; + + @Column({ type: 'int', nullable: true }) + amount: number; } diff --git a/src/modules/billing-plans/mapper/billing-plan.mapper.ts b/src/modules/billing-plans/mapper/billing-plan.mapper.ts new file mode 100644 index 000000000..667d9560f --- /dev/null +++ b/src/modules/billing-plans/mapper/billing-plan.mapper.ts @@ -0,0 +1,17 @@ +import { BillingPlan } from '../entities/billing-plan.entity'; + +export class BillingPlanMapper { + static mapToResponseFormat(billingPlan: BillingPlan) { + if (!billingPlan) { + throw new Error('Billing plan entity is required'); + } + + return { + id: billingPlan.id, + name: billingPlan.name, + amount: billingPlan.amount, + frequency: billingPlan.frequency, + is_active: billingPlan.is_active, + }; + } +} diff --git a/src/modules/billing-plans/tests/billing-plan.service.spec.ts b/src/modules/billing-plans/tests/billing-plan.service.spec.ts index 0b729252e..d16161069 100644 --- a/src/modules/billing-plans/tests/billing-plan.service.spec.ts +++ b/src/modules/billing-plans/tests/billing-plan.service.spec.ts @@ -3,8 +3,10 @@ import { BillingPlanService } from '../billing-plan.service'; import { Repository } from 'typeorm'; import { getRepositoryToken } from '@nestjs/typeorm'; import { BillingPlan } from '../entities/billing-plan.entity'; -import { NotFoundException, HttpException, BadRequestException } from '@nestjs/common'; -import { BillingPlanDto } from '../dto/billing-plan.dto'; +import { NotFoundException, BadRequestException, HttpStatus } from '@nestjs/common'; +import { CustomHttpException } from '../../../helpers/custom-http-filter'; +import * as SYS_MSG from '../../../helpers/SystemMessages'; +import { BillingPlanMapper } from '../mapper/billing-plan.mapper'; describe('BillingPlanService', () => { let service: BillingPlanService; @@ -26,29 +28,67 @@ describe('BillingPlanService', () => { }); describe('createBillingPlan', () => { - it('should return existing billing plans if they already exist', async () => { - const billingPlans = [ - { id: '1', name: 'Free', price: 0 }, - { id: '2', name: 'Basic', price: 20 }, - ]; - - jest.spyOn(repository, 'find').mockResolvedValue(billingPlans as BillingPlan[]); - - const result = await service.createBillingPlan(); - - expect(result).toEqual({ - status_code: 200, - message: 'Billing plans already exist in the database', - data: billingPlans, - }); + it('should throw an error if they already exist', async () => { + const createPlanDto = { + name: 'Free', + description: 'free plan', + amount: 0, + frequency: 'never', + is_active: true, + }; + + const billingPlan = { + id: '1', + name: 'Free', + description: 'free plan', + amount: 0, + frequency: 'never', + is_active: true, + created_at: new Date(), + updated_at: new Date(), + }; + + jest.spyOn(repository, 'findOne').mockResolvedValue(billingPlan as BillingPlan); + + await expect(service.createBillingPlan(createPlanDto)).rejects.toThrow( + new CustomHttpException(SYS_MSG.BILLING_PLAN_ALREADY_EXISTS, HttpStatus.BAD_REQUEST) + ); }); }); describe('getAllBillingPlans', () => { it('should return all billing plans', async () => { const billingPlans = [ - { id: '1', name: 'Free', price: 0 }, - { id: '2', name: 'Basic', price: 20 }, + { + id: '1', + name: 'Free', + description: 'free plan', + amount: 0, + frequency: 'never', + is_active: true, + created_at: new Date(), + updated_at: new Date(), + }, + { + id: '2', + name: 'Standard', + description: 'standard plan', + amount: 50, + frequency: 'monthly', + is_active: true, + created_at: new Date(), + updated_at: new Date(), + }, + { + id: '1', + name: 'Premium', + description: 'premium plan', + amount: 120, + frequency: 'monthly', + is_active: true, + created_at: new Date(), + updated_at: new Date(), + }, ]; jest.spyOn(repository, 'find').mockResolvedValue(billingPlans as BillingPlan[]); @@ -57,7 +97,7 @@ describe('BillingPlanService', () => { expect(result).toEqual({ message: 'Billing plans retrieved successfully', - data: billingPlans.map(plan => ({ id: plan.id, name: plan.name, price: plan.price })), + data: billingPlans.map(plan => BillingPlanMapper.mapToResponseFormat(plan)), }); }); @@ -70,7 +110,16 @@ describe('BillingPlanService', () => { describe('getSingleBillingPlan', () => { it('should return a single billing plan', async () => { - const billingPlan = { id: '1', name: 'Free', price: 0 }; + const billingPlan = { + id: '1', + name: 'Free', + description: 'free plan', + amount: 0, + frequency: 'never', + is_active: true, + created_at: new Date(), + updated_at: new Date(), + }; jest.spyOn(repository, 'findOneBy').mockResolvedValue(billingPlan as BillingPlan); @@ -78,7 +127,7 @@ describe('BillingPlanService', () => { expect(result).toEqual({ message: 'Billing plan retrieved successfully', - data: { id: billingPlan.id, name: billingPlan.name, price: billingPlan.price }, + data: BillingPlanMapper.mapToResponseFormat(billingPlan), }); }); diff --git a/src/modules/blog-category/blog-category.controller.ts b/src/modules/blog-category/blog-category.controller.ts new file mode 100644 index 000000000..47e421641 --- /dev/null +++ b/src/modules/blog-category/blog-category.controller.ts @@ -0,0 +1,39 @@ +import { Body, Controller, Post, UseGuards, Request, Patch, Param } from '@nestjs/common'; +import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { SuperAdminGuard } from '../../guards/super-admin.guard'; +import { BlogCategoryService } from './blog-category.service'; +import { CreateBlogCategoryDto } from './dto/create-blog-category.dto'; +import { UpdateBlogCategoryDto } from './dto/update-blog-category.dto'; + +@ApiTags('Blog Categories') +@Controller('blogs/categories') +export class BlogCategoryController { + constructor(private readonly blogCategoryService: BlogCategoryService) {} + + @Post() + @UseGuards(SuperAdminGuard) + @ApiBearerAuth() + @ApiOperation({ summary: 'Create a new blog category' }) + @ApiResponse({ status: 201, description: 'Blog category created successfully.' }) + @ApiResponse({ status: 400, description: 'Invalid request data. Please provide a valid category name.' }) + @ApiResponse({ status: 401, description: 'Unauthorized. Token is missing or invalid.' }) + @ApiResponse({ status: 403, description: 'Forbidden. You do not have permission to create blog categories.' }) + @ApiResponse({ status: 500, description: 'Internal Server Error. Please try again later.' }) + async create(@Body() createBlogCategoryDto: CreateBlogCategoryDto) { + return await this.blogCategoryService.createOrganisationCategory(createBlogCategoryDto); + } + + @Patch(':id') + @UseGuards(SuperAdminGuard) + @ApiBearerAuth() + @ApiOperation({ summary: 'Update an organisation category' }) + @ApiResponse({ status: 200, description: 'Organisation category updated successfully.' }) + @ApiResponse({ status: 400, description: 'Invalid request data. Please provide valid data.' }) + @ApiResponse({ status: 401, description: 'Unauthorized. Token is missing or invalid.' }) + @ApiResponse({ status: 403, description: 'Forbidden. You do not have permission to update this category.' }) + @ApiResponse({ status: 404, description: 'Not Found. Category with the given ID does not exist.' }) + @ApiResponse({ status: 500, description: 'Internal Server Error. Please try again later.' }) + async updateBlogCategory(@Param('id') id: string, @Body() updateBlogCategoryDto: UpdateBlogCategoryDto) { + return await this.blogCategoryService.updateOrganisationCategory(id, updateBlogCategoryDto); + } +} diff --git a/src/modules/blog-category/blog-category.module.ts b/src/modules/blog-category/blog-category.module.ts new file mode 100644 index 000000000..f5832e26b --- /dev/null +++ b/src/modules/blog-category/blog-category.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { BlogCategoryService } from './blog-category.service'; +import { BlogCategoryController } from './blog-category.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { SuperAdminGuard } from '../../guards/super-admin.guard'; +import { User } from '../user/entities/user.entity'; +import { BlogCategory } from './entities/blog-category.entity'; +import { Organisation } from '../organisations/entities/organisations.entity'; +import { OrganisationUserRole } from '../role/entities/organisation-user-role.entity'; +import { Role } from '../role/entities/role.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([BlogCategory, User, Organisation, OrganisationUserRole, Role])], + controllers: [BlogCategoryController], + providers: [BlogCategoryService, SuperAdminGuard], +}) +export class BlogCategoryModule {} diff --git a/src/modules/blog-category/blog-category.service.ts b/src/modules/blog-category/blog-category.service.ts new file mode 100644 index 000000000..6d8c04a3e --- /dev/null +++ b/src/modules/blog-category/blog-category.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { BlogCategory } from './entities/blog-category.entity'; +import { Repository } from 'typeorm'; +import { CreateBlogCategoryDto } from './dto/create-blog-category.dto'; +import { CustomHttpException } from '../../helpers/custom-http-filter'; +import { CATEGORY_NOT_FOUND, ORG_NOT_FOUND } from '../../helpers/SystemMessages'; + +@Injectable() +export class BlogCategoryService { + constructor( + @InjectRepository(BlogCategory) + private blogCategoryRepository: Repository + ) {} + + async createOrganisationCategory(createBlogCategoryDto: CreateBlogCategoryDto) { + const blogCategory = this.blogCategoryRepository.create(createBlogCategoryDto); + await this.blogCategoryRepository.save(blogCategory); + + return { data: blogCategory, message: 'Blog category created successfully.' }; + } + + async updateOrganisationCategory(id: string, updateOrganisationCategoryDto: CreateBlogCategoryDto) { + const category = await this.blogCategoryRepository.findOne({ where: { id } }); + if (!category) { + throw new CustomHttpException(CATEGORY_NOT_FOUND, 404); + } + Object.assign(category, updateOrganisationCategoryDto); + await this.blogCategoryRepository.save(category); + return { data: category, message: 'Organisation category updated successfully.' }; + } +} diff --git a/src/modules/blog-category/dto/create-blog-category.dto.ts b/src/modules/blog-category/dto/create-blog-category.dto.ts new file mode 100644 index 000000000..c5822db7b --- /dev/null +++ b/src/modules/blog-category/dto/create-blog-category.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class CreateBlogCategoryDto { + @ApiProperty({ example: 'Technology' }) + @IsNotEmpty() + @IsString() + name: string; +} diff --git a/src/modules/blog-category/dto/update-blog-category.dto.ts b/src/modules/blog-category/dto/update-blog-category.dto.ts new file mode 100644 index 000000000..ebfbe9c55 --- /dev/null +++ b/src/modules/blog-category/dto/update-blog-category.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class UpdateBlogCategoryDto { + @ApiProperty({ example: 'Technology' }) + @IsNotEmpty() + @IsString() + name: string; +} diff --git a/src/modules/blog-category/entities/blog-category.entity.ts b/src/modules/blog-category/entities/blog-category.entity.ts new file mode 100644 index 000000000..b8e9fb4b9 --- /dev/null +++ b/src/modules/blog-category/entities/blog-category.entity.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { AbstractBaseEntity } from '../../../entities/base.entity'; +import { Entity, Column } from 'typeorm'; + +@Entity('blog_categories') +export class BlogCategory extends AbstractBaseEntity { + @ApiProperty() + @Column({ type: 'varchar', length: 255 }) + name: string; +} diff --git a/src/modules/blog-category/tests/blog-category.service.spec.ts b/src/modules/blog-category/tests/blog-category.service.spec.ts new file mode 100644 index 000000000..a310844ef --- /dev/null +++ b/src/modules/blog-category/tests/blog-category.service.spec.ts @@ -0,0 +1,60 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { BlogCategory } from '../entities/blog-category.entity'; +import { CreateBlogCategoryDto } from '../dto/create-blog-category.dto'; +import { BlogCategoryService } from '../blog-category.service'; + +describe('BlogCategoryService', () => { + let service: BlogCategoryService; + let repository: Repository; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + BlogCategoryService, + { + provide: getRepositoryToken(BlogCategory), + useClass: Repository, + }, + ], + }).compile(); + + service = module.get(BlogCategoryService); + repository = module.get>(getRepositoryToken(BlogCategory)); + }); + + it('should create a blog category successfully', async () => { + const createBlogCategoryDto: CreateBlogCategoryDto = { + name: 'Tech', + }; + + const blogCategory: BlogCategory = { + id: '1', + name: 'Tech', + } as any; + + jest.spyOn(repository, 'create').mockReturnValue(blogCategory); + jest.spyOn(repository, 'save').mockResolvedValue(blogCategory); + + const result = await service.createOrganisationCategory(createBlogCategoryDto); + + expect(repository.create).toHaveBeenCalledWith(createBlogCategoryDto); + expect(repository.save).toHaveBeenCalledWith(blogCategory); + expect(result).toEqual({ + data: blogCategory, + message: 'Blog category created successfully.', + }); + }); + + it('should throw an error if repository save fails', async () => { + const createBlogCategoryDto: CreateBlogCategoryDto = { + name: 'Tech', + }; + + jest.spyOn(repository, 'create').mockReturnValue({} as BlogCategory); + jest.spyOn(repository, 'save').mockRejectedValue(new Error('Save failed')); + + await expect(service.createOrganisationCategory(createBlogCategoryDto)).rejects.toThrow('Save failed'); + }); +}); diff --git a/src/modules/blog-category/tests/update-blog-category.service.spec.ts b/src/modules/blog-category/tests/update-blog-category.service.spec.ts new file mode 100644 index 000000000..f6f736a66 --- /dev/null +++ b/src/modules/blog-category/tests/update-blog-category.service.spec.ts @@ -0,0 +1,44 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { BlogCategory } from '../entities/blog-category.entity'; +import { CreateBlogCategoryDto } from '../dto/create-blog-category.dto'; +import { BlogCategoryService } from '../blog-category.service'; +import { CustomHttpException } from '../../../helpers/custom-http-filter'; + +describe('BlogCategoryService', () => { + let service: BlogCategoryService; + let repository: Repository; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + BlogCategoryService, + { + provide: getRepositoryToken(BlogCategory), + useClass: Repository, + }, + ], + }).compile(); + + service = module.get(BlogCategoryService); + repository = module.get>(getRepositoryToken(BlogCategory)); + }); + + it('should update an organisation category successfully', async () => { + const category = { id: '1', name: 'Tech' } as any; + const updatedCategory = { id: '1', name: 'Technology' } as any; + + jest.spyOn(repository, 'findOne').mockResolvedValue(category); + jest.spyOn(repository, 'save').mockResolvedValue(updatedCategory); + + const result = await service.updateOrganisationCategory('1', { name: 'Technology' }); + expect(result).toEqual({ data: updatedCategory, message: 'Organisation category updated successfully.' }); + }); + + it('should throw NotFoundException if category does not exist', async () => { + jest.spyOn(repository, 'findOne').mockResolvedValue(null); + + await expect(service.updateOrganisationCategory('1', { name: 'Technology' })).rejects.toThrow(CustomHttpException); + }); +}); diff --git a/src/modules/blogs/blogs.controller.ts b/src/modules/blogs/blogs.controller.ts new file mode 100644 index 000000000..1a4a0f842 --- /dev/null +++ b/src/modules/blogs/blogs.controller.ts @@ -0,0 +1,101 @@ +import { + Controller, + Post, + Body, + UseGuards, + Request, + Get, + Param, + ParseUUIDPipe, + Put, + Delete, + HttpCode, + HttpStatus, + Query, +} from '@nestjs/common'; +import { BlogService } from './blogs.service'; +import { SuperAdminGuard } from '../../guards/super-admin.guard'; +import { CreateBlogDto } from './dtos/create-blog.dto'; +import { BlogResponseDto } from './dtos/blog-response.dto'; +import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; +import { BlogDto } from './dtos/blog.dto'; +import { UpdateBlogDto } from './dtos/update-blog.dto'; +import { UpdateBlogResponseDto } from './dtos/update-blog-response.dto'; +import { BLOG_DELETED } from '../../helpers/SystemMessages'; + +@ApiTags('blogs') +@Controller('/blogs') +export class BlogController { + constructor(private readonly blogService: BlogService) {} + + @ApiBearerAuth() + @Post() + @UseGuards(SuperAdminGuard) + @ApiOperation({ summary: 'Create a new blog' }) + @ApiResponse({ status: 201, description: 'The blog has been successfully created.', type: BlogResponseDto }) + @ApiResponse({ status: 403, description: 'Forbidden.' }) + async createBlog(@Body() createBlogDto: CreateBlogDto, @Request() req): Promise { + return this.blogService.createBlog(createBlogDto, req.user); + } + + @Get() + @ApiOperation({ summary: 'Get all blogs' }) + @ApiResponse({ status: 200, description: 'All blogs fetched successfully.', type: [BlogResponseDto] }) + async getAllBlogs(@Query('page') page: number = 1, @Query('pageSize') pageSize: number = 10): Promise { + const result = await this.blogService.getAllBlogs(page, pageSize); + return result; + } + + @Get('/search') + @ApiOperation({ summary: 'Search and filter blogs' }) + @ApiResponse({ status: 200, description: 'Search results returned successfully.', type: [BlogResponseDto] }) + async searchBlogs(@Query() query: any): Promise { + const result = await this.blogService.searchBlogs(query); + return result; + } + + @ApiBearerAuth() + @Put(':id') + @UseGuards(SuperAdminGuard) + @ApiOperation({ summary: 'Update a blog post by ID' }) + @ApiResponse({ status: 200, description: 'Blog post updated successfully.', type: UpdateBlogResponseDto }) + @ApiResponse({ status: 404, description: 'Blog post not found.' }) + @ApiResponse({ status: 500, description: 'Internal server error.' }) + async updateBlog( + @Param('id') id: string, + @Body() updateBlogDto: UpdateBlogDto, + @Request() req + ): Promise { + const updatedBlog = await this.blogService.updateBlog(id, updateBlogDto, req.user); + + return { + message: 'Blog post updated successfully', + post: updatedBlog, + }; + } + + @Get(':id') + @ApiOperation({ summary: 'Get a single blog' }) + @ApiResponse({ status: 200, description: 'Blog fetched successfully.', type: BlogDto }) + @ApiResponse({ status: 404, description: 'Blog not found.' }) + @ApiResponse({ status: 500, description: 'Internal server error.' }) + async getSingleBlog(@Param('id', new ParseUUIDPipe()) id: string, @Request() req): Promise { + return await this.blogService.getSingleBlog(id, req.user); + } + + @ApiBearerAuth() + @Delete(':id') + @UseGuards(SuperAdminGuard) + @HttpCode(HttpStatus.ACCEPTED) + @ApiOperation({ summary: 'Delete a blog post' }) + @ApiResponse({ status: 202, description: 'Blog successfully deleted.' }) + @ApiResponse({ status: 404, description: 'Blog with the given Id does not exist.' }) + @ApiResponse({ status: 403, description: 'You are not authorized to perform this action.' }) + async deleteBlog(@Param('id', ParseUUIDPipe) id: string): Promise { + await this.blogService.deleteBlogPost(id); + return { + message: BLOG_DELETED, + status_code: HttpStatus.ACCEPTED, + }; + } +} diff --git a/src/modules/blogs/blogs.module.ts b/src/modules/blogs/blogs.module.ts new file mode 100644 index 000000000..90d6c2369 --- /dev/null +++ b/src/modules/blogs/blogs.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { BlogController } from './blogs.controller'; +import { BlogService } from './blogs.service'; +import { Blog } from './entities/blog.entity'; +import { User } from '../user/entities/user.entity'; +import { Role } from '../role/entities/role.entity'; +import { OrganisationUserRole } from '../role/entities/organisation-user-role.entity'; +import { Organisation } from '../organisations/entities/organisations.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Blog, User, Organisation, OrganisationUserRole, Role])], + controllers: [BlogController], + providers: [BlogService], +}) +export class BlogModule {} diff --git a/src/modules/blogs/blogs.service.ts b/src/modules/blogs/blogs.service.ts new file mode 100644 index 000000000..20bbf159b --- /dev/null +++ b/src/modules/blogs/blogs.service.ts @@ -0,0 +1,255 @@ +import * as SYS_MSG from '../../helpers/SystemMessages'; +import { HttpStatus, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, Like, MoreThanOrEqual, FindOptionsWhere } from 'typeorm'; +import { Blog } from './entities/blog.entity'; +import { User } from '../user/entities/user.entity'; +import { CreateBlogDto } from './dtos/create-blog.dto'; +import { UpdateBlogDto } from './dtos/update-blog.dto'; +import { CustomHttpException } from '../../helpers/custom-http-filter'; +import { BlogResponseDto } from './dtos/blog-response.dto'; + +@Injectable() +export class BlogService { + constructor( + @InjectRepository(Blog) + private blogRepository: Repository, + @InjectRepository(User) + private userRepository: Repository + ) {} + + private async fetchUserById(userId: string): Promise { + const user = await this.userRepository.findOne({ + where: { id: userId }, + select: ['first_name', 'last_name'], + }); + + if (!user) { + throw new CustomHttpException('User not found.', HttpStatus.NOT_FOUND); + } + + return user; + } + + async createBlog(createBlogDto: CreateBlogDto, user: User): Promise { + const fullUser = await this.fetchUserById(user.id); + + const blog = this.blogRepository.create({ + ...createBlogDto, + author: fullUser, + }); + + const savedBlog = await this.blogRepository.save(blog); + + return { + blog_id: savedBlog.id, + title: savedBlog.title, + content: savedBlog.content, + tags: savedBlog.tags, + image_urls: savedBlog.image_urls, + author: `${fullUser.first_name} ${fullUser.last_name}`, + created_at: savedBlog.created_at, + }; + } + + async getSingleBlog(blogId: string, user: User): Promise { + const singleBlog = await this.blogRepository.findOneBy({ id: blogId }); + const fullName = await this.fetchUserById(user.id); + + if (!singleBlog) { + throw new CustomHttpException(SYS_MSG.BLOG_NOT_FOUND, HttpStatus.NOT_FOUND); + } + + const { id, created_at, updated_at, ...rest } = singleBlog; + const author = `${fullName.first_name} ${fullName.last_name}`; + + return { + status_code: 200, + message: SYS_MSG.BLOG_FETCHED_SUCCESSFUL, + data: { blog_id: id, ...rest, author, published_date: created_at }, + }; + } + + async updateBlog(id: string, updateBlogDto: UpdateBlogDto, user: User): Promise { + const blog = await this.blogRepository.findOne({ + where: { id }, + relations: ['author'], + }); + + if (!blog) { + throw new CustomHttpException('Blog post not found.', HttpStatus.NOT_FOUND); + } + + const fullUser = await this.fetchUserById(user.id); + + Object.assign(blog, updateBlogDto, { author: fullUser }); + + const updatedBlog = await this.blogRepository.save(blog); + + return { + blog_id: updatedBlog.id, + title: updatedBlog.title, + content: updatedBlog.content, + tags: updatedBlog.tags, + image_urls: updatedBlog.image_urls, + author: `${updatedBlog.author.first_name} ${updatedBlog.author.last_name}`, + created_at: updatedBlog.created_at, + }; + } + async deleteBlogPost(id: string): Promise { + const blog = await this.blogRepository.findOne({ where: { id } }); + if (!blog) { + throw new CustomHttpException('Blog post with this id does not exist.', HttpStatus.NOT_FOUND); + } else await this.blogRepository.remove(blog); + } + + async getAllBlogs( + page: number, + pageSize: number + ): Promise<{ + status_code: number; + message: string; + data: { currentPage: number; totalPages: number; totalResults: number; blogs: BlogResponseDto[]; meta: any }; + }> { + const skip = (page - 1) * pageSize; + + const [result, total] = await this.blogRepository.findAndCount({ + skip, + take: pageSize, + relations: ['author'], + }); + + const data = this.mapBlogResults(result); + const totalPages = Math.ceil(total / pageSize); + + return { + status_code: HttpStatus.OK, + message: SYS_MSG.BLOG_FETCHED_SUCCESSFUL, + data: { + currentPage: page, + totalPages, + totalResults: total, + blogs: data, + meta: { + hasNext: page < totalPages, + total, + nextPage: page < totalPages ? page + 1 : null, + prevPage: page > 1 ? page - 1 : null, + }, + }, + }; + } + + async searchBlogs(query: any): Promise<{ + status_code: number; + message: string; + data: { current_page: number; total_pages: number; total_results: number; blogs: BlogResponseDto[]; meta: any }; + }> { + const { page = 1, page_size = 10 } = query; + const skip = (page - 1) * page_size; + + this.validateEmptyValues(query); + + const where: FindOptionsWhere = this.buildWhereClause(query); + + const [result, total] = await this.blogRepository.findAndCount({ + where: Object.keys(where).length ? where : undefined, + skip, + take: page_size, + relations: ['author'], + }); + + if (!result || result.length === 0) { + return { + status_code: HttpStatus.NOT_FOUND, + message: 'no_results_found_for_the_provided_search_criteria', + data: { + current_page: page, + total_pages: 0, + total_results: 0, + blogs: [], + meta: { + has_next: false, + total: 0, + next_page: null, + prev_page: null, + }, + }, + }; + } + + const data = this.mapBlogResults(result); + const totalPages = Math.ceil(total / page_size); + + return { + status_code: HttpStatus.OK, + message: SYS_MSG.BLOG_FETCHED_SUCCESSFUL, + data: { + current_page: page, + total_pages: totalPages, + total_results: total, + blogs: data, + meta: { + has_next: page < totalPages, + total, + next_page: page < totalPages ? page + 1 : null, + prev_page: page > 1 ? page - 1 : null, + }, + }, + }; + } + + private buildWhereClause(query: any): FindOptionsWhere { + const where: FindOptionsWhere = {}; + + if (query.author !== undefined) { + where.author = { + first_name: Like(`%${query.author}%`), + last_name: Like(`%${query.author}%`), + }; + } + if (query.title !== undefined) { + where.title = Like(`%${query.title}%`); + } + if (query.content !== undefined) { + where.content = Like(`%${query.content}%`); + } + if (query.tags !== undefined) { + where.tags = Like(`%${query.tags}%`); + } + if (query.created_date !== undefined) { + where.created_at = MoreThanOrEqual(new Date(query.created_date)); + } + + return where; + } + + private validateEmptyValues(query: any): void { + for (const key in query) { + if (Object.prototype.hasOwnProperty.call(query, key) && query[key] !== undefined) { + const value = query[key]; + if (typeof value === 'string' && !value.trim()) { + throw new CustomHttpException(`${key.replace(/_/g, ' ')} value is empty`, HttpStatus.BAD_REQUEST); + } + } + } + } + + private mapBlogResults(result: Blog[]): BlogResponseDto[] { + return result.map(blog => { + if (!blog.author) { + throw new CustomHttpException('author_not_found', HttpStatus.INTERNAL_SERVER_ERROR); + } + const author_name = blog.author ? `${blog.author.first_name} ${blog.author.last_name}` : 'Unknown'; + return { + blog_id: blog.id, + title: blog.title, + content: blog.content, + tags: blog.tags, + image_urls: blog.image_urls, + author: author_name, + created_at: blog.created_at, + }; + }); + } +} diff --git a/src/modules/blogs/dtos/blog-response.dto.ts b/src/modules/blogs/dtos/blog-response.dto.ts new file mode 100644 index 000000000..04814b643 --- /dev/null +++ b/src/modules/blogs/dtos/blog-response.dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class BlogResponseDto { + @ApiProperty({ description: 'The ID of the blog' }) + blog_id: string; + + @ApiProperty({ description: 'The title of the blog' }) + title: string; + + @ApiProperty({ description: 'The content of the blog' }) + content: string; + + @ApiProperty({ description: 'The tags associated with the blog', isArray: true, required: false }) + tags?: string[]; + + @ApiProperty({ description: 'The image URLs associated with the blog', isArray: true, required: false }) + image_urls?: string[]; + + @ApiProperty({ description: 'The author of the blog' }) + author: string; + + @ApiProperty({ description: 'The creation date of the blog' }) + created_at: Date; +} diff --git a/src/modules/blogs/dtos/blog.dto.ts b/src/modules/blogs/dtos/blog.dto.ts new file mode 100644 index 000000000..8a7d914af --- /dev/null +++ b/src/modules/blogs/dtos/blog.dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class BlogDto { + @ApiProperty({ description: 'The ID of the blog' }) + blog_id: string; + + @ApiProperty({ description: 'The title of the blog' }) + title: string; + + @ApiProperty({ description: 'The content of the blog' }) + content: string; + + @ApiProperty({ description: 'The tags associated with the blog', required: false }) + tags?: string[]; + + @ApiProperty({ description: 'The image URLs associated with the blog', required: false }) + image_urls?: string[]; + + @ApiProperty({ description: 'The author of the blog' }) + author: string; + + @ApiProperty({ description: 'The creation date of the blog' }) + published_date: Date; +} diff --git a/src/modules/blogs/dtos/create-blog.dto.ts b/src/modules/blogs/dtos/create-blog.dto.ts new file mode 100644 index 000000000..2f5e4ee58 --- /dev/null +++ b/src/modules/blogs/dtos/create-blog.dto.ts @@ -0,0 +1,25 @@ +import { IsNotEmpty, IsOptional, IsArray, IsString, MinLength } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateBlogDto { + @ApiProperty({ description: 'The title of the blog' }) + @IsNotEmpty() + @MinLength(5, { message: 'Title must be at least 5 characters long' }) + title: string; + + @ApiProperty({ description: 'The content of the blog' }) + @IsNotEmpty() + content: string; + + @ApiProperty({ description: 'The tags associated with the blog', required: false }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + tags?: string[]; + + @ApiProperty({ description: 'The image URLs associated with the blog', required: false }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + image_urls?: string[]; +} diff --git a/src/modules/blogs/dtos/update-blog-response.dto.ts b/src/modules/blogs/dtos/update-blog-response.dto.ts new file mode 100644 index 000000000..75ca5ed35 --- /dev/null +++ b/src/modules/blogs/dtos/update-blog-response.dto.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { BlogResponseDto } from './blog-response.dto'; + +export class UpdateBlogResponseDto { + @ApiProperty({ description: 'Success message' }) + message: string; + + @ApiProperty({ description: 'Updated blog details', type: BlogResponseDto }) + post: BlogResponseDto; +} diff --git a/src/modules/blogs/dtos/update-blog.dto.ts b/src/modules/blogs/dtos/update-blog.dto.ts new file mode 100644 index 000000000..f189106c4 --- /dev/null +++ b/src/modules/blogs/dtos/update-blog.dto.ts @@ -0,0 +1,37 @@ +import { IsOptional, IsArray, IsString, IsDateString, MinLength } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class UpdateBlogDto { + @ApiProperty({ description: 'The title of the blog', required: false }) + @IsOptional() + @IsString() + @MinLength(5, { message: 'Title must be at least 5 characters long' }) + title?: string; + + @ApiProperty({ description: 'The content of the blog', required: false }) + @IsOptional() + @IsString() + content?: string; + + @ApiProperty({ description: 'The publish date of the blog', required: false }) + @IsOptional() + @IsDateString() + publish_date?: string; + + @ApiProperty({ description: 'The author of the blog', required: false }) + @IsOptional() + @IsString() + author?: string; + + @ApiProperty({ description: 'The tags associated with the blog', isArray: true, required: false }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + tags?: string[]; + + @ApiProperty({ description: 'The image URLs associated with the blog', isArray: true, required: false }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + image_urls?: string[]; +} diff --git a/src/modules/blogs/entities/blog.entity.ts b/src/modules/blogs/entities/blog.entity.ts new file mode 100644 index 000000000..354292ece --- /dev/null +++ b/src/modules/blogs/entities/blog.entity.ts @@ -0,0 +1,21 @@ +import { Entity, Column, ManyToOne } from 'typeorm'; +import { AbstractBaseEntity } from '../../../entities/base.entity'; +import { User } from '../../user/entities/user.entity'; + +@Entity() +export class Blog extends AbstractBaseEntity { + @Column({ nullable: false }) + title: string; + + @Column('text', { nullable: false }) + content: string; + + @Column('simple-array', { nullable: true }) + tags?: string[]; + + @Column('simple-array', { nullable: true }) + image_urls?: string[]; + + @ManyToOne(() => User, user => user.blogs) + author: User; +} diff --git a/src/modules/blogs/tests/blog.service.update.spec.ts b/src/modules/blogs/tests/blog.service.update.spec.ts new file mode 100644 index 000000000..92e93814f --- /dev/null +++ b/src/modules/blogs/tests/blog.service.update.spec.ts @@ -0,0 +1,149 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { BlogService } from '../blogs.service'; +import { Blog } from '../entities/blog.entity'; +import { User } from '../../user/entities/user.entity'; +import { HttpStatus } from '@nestjs/common'; +import { CustomHttpException } from '../../../helpers/custom-http-filter'; + +describe('BlogService', () => { + let service: BlogService; + let blogRepository: Repository; + let userRepository: Repository; + + const mockUserRepository = () => ({ + findOne: jest.fn(), + }); + + const mockBlogRepository = () => ({ + findOne: jest.fn(), + save: jest.fn(), + }); + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + BlogService, + { provide: getRepositoryToken(Blog), useFactory: mockBlogRepository }, + { provide: getRepositoryToken(User), useFactory: mockUserRepository }, + ], + }).compile(); + + service = module.get(BlogService); + blogRepository = module.get>(getRepositoryToken(Blog)); + userRepository = module.get>(getRepositoryToken(User)); + }); + + describe('updateBlog', () => { + it('should successfully update a blog', async () => { + const updateBlogDto = { + title: 'Updated Blog', + content: 'Updated Content', + tags: ['updated'], + image_urls: ['http://example.com/updated.jpg'], + }; + + const user = new User(); + user.id = 'user-id'; + user.first_name = 'John'; + user.last_name = 'Doe'; + + const fullUser = { first_name: 'John', last_name: 'Doe' }; + + const blog = new Blog(); + blog.id = 'blog-id'; + blog.title = 'Old Title'; + blog.content = 'Old Content'; + blog.tags = ['old']; + blog.image_urls = ['http://example.com/old.jpg']; + blog.author = fullUser as unknown as User; + blog.created_at = new Date(); + blog.updated_at = new Date(); + + const updatedBlog = { + ...blog, + ...updateBlogDto, + author: fullUser, + }; + + const expectedResponse = { + blog_id: 'blog-id', + title: 'Updated Blog', + content: 'Updated Content', + tags: ['updated'], + image_urls: ['http://example.com/updated.jpg'], + author: 'John Doe', + created_at: blog.created_at, + }; + + jest.spyOn(blogRepository, 'findOne').mockResolvedValue(blog); + jest.spyOn(userRepository, 'findOne').mockResolvedValue(fullUser as unknown as User); + jest.spyOn(blogRepository, 'save').mockResolvedValue(updatedBlog as Blog); + + const result = await service.updateBlog('blog-id', updateBlogDto, user); + + expect(result).toEqual(expectedResponse); + expect(blogRepository.findOne).toHaveBeenCalledWith({ + where: { id: 'blog-id' }, + relations: ['author'], + }); + expect(userRepository.findOne).toHaveBeenCalledWith({ + where: { id: user.id }, + select: ['first_name', 'last_name'], + }); + expect(blogRepository.save).toHaveBeenCalledWith({ + ...blog, + ...updateBlogDto, + author: fullUser, + }); + }); + + it('should throw a custom error if blog not found', async () => { + const updateBlogDto = { + title: 'Updated Blog', + content: 'Updated Content', + tags: ['updated'], + image_urls: ['http://example.com/updated.jpg'], + }; + + const user = new User(); + user.id = 'user-id'; + + jest.spyOn(blogRepository, 'findOne').mockResolvedValue(null); + + await expect(service.updateBlog('blog-id', updateBlogDto, user)).rejects.toThrowError( + new CustomHttpException('Blog post not found.', HttpStatus.NOT_FOUND) + ); + }); + + it('should throw a custom error if user not found', async () => { + const updateBlogDto = { + title: 'Updated Blog', + content: 'Updated Content', + tags: ['updated'], + image_urls: ['http://example.com/updated.jpg'], + }; + + const user = new User(); + user.id = 'user-id'; + + const blog = new Blog(); + blog.id = 'blog-id'; + blog.title = 'Old Title'; + blog.content = 'Old Content'; + blog.tags = ['old']; + blog.image_urls = ['http://example.com/old.jpg']; + blog.author = { first_name: 'John', last_name: 'Doe' } as unknown as User; + blog.created_at = new Date(); + blog.updated_at = new Date(); + + jest.spyOn(blogRepository, 'findOne').mockResolvedValue(blog); + jest.spyOn(userRepository, 'findOne').mockResolvedValue(null); + + await expect(service.updateBlog('blog-id', updateBlogDto, user)).rejects.toThrowError( + new CustomHttpException('User not found.', HttpStatus.NOT_FOUND) + ); + }); + }); +}); diff --git a/src/modules/blogs/tests/blogs.service.spec.ts b/src/modules/blogs/tests/blogs.service.spec.ts new file mode 100644 index 000000000..5fcb5d455 --- /dev/null +++ b/src/modules/blogs/tests/blogs.service.spec.ts @@ -0,0 +1,305 @@ +import * as SYS_MSG from '../../../helpers/SystemMessages'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository, Like, MoreThanOrEqual } from 'typeorm'; +import { BlogService } from '../blogs.service'; +import { Blog } from '../entities/blog.entity'; +import { User } from '../../user/entities/user.entity'; + +describe('BlogService', () => { + let service: BlogService; + let blogRepository: Repository; + let userRepository: Repository; + + const mockUserRepository = () => ({ + findOne: jest.fn(), + }); + + const mockBlogRepository = () => ({ + create: jest.fn(), + save: jest.fn(), + findAndCount: jest.fn(), + findOneBy: jest.fn(), + findOne: jest.fn(), + remove: jest.fn(), + }); + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + BlogService, + { provide: getRepositoryToken(Blog), useFactory: mockBlogRepository }, + { provide: getRepositoryToken(User), useFactory: mockUserRepository }, + ], + }).compile(); + + service = module.get(BlogService); + blogRepository = module.get>(getRepositoryToken(Blog)); + userRepository = module.get>(getRepositoryToken(User)); + }); + + describe('createBlog', () => { + it('should successfully create a blog', async () => { + const createBlogDto = { + title: 'Test Blog', + content: 'Test Content', + tags: ['test'], + image_urls: ['http://example.com/image.jpg'], + }; + + const user = new User(); + user.id = 'user-id'; + user.first_name = 'John'; + user.last_name = 'Doe'; + + const fullUser = { first_name: 'John', last_name: 'Doe' }; + + const blog = new Blog(); + blog.id = 'blog-id'; + blog.title = 'Test Blog'; + blog.content = 'Test Content'; + blog.tags = ['test']; + blog.image_urls = ['http://example.com/image.jpg']; + blog.author = fullUser as unknown as User; + blog.created_at = new Date(); + blog.updated_at = new Date(); + + const expectedResponse = { + blog_id: 'blog-id', + title: 'Test Blog', + content: 'Test Content', + tags: ['test'], + image_urls: ['http://example.com/image.jpg'], + author: 'John Doe', + created_at: blog.created_at, + }; + + jest.spyOn(userRepository, 'findOne').mockResolvedValue(fullUser as unknown as User); + jest.spyOn(blogRepository, 'create').mockReturnValue(blog); + jest.spyOn(blogRepository, 'save').mockResolvedValue(blog); + + const result = await service.createBlog(createBlogDto, user); + + expect(result).toEqual(expectedResponse); + expect(userRepository.findOne).toHaveBeenCalledWith({ + where: { id: user.id }, + select: ['first_name', 'last_name'], + }); + expect(blogRepository.create).toHaveBeenCalledWith({ + ...createBlogDto, + author: fullUser, + }); + expect(blogRepository.save).toHaveBeenCalledWith(blog); + }); + + it('should throw an error if user not found', async () => { + const createBlogDto = { + title: 'Test Blog', + content: 'Test Content', + tags: ['test'], + image_urls: ['http://example.com/image.jpg'], + }; + + const user = new User(); + user.id = 'user-id'; + + jest.spyOn(userRepository, 'findOne').mockResolvedValue(null); + + await expect(service.createBlog(createBlogDto, user)).rejects.toThrow('User not found'); + }); + }); + + describe('searchBlogs', () => { + it('should return paginated blog results based on search criteria', async () => { + const query = { + author: 'John', + title: 'Test', + content: 'Content', + tags: 'test', + created_date: '2023-01-01', + page: 1, + page_size: 10, + }; + + const user = new User(); + user.first_name = 'John'; + user.last_name = 'Doe'; + + const blog = new Blog(); + blog.id = 'blog-id'; + blog.title = 'Test Blog'; + blog.content = 'Test Content'; + blog.tags = ['test']; + blog.image_urls = ['http://example.com/image.jpg']; + blog.author = user; + blog.created_at = new Date('2023-01-01'); + blog.updated_at = new Date(); + + const expectedResponse = { + status_code: 200, + message: SYS_MSG.BLOG_FETCHED_SUCCESSFUL, + data: { + current_page: 1, + total_pages: 1, + total_results: 1, + blogs: [ + { + blog_id: 'blog-id', + title: 'Test Blog', + content: 'Test Content', + tags: ['test'], + image_urls: ['http://example.com/image.jpg'], + author: 'John Doe', + created_at: new Date('2023-01-01'), + }, + ], + meta: { + has_next: false, + total: 1, + next_page: null, + prev_page: null, + }, + }, + }; + + jest.spyOn(blogRepository, 'findAndCount').mockResolvedValue([[blog], 1]); + + const result = await service.searchBlogs(query); + + expect(result).toEqual(expectedResponse); + expect(blogRepository.findAndCount).toHaveBeenCalledWith({ + where: { + author: { + first_name: Like('%John%'), + last_name: Like('%John%'), + }, + title: Like('%Test%'), + content: Like('%Content%'), + tags: Like('%test%'), + created_at: MoreThanOrEqual(new Date('2023-01-01')), + }, + skip: 0, + take: 10, + relations: ['author'], + }); + }); + + it('should return an empty response if no results are found', async () => { + const query = { + author: 'NonExistentAuthor', + page: 1, + page_size: 10, + }; + + const expectedResponse = { + status_code: 404, + message: 'no_results_found_for_the_provided_search_criteria', + data: { + current_page: 1, + total_pages: 0, + total_results: 0, + blogs: [], + meta: { + has_next: false, + total: 0, + next_page: null, + prev_page: null, + }, + }, + }; + + jest.spyOn(blogRepository, 'findAndCount').mockResolvedValue([[], 0]); + + const result = await service.searchBlogs(query); + + expect(result).toEqual(expectedResponse); + }); + + it('should validate empty query values and throw an error', async () => { + const query = { + author: '', + page: 1, + page_size: 10, + }; + + await expect(service.searchBlogs(query)).rejects.toThrow('author value is empty'); + }); + }); + + describe('getSingleBlog', () => { + it('should successfully retrieve a blog', async () => { + const user = new User(); + user.id = 'user-id'; + user.first_name = 'John'; + user.last_name = 'Doe'; + + const blogId = 'blog-id'; + const blog = new Blog(); + blog.id = 'blog-id'; + blog.title = 'Test Blog'; + blog.content = 'Test Content'; + blog.tags = ['test']; + blog.image_urls = ['http://example.com/image.jpg']; + blog.created_at = new Date(); + blog.updated_at = new Date(); + + jest.spyOn(blogRepository, 'findOneBy').mockResolvedValue(blog); + jest.spyOn(userRepository, 'findOne').mockResolvedValue(user); + + const result = await service.getSingleBlog(blogId, user); + + expect(result).toEqual({ + status_code: 200, + message: SYS_MSG.BLOG_FETCHED_SUCCESSFUL, + data: { + blog_id: blog.id, + title: blog.title, + content: blog.content, + tags: blog.tags, + image_urls: blog.image_urls, + published_date: blog.created_at, + author: 'John Doe', + }, + }); + expect(blogRepository.findOneBy).toHaveBeenCalledWith({ id: blogId }); + expect(userRepository.findOne).toHaveBeenCalledWith({ + where: { id: user.id }, + select: ['first_name', 'last_name'], + }); + }); + + it('should throw an error if blog not found', async () => { + const blogId = 'non-existent-blog-id'; + const user = new User(); + user.id = 'user-id-is-here'; + user.first_name = 'John'; + user.last_name = 'Doe'; + + jest.spyOn(userRepository, 'findOne').mockResolvedValue(user); + jest.spyOn(blogRepository, 'findOneBy').mockResolvedValue(null); + + await expect(service.getSingleBlog(blogId, user)).rejects.toThrow(SYS_MSG.BLOG_NOT_FOUND); + }); + }); + + describe('deleteBlogPost', () => { + it('should successfully delete a blog post', async () => { + const blog = new Blog(); + blog.id = 'blog-id'; + + jest.spyOn(blogRepository, 'findOne').mockResolvedValue(blog); + jest.spyOn(blogRepository, 'remove').mockResolvedValue(undefined); + + await service.deleteBlogPost('blog-id'); + + expect(blogRepository.findOne).toHaveBeenCalledWith({ where: { id: 'blog-id' } }); + expect(blogRepository.remove).toHaveBeenCalledWith(blog); + }); + + it('should throw a 404 error if blog not found', async () => { + jest.spyOn(blogRepository, 'findOne').mockResolvedValue(null); + + await expect(service.deleteBlogPost('blog-id')).rejects.toThrow('Blog post with this id does not exist'); + }); + }); +}); diff --git a/src/modules/comments/comments.controller.ts b/src/modules/comments/comments.controller.ts new file mode 100644 index 000000000..a0d6f48dd --- /dev/null +++ b/src/modules/comments/comments.controller.ts @@ -0,0 +1,28 @@ +import { Controller, Body, Post, Request, Get, Param } from '@nestjs/common'; +import { CommentsService } from './comments.service'; +import { CreateCommentDto } from './dtos/create-comment.dto'; +import { CommentResponseDto } from './dtos/comment-response.dto'; +import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; + +@ApiBearerAuth() +@ApiTags('Comments') +@Controller('comments') +export class CommentsController { + constructor(private readonly commentsService: CommentsService) {} + @Post('add') + @ApiOperation({ summary: 'Create a new comment' }) + @ApiResponse({ status: 201, description: 'The comment has been successfully created.', type: CommentResponseDto }) + @ApiResponse({ status: 400, description: 'Bad Request.' }) + @ApiResponse({ status: 500, description: 'Internal Server Error.' }) + async addComment(@Body() createCommentDto: CreateCommentDto, @Request() req): Promise { + const { userId } = req.user; + return await this.commentsService.addComment(createCommentDto, userId); + } + + @ApiOperation({ summary: 'Get a comment' }) + @ApiResponse({ status: 200, description: 'The comment has been retrieved successfully.' }) + @Get(':id') + async getAComment(@Param('id') id: string): Promise { + return await this.commentsService.getAComment(id); + } +} diff --git a/src/modules/comments/comments.module.ts b/src/modules/comments/comments.module.ts new file mode 100644 index 000000000..810a87ea2 --- /dev/null +++ b/src/modules/comments/comments.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { CommentsController } from './comments.controller'; +import { CommentsService } from './comments.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Comment } from './entities/comments.entity'; +import { User } from '../user/entities/user.entity'; +import { UserModule } from '../user/user.module'; + +@Module({ + imports: [TypeOrmModule.forFeature([Comment, User]), UserModule], + controllers: [CommentsController], + providers: [CommentsService], +}) +export class CommentsModule {} diff --git a/src/modules/comments/comments.service.ts b/src/modules/comments/comments.service.ts new file mode 100644 index 000000000..cad87ebac --- /dev/null +++ b/src/modules/comments/comments.service.ts @@ -0,0 +1,56 @@ +import { Injectable, HttpStatus } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Comment } from './entities/comments.entity'; +import { CreateCommentDto } from './dtos/create-comment.dto'; +import { User } from '../user/entities/user.entity'; +import { CommentResponseDto } from './dtos/comment-response.dto'; +import { CustomHttpException } from '../../helpers/custom-http-filter'; +@Injectable() +export class CommentsService { + constructor( + @InjectRepository(Comment) + private readonly commentRepository: Repository, + @InjectRepository(User) + private readonly userRepository: Repository + ) {} + + async addComment(createCommentDto: CreateCommentDto, userId: string): Promise { + const { model_id, model_type, comment } = createCommentDto; + + if (!comment || comment.trim().length === 0) { + throw new CustomHttpException('Comment cannot be empty', HttpStatus.BAD_REQUEST); + } + + const user = await this.userRepository.findOne({ where: { id: userId } }); + if (!user) { + throw new CustomHttpException('User not found', HttpStatus.NOT_FOUND); + } + + const commentedBy: string = user.first_name + ' ' + user.last_name; + + const Comment = this.commentRepository.create({ + model_id, + model_type, + comment, + }); + + const loadComment = await this.commentRepository.save(Comment); + return { + message: 'Comment added successfully!', + savedComment: loadComment, + commentedBy, + }; + } + + async getAComment(commentId: string) { + const comment = await this.commentRepository.findOneBy({ id: commentId }); + if (!comment) { + throw new CustomHttpException('Comment not found', HttpStatus.NOT_FOUND); + } + return { + message: 'Comment retrieved successfully', + data: { comment }, + }; + } +} diff --git a/src/modules/comments/dto/add-comment.dto.ts b/src/modules/comments/dto/add-comment.dto.ts new file mode 100644 index 000000000..1e3105387 --- /dev/null +++ b/src/modules/comments/dto/add-comment.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class AddCommentDto { + @ApiProperty({ + type: String, + example: 'I love this product', + description: 'Comment to be added to a product', + }) + @IsNotEmpty() + @IsString() + comment: string; +} diff --git a/src/modules/comments/dtos/comment-response.dto.ts b/src/modules/comments/dtos/comment-response.dto.ts new file mode 100644 index 000000000..b76be078c --- /dev/null +++ b/src/modules/comments/dtos/comment-response.dto.ts @@ -0,0 +1,7 @@ +import { Comment } from '../entities/comments.entity'; + +export class CommentResponseDto { + message: string; + savedComment: Comment; + commentedBy: string; +} diff --git a/src/modules/comments/dtos/create-comment.dto.ts b/src/modules/comments/dtos/create-comment.dto.ts new file mode 100644 index 000000000..c0c0e3d1d --- /dev/null +++ b/src/modules/comments/dtos/create-comment.dto.ts @@ -0,0 +1,19 @@ +import { IsString, IsNotEmpty } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateCommentDto { + @ApiProperty({ description: 'The id of the model creating comment for' }) + @IsString() + @IsNotEmpty() + model_id: string; + + @ApiProperty({ description: 'The type of the model creating comment for' }) + @IsString() + @IsNotEmpty() + model_type: string; + + @ApiProperty({ description: 'The comment to be added' }) + @IsString() + @IsNotEmpty() + comment: string; +} diff --git a/src/modules/comments/entities/comment.entity.ts b/src/modules/comments/entities/comment.entity.ts new file mode 100644 index 000000000..830fd2643 --- /dev/null +++ b/src/modules/comments/entities/comment.entity.ts @@ -0,0 +1,18 @@ +import { AbstractBaseEntity } from '../../../entities/base.entity'; +import { Entity, Column, ManyToOne } from 'typeorm'; +import { User } from '../../user/entities/user.entity'; + +@Entity() +export class Comment extends AbstractBaseEntity { + @Column({ nullable: false }) + model_id: string; + + @Column() + model_type: string; + + @Column() + content: string; + + @ManyToOne(() => User, user => user.comments) + user: User; +} diff --git a/src/modules/comments/entities/comments.entity.ts b/src/modules/comments/entities/comments.entity.ts new file mode 100644 index 000000000..1ecbf0ef7 --- /dev/null +++ b/src/modules/comments/entities/comments.entity.ts @@ -0,0 +1,22 @@ +import { Column, Entity, ManyToOne } from 'typeorm'; +import { AbstractBaseEntity } from '../../../entities/base.entity'; +import { Product } from '../../products/entities/product.entity'; +import { User } from '../../user/entities/user.entity'; + +@Entity() +export class Comment extends AbstractBaseEntity { + @Column({ type: 'text', nullable: false }) + comment: string; + + @ManyToOne(() => Product, product => product.comments, { cascade: true }) + product: Product; + + @ManyToOne(() => User, user => user.comments, { cascade: true }) + user: User; + + @Column({ nullable: false }) + model_id: string; + + @Column() + model_type: string; +} diff --git a/src/modules/comments/tests/comments.service.spec.ts b/src/modules/comments/tests/comments.service.spec.ts new file mode 100644 index 000000000..903b9ab00 --- /dev/null +++ b/src/modules/comments/tests/comments.service.spec.ts @@ -0,0 +1,78 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { CommentsService } from '../comments.service'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Comment } from '../entities/comments.entity'; +import { User } from '../../user/entities/user.entity'; +import { Repository } from 'typeorm'; +import { CustomHttpException } from '../../../helpers/custom-http-filter'; +import { HttpStatus } from '@nestjs/common'; + +const mockCommentRepository = () => ({ + create: jest.fn(), + save: jest.fn(), +}); + +const mockUserRepository = () => ({ + findOne: jest.fn(), +}); + +describe('CommentsService', () => { + let service: CommentsService; + let commentRepository: ReturnType; + let userRepository: ReturnType; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + CommentsService, + { provide: getRepositoryToken(Comment), useFactory: mockCommentRepository }, + { provide: getRepositoryToken(User), useFactory: mockUserRepository }, + ], + }).compile(); + + service = module.get(CommentsService); + commentRepository = module.get(getRepositoryToken(Comment)); + userRepository = module.get(getRepositoryToken(User)); + }); + + describe('addComment', () => { + it('should throw CustomHttpException if comment is empty', async () => { + const createCommentDto = { model_id: '1', model_type: 'post', comment: '' }; + + await expect(service.addComment(createCommentDto, 'user-id')).rejects.toThrow(CustomHttpException); + await expect(service.addComment(createCommentDto, 'user-id')).rejects.toMatchObject({ + message: 'Comment cannot be empty', + status: HttpStatus.BAD_REQUEST, + }); + }); + + it('should throw CustomHttpException if user is not found', async () => { + const createCommentDto = { model_id: '1', model_type: 'post', comment: 'A valid comment' }; + userRepository.findOne.mockResolvedValue(null); + + await expect(service.addComment(createCommentDto, 'user-id')).rejects.toThrow(CustomHttpException); + await expect(service.addComment(createCommentDto, 'user-id')).rejects.toMatchObject({ + message: 'User not found', + status: HttpStatus.NOT_FOUND, + }); + }); + + it('should add a comment successfully', async () => { + const createCommentDto = { model_id: '1', model_type: 'post', comment: 'A valid comment' }; + const mockUser = { id: 'user-id', first_name: 'John', last_name: 'Doe' }; + const mockComment = { id: 'comment-id', model_id: '1', model_type: 'post', comment: 'A valid comment' }; + + userRepository.findOne.mockResolvedValue(mockUser); + commentRepository.create.mockReturnValue(mockComment); + commentRepository.save.mockResolvedValue(mockComment); + + const result = await service.addComment(createCommentDto, 'user-id'); + + expect(result).toEqual({ + message: 'Comment added successfully!', + savedComment: mockComment, + commentedBy: 'John Doe', + }); + }); + }); +}); diff --git a/src/modules/contact-us/contact-us.controller.ts b/src/modules/contact-us/contact-us.controller.ts index d4c41f969..cbb8ef67c 100644 --- a/src/modules/contact-us/contact-us.controller.ts +++ b/src/modules/contact-us/contact-us.controller.ts @@ -1,8 +1,9 @@ -import { Controller, Post, Body, HttpCode } from '@nestjs/common'; +import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common'; import { ContactUsService } from './contact-us.service'; import { CreateContactDto } from '../contact-us/dto/create-contact-us.dto'; -import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { ApiTags } from '@nestjs/swagger'; import { skipAuth } from '../..//helpers/skipAuth'; +import { createContactDocs } from './docs/contact-us-swagger.docs'; @ApiTags('Contact Us') @skipAuth() @@ -10,10 +11,10 @@ import { skipAuth } from '../..//helpers/skipAuth'; export class ContactUsController { constructor(private readonly contactUsService: ContactUsService) {} - @ApiOperation({ summary: 'Post a Contact us Message' }) - @ApiBearerAuth() @Post() - @HttpCode(200) + @skipAuth() + @HttpCode(HttpStatus.CREATED) + @createContactDocs() async createContact(@Body() createContactDto: CreateContactDto) { return this.contactUsService.createContactMessage(createContactDto); } diff --git a/src/modules/contact-us/contact-us.service.ts b/src/modules/contact-us/contact-us.service.ts index 2009cff26..19b2aa1a1 100644 --- a/src/modules/contact-us/contact-us.service.ts +++ b/src/modules/contact-us/contact-us.service.ts @@ -1,9 +1,11 @@ -import { Injectable, InternalServerErrorException } from '@nestjs/common'; +import { HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { CreateContactDto } from '../contact-us/dto/create-contact-us.dto'; import { ContactUs } from './entities/contact-us.entity'; import { MailerService } from '@nestjs-modules/mailer'; +import * as CONTACTHELPER from '../../helpers/contactHelper'; +import * as SYS_MSG from '../../helpers/SystemMessages'; @Injectable() export class ContactUsService { @@ -16,27 +18,22 @@ export class ContactUsService { async createContactMessage(createContactDto: CreateContactDto) { const contact = this.contactRepository.create(createContactDto); await this.contactRepository.save(contact); - - try { - await this.sendEmail(createContactDto); - } catch (error) { - throw new InternalServerErrorException('Failed to send email'); - } - + await this.sendEmail(createContactDto); return { - message: 'Inquiry sent successfully', - status_code: 200, + message: SYS_MSG.INQUIRY_SENT, + status_code: HttpStatus.CREATED, }; } private async sendEmail(contactDto: CreateContactDto) { await this.mailerService.sendMail({ - to: 'amal_salam@yahoo.com', - subject: 'New Contact Inquiry', + to: [contactDto.email, CONTACTHELPER.COMPANYEMAIL], + subject: CONTACTHELPER.SUBJECT, template: 'contact-inquiry', context: { name: contactDto.name, email: contactDto.email, + phonenumber: contactDto.phone, message: contactDto.message, date: new Date().toLocaleString(), }, diff --git a/src/modules/contact-us/docs/contact-us-swagger.docs.ts b/src/modules/contact-us/docs/contact-us-swagger.docs.ts new file mode 100644 index 000000000..4728a5ba6 --- /dev/null +++ b/src/modules/contact-us/docs/contact-us-swagger.docs.ts @@ -0,0 +1,21 @@ +import { applyDecorators, HttpStatus } from '@nestjs/common'; +import { ApiBearerAuth, ApiOperation, ApiProperty, ApiResponse } from '@nestjs/swagger'; +import { CreateContactResponseDto } from '../dto/create-contact-response.dto'; +import { CreateContactErrorDto } from '../dto/create-contact-error.dto'; + +export function createContactDocs() { + return applyDecorators( + ApiBearerAuth(), + ApiOperation({ summary: 'Post a Contact us Message' }), + ApiResponse({ + status: HttpStatus.CREATED, + description: 'Successfully made enquiry.', + type: CreateContactResponseDto, + }), + ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'Invalid input data.', + type: CreateContactErrorDto, + }) + ); +} diff --git a/src/modules/contact-us/dto/create-contact-error.dto.ts b/src/modules/contact-us/dto/create-contact-error.dto.ts new file mode 100644 index 000000000..3d56059bd --- /dev/null +++ b/src/modules/contact-us/dto/create-contact-error.dto.ts @@ -0,0 +1,23 @@ +import { HttpStatus } from '@nestjs/common'; +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateContactErrorDto { + @ApiProperty({ + description: 'HTTP status code of the error response.', + example: HttpStatus.BAD_REQUEST, + }) + status_code: number; + + @ApiProperty({ + description: 'Error message(s) describing the issue. Can be a single string or an array of strings.', + example: ['Name should not be empty', 'Email must be an email', 'Message should not be empty'], + oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], + }) + message: string | string[]; + + @ApiProperty({ + description: 'Error type.', + example: 'Bad Request', + }) + error: string; +} diff --git a/src/modules/contact-us/dto/create-contact-response.dto.ts b/src/modules/contact-us/dto/create-contact-response.dto.ts new file mode 100644 index 000000000..6a4070b0e --- /dev/null +++ b/src/modules/contact-us/dto/create-contact-response.dto.ts @@ -0,0 +1,14 @@ +import { HttpStatus } from '@nestjs/common'; +import { ApiProperty } from '@nestjs/swagger'; +import * as SYS_MSG from '../../../helpers/SystemMessages'; + +export class CreateContactResponseDto { + @ApiProperty({ + description: 'Status code for successfull inquiry.', + example: HttpStatus.CREATED, + }) + status_code: number; + + @ApiProperty({ description: 'Response message for sent enquiry', example: SYS_MSG.INQUIRY_SENT }) + messsage: string; +} diff --git a/src/modules/contact-us/dto/create-contact-us.dto.ts b/src/modules/contact-us/dto/create-contact-us.dto.ts index 453eb8432..f5b3893e7 100644 --- a/src/modules/contact-us/dto/create-contact-us.dto.ts +++ b/src/modules/contact-us/dto/create-contact-us.dto.ts @@ -1,15 +1,19 @@ -import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; +import { IsEmail, IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator'; export class CreateContactDto { - @IsNotEmpty() - @IsString() + @IsNotEmpty({ message: 'Name should not be empty' }) + @IsString({ message: 'Name must be a string' }) name: string; - @IsNotEmpty() - @IsEmail() + @IsNotEmpty({ message: 'Email should not be empty' }) + @IsEmail({}, { message: 'Email must be an email' }) email: string; - @IsNotEmpty() - @IsString() + @IsOptional() + @IsInt() + phone: number; + + @IsNotEmpty({ message: 'Message should not be empty' }) + @IsString({ message: 'Message should not be a string' }) message: string; } diff --git a/src/modules/contact-us/entities/contact-us.entity.ts b/src/modules/contact-us/entities/contact-us.entity.ts index e4c4a7e18..f08d7df26 100644 --- a/src/modules/contact-us/entities/contact-us.entity.ts +++ b/src/modules/contact-us/entities/contact-us.entity.ts @@ -9,6 +9,9 @@ export class ContactUs extends AbstractBaseEntity { @Column('varchar', { nullable: false }) email: string; + @Column('int', { nullable: true }) + phone: number; + @Column('text', { nullable: false }) message: string; diff --git a/src/modules/contact-us/test/contact-us.service.spec.ts b/src/modules/contact-us/test/contact-us.service.spec.ts index 15479c997..a05c011a8 100644 --- a/src/modules/contact-us/test/contact-us.service.spec.ts +++ b/src/modules/contact-us/test/contact-us.service.spec.ts @@ -3,8 +3,9 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { ContactUs } from '../entities/contact-us.entity'; import { MailerService } from '@nestjs-modules/mailer'; import { CreateContactDto } from '../dto/create-contact-us.dto'; -import { InternalServerErrorException } from '@nestjs/common'; import { ContactUsService } from '../contact-us.service'; +import * as SYS_MSG from '../../../helpers/SystemMessages'; +import { HttpStatus } from '@nestjs/common'; describe('ContactUsService', () => { let service: ContactUsService; @@ -38,15 +39,12 @@ describe('ContactUsService', () => { service = module.get(ContactUsService); }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); - describe('createContactMessage', () => { it('should create a contact message and send an email', async () => { const createContactDto: CreateContactDto = { name: 'John Doe', email: 'john@example.com', + phone: 123456789, message: 'Test message', }; @@ -59,21 +57,7 @@ describe('ContactUsService', () => { expect(mockRepository.create).toHaveBeenCalledWith(createContactDto); expect(mockRepository.save).toHaveBeenCalledWith(createContactDto); expect(mockMailerService.sendMail).toHaveBeenCalled(); - expect(result).toEqual({ message: 'Inquiry sent successfully', status_code: 200 }); - }); - - it('should throw InternalServerErrorException when email sending fails', async () => { - const createContactDto: CreateContactDto = { - name: 'John Doe', - email: 'john@example.com', - message: 'Test message', - }; - - mockRepository.create.mockReturnValue(createContactDto); - mockRepository.save.mockResolvedValue(createContactDto); - mockMailerService.sendMail.mockRejectedValue(new Error('Email sending failed')); - - await expect(service.createContactMessage(createContactDto)).rejects.toThrow(InternalServerErrorException); + expect(result).toEqual({ message: SYS_MSG.INQUIRY_SENT, status_code: HttpStatus.CREATED }); }); }); }); diff --git a/src/modules/dashboard/dashboard.controller.ts b/src/modules/dashboard/dashboard.controller.ts new file mode 100644 index 000000000..345fc2010 --- /dev/null +++ b/src/modules/dashboard/dashboard.controller.ts @@ -0,0 +1,52 @@ +import { Controller, Get } from '@nestjs/common'; +import { + ApiBearerAuth, + ApiInternalServerErrorResponse, + ApiOkResponse, + ApiTags, + ApiUnauthorizedResponse, +} from '@nestjs/swagger'; +import { DashboardService } from './dashboard.service'; +import { GetMoMStatisticsDto } from './dto/get-mom-statistics.dto'; +import { SalesStatisticsDto } from './dto/get-sales-statistics.dto'; +import { GetStatisticsDto } from './dto/get-statistics.dto'; + +@ApiTags('Admin Dashboard') +@Controller() +@ApiBearerAuth() +export class DashboardController { + constructor(private readonly dashDashboardService: DashboardService) {} + + @Get('statistics') + @ApiOkResponse({ + description: 'Admin Statistics Fetched', + type: GetStatisticsDto, + }) + @ApiInternalServerErrorResponse({ description: 'Internal Server Error' }) + @ApiUnauthorizedResponse({ description: 'Unauthorized' }) + getStatistics(): Promise { + return this.dashDashboardService.getStatistics(); + } + + @Get('analytics') + @ApiOkResponse({ + description: 'Admin Analytics Fetched', + type: GetMoMStatisticsDto, + }) + @ApiInternalServerErrorResponse({ description: 'Internal Server Error' }) + @ApiUnauthorizedResponse({ description: 'Unauthorized' }) + async getMoMRevenue() { + return this.dashDashboardService.getMoMRevenue(); + } + + @Get('sales') + @ApiOkResponse({ + description: 'Admin Sales Data Fetch LogicIn Progress', + type: SalesStatisticsDto, + }) + @ApiInternalServerErrorResponse({ description: 'Internal Server Error' }) + @ApiUnauthorizedResponse({ description: 'Unauthorized' }) + async getSales(): Promise<{ message: string }> { + return this.dashDashboardService.getSales(); + } +} diff --git a/src/modules/dashboard/dashboard.module.ts b/src/modules/dashboard/dashboard.module.ts new file mode 100644 index 000000000..1136dab72 --- /dev/null +++ b/src/modules/dashboard/dashboard.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { NewsletterSubscription } from '../newsletter-subscription/entities/newsletter-subscription.entity'; +import { DashboardController } from './dashboard.controller'; +import { DashboardService } from './dashboard.service'; +import { Cart } from './entities/cart.entity'; +import { OrderItem } from './entities/order-items.entity'; +import { Order } from './entities/order.entity'; +import { Transaction } from './entities/transaction.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Transaction, Order, OrderItem, Cart, NewsletterSubscription])], + controllers: [DashboardController], + providers: [DashboardService], +}) +export class RevenueModule {} diff --git a/src/modules/dashboard/dashboard.service.spec.ts b/src/modules/dashboard/dashboard.service.spec.ts new file mode 100644 index 000000000..b9015fc44 --- /dev/null +++ b/src/modules/dashboard/dashboard.service.spec.ts @@ -0,0 +1,169 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import * as SYS_MSG from '../../helpers/SystemMessages'; +import { NewsletterSubscription } from '../newsletter-subscription/entities/newsletter-subscription.entity'; +import { DashboardService } from './dashboard.service'; +import { Transaction } from './entities/transaction.entity'; + +describe('DashboardService', () => { + let service: DashboardService; + let transactionRepository: Repository; + let newsletterSubscriptionRepository: Repository; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + DashboardService, + { + provide: getRepositoryToken(Transaction), + useValue: { + createQueryBuilder: jest.fn(), + }, + }, + { + provide: getRepositoryToken(NewsletterSubscription), + useValue: { + findAndCount: jest.fn(), + }, + }, + ], + }).compile(); + + service = module.get(DashboardService); + transactionRepository = module.get>(getRepositoryToken(Transaction)); + newsletterSubscriptionRepository = module.get>( + getRepositoryToken(NewsletterSubscription) + ); + }); + + describe('getPercentageDifference', () => { + it('should return "100.00%" if previous value is 0', () => { + const result = service['getPercentageDifference'](10, 0); + expect(result).toBe('100.00%'); + }); + + it('should return the correct percentage difference', () => { + const result = service['getPercentageDifference'](120, 100); + expect(result).toBe('20.00%'); + }); + }); + + describe('getRevenue', () => { + it('should return revenue data with percentage difference', async () => { + const mockCurrentMonthRevenue = { revenue: '1000' }; + const mockPreviousMonthRevenue = { revenue: '800' }; + + jest.spyOn(transactionRepository, 'createQueryBuilder').mockReturnValueOnce({ + select: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + getRawOne: jest.fn().mockResolvedValue(mockCurrentMonthRevenue), + } as any); + + jest.spyOn(transactionRepository, 'createQueryBuilder').mockReturnValueOnce({ + select: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + getRawOne: jest.fn().mockResolvedValue(mockPreviousMonthRevenue), + } as any); + + const result = await service['getRevenue'](); + expect(result).toEqual({ + message: SYS_MSG.REVENUE_FETCHED_SUCCESSFULLY, + data: { + totalRevenueCurrentMonth: '1000', + totalRevenuePreviousMonth: '800', + revenuePercentChange: '25.00%', + }, + }); + }); + }); + + describe('getSubscriptions', () => { + it('should return subscription counts with percentage difference', async () => { + jest.spyOn(newsletterSubscriptionRepository, 'findAndCount').mockResolvedValueOnce([[], 50]); + jest.spyOn(newsletterSubscriptionRepository, 'findAndCount').mockResolvedValueOnce([[], 40]); + + const result = await service['getSubscriptions'](); + expect(result).toEqual({ + currentMonthSubscriptionCount: 50, + previousMonthSubscriptionCount: 40, + percentageDifference: '25.00%', + }); + }); + }); + + describe('getStatistics', () => { + it('should return dashboard statistics', async () => { + jest.spyOn(service, 'getRevenue').mockResolvedValue({ + message: SYS_MSG.REVENUE_FETCHED_SUCCESSFULLY, + data: { + totalRevenueCurrentMonth: 1000, + totalRevenuePreviousMonth: 800, + revenuePercentChange: '25.00%', + }, + }); + + jest.spyOn(service, 'getSubscriptions').mockResolvedValue({ + currentMonthSubscriptionCount: 50, + previousMonthSubscriptionCount: 40, + percentageDifference: '25.00%', + }); + + const result = await service.getStatistics(); + expect(result).toEqual({ + message: SYS_MSG.DASHBOARD_FETCHED_SUCCESSFULLY, + data: { + revenue: { + current_month: 1000, + previous_month: 800, + percentage_difference: '25.00%', + }, + Subscriptions: { + current_month: 50, + previous_month: 40, + percentage_difference: '25.00%', + }, + orders: { + current_month: 0, + previous_month: 0, + percentage_difference: '0%', + }, + active_users: { + current: 1, + difference_an_hour_ago: 0, + }, + }, + }); + }); + }); + + describe('getMoMRevenue', () => { + it('should return month-over-month revenue data', async () => { + const mockRevenue = { revenue: '500' }; + + jest.spyOn(transactionRepository, 'createQueryBuilder').mockImplementation( + () => + ({ + select: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + getRawOne: jest.fn().mockResolvedValue(mockRevenue), + }) as any + ); + + const result = await service.getMoMRevenue(); + expect(result.message).toBe(SYS_MSG.ANALYTICS_FETCHED_SUCCESSFULLY); + expect(result.data).toHaveProperty('Jan'); + expect(result.data).toHaveProperty('Feb'); + // Add more expectations based on your implementation + }); + }); + + describe('getSales', () => { + it('should return a work in progress message', async () => { + const result = await service.getSales(); + expect(result).toEqual({ + message: SYS_MSG.WORK_IN_PROGRESS, + }); + }); + }); +}); diff --git a/src/modules/dashboard/dashboard.service.ts b/src/modules/dashboard/dashboard.service.ts new file mode 100644 index 000000000..1b5255129 --- /dev/null +++ b/src/modules/dashboard/dashboard.service.ts @@ -0,0 +1,164 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Between, Repository } from 'typeorm'; +import * as SYS_MSG from '../../helpers/SystemMessages'; +import { NewsletterSubscription } from '../newsletter-subscription/entities/newsletter-subscription.entity'; +import { GetRevenueResponseDto } from './dto/get-revenue-response.dto'; +import { GetStatisticsDto } from './dto/get-statistics.dto'; +import { GetSubscriptionCountDto } from './dto/get-subscription-count.dto'; +import { Transaction } from './entities/transaction.entity'; + +@Injectable() +export class DashboardService { + private currentMonth: Date; + private previousMonth: Date; + constructor( + @InjectRepository(Transaction) + private readonly transactionRepository: Repository, + + @InjectRepository(NewsletterSubscription) + private readonly newsletterSubscriptionRepository: Repository + ) { + this.currentMonth = new Date(); + this.previousMonth = new Date(); + + this.previousMonth.setMonth(this.previousMonth.getMonth() - 1); + } + + getPercentageDifference(currentValue: number, previousValue: number): string { + return previousValue === 0 ? '100.00%' : (((currentValue - previousValue) / previousValue) * 100).toFixed(2) + '%'; + } + + async getRevenue(): Promise { + const currentMonthRevenue = await this.transactionRepository + .createQueryBuilder('transaction') + .select('SUM(transaction.amount)', 'revenue') + .where( + 'EXTRACT(MONTH FROM transaction.date::timestamp)= :month AND EXTRACT(YEAR FROM transaction.date::timestamp)= :year', + { + month: this.currentMonth.getMonth() + 1, + year: this.currentMonth.getFullYear(), + } + ) + .getRawOne(); + + const previousMonthRevenue = await this.transactionRepository + .createQueryBuilder('transaction') + .select('SUM(transaction.amount)', 'revenue') + .where( + 'EXTRACT(MONTH FROM transaction.date::timestamp)= :month AND EXTRACT(YEAR FROM transaction.date::timestamp)= :year', + { + month: this.previousMonth.getMonth() + 1, + year: this.previousMonth.getFullYear(), + } + ) + .getRawOne(); + + const previousRevenue = previousMonthRevenue.revenue || 0; + const currentRevenue = currentMonthRevenue.revenue || 0; + + const revenuePercentChange = this.getPercentageDifference(currentRevenue, previousRevenue); + + return { + message: SYS_MSG.REVENUE_FETCHED_SUCCESSFULLY, + data: { + totalRevenueCurrentMonth: currentMonthRevenue.revenue, + totalRevenuePreviousMonth: previousMonthRevenue.revenue, + revenuePercentChange, + }, + }; + } + + async getSubscriptions(): Promise { + const startOfMonth = new Date(this.currentMonth.getFullYear(), this.currentMonth.getMonth(), 1); + const startOfNextMonth = new Date(this.currentMonth.getFullYear(), this.currentMonth.getMonth() + 1, 1); + + const [, currentMonthSubscriptionCount] = await this.newsletterSubscriptionRepository.findAndCount({ + where: { + deletedAt: null, + updated_at: Between(startOfMonth, startOfNextMonth), + }, + }); + + const [, previousMonthSubscriptionCount] = await this.newsletterSubscriptionRepository.findAndCount({ + where: { + deletedAt: null, + updated_at: Between(this.previousMonth, startOfMonth), + }, + }); + + const percentageDifference = this.getPercentageDifference( + currentMonthSubscriptionCount, + previousMonthSubscriptionCount + ); + + return { + currentMonthSubscriptionCount, + previousMonthSubscriptionCount, + percentageDifference, + }; + } + + async getStatistics(): Promise { + const revenueStats = await this.getRevenue(); + const subscriptionsCount = await this.getSubscriptions(); + + // TODO: Implement the logic to return the dashboard metric for Orders and Active users + return { + message: SYS_MSG.DASHBOARD_FETCHED_SUCCESSFULLY, + data: { + revenue: { + current_month: revenueStats.data.totalRevenueCurrentMonth, + previous_month: revenueStats.data.totalRevenuePreviousMonth, + percentage_difference: revenueStats.data.revenuePercentChange, + }, + Subscriptions: { + current_month: subscriptionsCount.currentMonthSubscriptionCount || 0, + previous_month: subscriptionsCount.previousMonthSubscriptionCount || 0, + percentage_difference: subscriptionsCount.percentageDifference || '0%', + }, + orders: { + current_month: 0, + previous_month: 0, + percentage_difference: '0%', + }, + active_users: { + current: 1, + difference_an_hour_ago: 0, + }, + }, + }; + } + + async getMoMRevenue(): Promise { + const year = new Date().getFullYear(); + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + + const revenueData = {}; + + for (let i = 0; i < months.length; i++) { + const month = i + 1; + const monthRevenue = await this.transactionRepository + .createQueryBuilder('transaction') + .select('SUM(transaction.amount)', 'revenue') + .where( + 'EXTRACT(MONTH FROM transaction.date::timestamp) = :month AND EXTRACT(YEAR FROM transaction.date::timestamp) = :year', + { + month, + year, + } + ) + .getRawOne(); + + revenueData[months[i]] = monthRevenue?.revenue || 0; + } + + return { message: SYS_MSG.ANALYTICS_FETCHED_SUCCESSFULLY, data: revenueData }; + } + + async getSales(): Promise<{ message: string }> { + return { + message: SYS_MSG.WORK_IN_PROGRESS, + }; + } +} diff --git a/src/modules/dashboard/dto/get-mom-statistics.dto.ts b/src/modules/dashboard/dto/get-mom-statistics.dto.ts new file mode 100644 index 000000000..77d325e39 --- /dev/null +++ b/src/modules/dashboard/dto/get-mom-statistics.dto.ts @@ -0,0 +1,6 @@ +import { MoMStatsDto } from './mom-stats.dto'; + +export class GetMoMStatisticsDto { + message: string; + data: MoMStatsDto; +} diff --git a/src/modules/dashboard/dto/get-revenue-response.dto.ts b/src/modules/dashboard/dto/get-revenue-response.dto.ts new file mode 100644 index 000000000..e1922ebec --- /dev/null +++ b/src/modules/dashboard/dto/get-revenue-response.dto.ts @@ -0,0 +1,10 @@ +export class GetRevenueResponseDto { + message: string; + data: IResponseData; +} + +interface IResponseData { + totalRevenueCurrentMonth: number; + totalRevenuePreviousMonth: number; + revenuePercentChange: string; +} diff --git a/src/modules/dashboard/dto/get-sales-statistics.dto.ts b/src/modules/dashboard/dto/get-sales-statistics.dto.ts new file mode 100644 index 000000000..d6cb9ac49 --- /dev/null +++ b/src/modules/dashboard/dto/get-sales-statistics.dto.ts @@ -0,0 +1,43 @@ +export class SalesStatisticsDto { + message: string; + status_code: number; + data: Datum[]; +} + +class Datum { + id: string; + user_id: string; + order_number: string; + total_amount: string; + tax: string; + shipping_cost: string; + discount: string; + status: string; + payment_status: string; + payment_method: string; + shipping_address: string; + billing_address: string; + quantity: number; + notes: string; + product_id: string; + created_at: Date; + updated_at: Date; + deleted_at: Date; + user: UserDetails; +} + +class UserDetails { + id: string; + name: string; + email: string; + phone: string; + role: string; + email_verified_at: Date; + is_active: boolean; + is_verified: boolean; + signup_type: string; + social_id: string; + created_at: Date; + updated_at: Date; + deleted_at: Date; +} diff --git a/src/modules/dashboard/dto/get-statistics.dto.ts b/src/modules/dashboard/dto/get-statistics.dto.ts new file mode 100644 index 000000000..88044e220 --- /dev/null +++ b/src/modules/dashboard/dto/get-statistics.dto.ts @@ -0,0 +1,6 @@ +import { GetStatsDataDto } from './get-stats-data.dto'; + +export class GetStatisticsDto { + message: string; + data: GetStatsDataDto; +} diff --git a/src/modules/dashboard/dto/get-stats-data.dto.ts b/src/modules/dashboard/dto/get-stats-data.dto.ts new file mode 100644 index 000000000..63904fefc --- /dev/null +++ b/src/modules/dashboard/dto/get-stats-data.dto.ts @@ -0,0 +1,17 @@ +export class GetStatsDataDto { + revenue: IStatsData; + Subscriptions: IStatsData; + orders: IStatsData; + active_users: IActiveUsersData; +} + +class IStatsData { + current_month: number; + previous_month: number; + percentage_difference: string; +} + +class IActiveUsersData { + current: number; + difference_an_hour_ago: number; +} diff --git a/src/modules/dashboard/dto/get-subscription-count.dto.ts b/src/modules/dashboard/dto/get-subscription-count.dto.ts new file mode 100644 index 000000000..824f4b393 --- /dev/null +++ b/src/modules/dashboard/dto/get-subscription-count.dto.ts @@ -0,0 +1,5 @@ +export class GetSubscriptionCountDto { + currentMonthSubscriptionCount: number; + previousMonthSubscriptionCount: number; + percentageDifference: string; +} diff --git a/src/modules/dashboard/dto/mom-stats.dto.ts b/src/modules/dashboard/dto/mom-stats.dto.ts new file mode 100644 index 000000000..b4973ef8c --- /dev/null +++ b/src/modules/dashboard/dto/mom-stats.dto.ts @@ -0,0 +1,14 @@ +export class MoMStatsDto { + Jan: number; + Feb: number; + Mar: number; + Apr: number; + May: number; + Jun: number; + Jul: number; + Aug: number; + Sep: number; + Oct: number; + Nov: number; + Dec: number; +} diff --git a/src/modules/dashboard/entities/cart.entity.ts b/src/modules/dashboard/entities/cart.entity.ts new file mode 100644 index 000000000..cd06a2edf --- /dev/null +++ b/src/modules/dashboard/entities/cart.entity.ts @@ -0,0 +1,15 @@ +import { Column, Entity, ManyToOne } from 'typeorm'; +import { AbstractBaseEntity } from '../../../entities/base.entity'; +import { Product } from '../../products/entities/product.entity'; +import { User } from '../../user/entities/user.entity'; +@Entity() +export class Cart extends AbstractBaseEntity { + @Column({ type: 'int', nullable: false, default: 0 }) + quantity: number; + + @ManyToOne(() => Product, product => product.cart) + product: Product; + + @ManyToOne(() => User, user => user.cart) + user: User; +} diff --git a/src/modules/dashboard/entities/order-items.entity.ts b/src/modules/dashboard/entities/order-items.entity.ts new file mode 100644 index 000000000..5ad949fd5 --- /dev/null +++ b/src/modules/dashboard/entities/order-items.entity.ts @@ -0,0 +1,19 @@ +import { Column, Entity, ManyToOne } from 'typeorm'; +import { AbstractBaseEntity } from '../../../entities/base.entity'; +import { Product } from '../../products/entities/product.entity'; +import { Order } from './order.entity'; + +@Entity() +export class OrderItem extends AbstractBaseEntity { + @ManyToOne(() => Order, order => order.orderItems) + order: Order; + + @ManyToOne(() => Product, product => product.orderItems) + product: Product; + + @Column({ type: 'int', nullable: false, default: 0 }) + quantity: number; + + @Column({ type: 'float', nullable: false, default: 0 }) + total_price: number; +} diff --git a/src/modules/dashboard/entities/order.entity.ts b/src/modules/dashboard/entities/order.entity.ts new file mode 100644 index 000000000..592eef2e8 --- /dev/null +++ b/src/modules/dashboard/entities/order.entity.ts @@ -0,0 +1,20 @@ +import { Column, Entity, ManyToOne, OneToMany } from 'typeorm'; +import { AbstractBaseEntity } from '../../../entities/base.entity'; +import { User } from '../../user/entities/user.entity'; +import { OrderItem } from './order-items.entity'; +import { Transaction } from './transaction.entity'; + +@Entity() +export class Order extends AbstractBaseEntity { + @Column({ type: 'float', nullable: false, default: 0 }) + total_price: number; + + @ManyToOne(() => User, user => user.orders) + user: User; + + @OneToMany(() => OrderItem, orderItem => orderItem.order) + orderItems: OrderItem[]; + + @OneToMany(() => Transaction, transaction => transaction.order) + transactions: Transaction[]; +} diff --git a/src/modules/dashboard/entities/transaction.entity.ts b/src/modules/dashboard/entities/transaction.entity.ts new file mode 100644 index 000000000..3d590d75f --- /dev/null +++ b/src/modules/dashboard/entities/transaction.entity.ts @@ -0,0 +1,15 @@ +import { Column, Entity, ManyToOne } from 'typeorm'; +import { AbstractBaseEntity } from '../../../entities/base.entity'; +import { Order } from './order.entity'; + +@Entity() +export class Transaction extends AbstractBaseEntity { + @Column({ type: 'float', nullable: false, default: 0 }) + amount: number; + + @Column({ type: 'text', nullable: true }) + date: Date; + + @ManyToOne(() => Order, order => order.transactions) + order: Order; +} diff --git a/src/modules/email/dto/create-template-response.dto.ts b/src/modules/email/dto/create-template-response.dto.ts new file mode 100644 index 000000000..57fedcc28 --- /dev/null +++ b/src/modules/email/dto/create-template-response.dto.ts @@ -0,0 +1,13 @@ +import { HttpStatus } from '@nestjs/common'; +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateTemplateResponseDto { + @ApiProperty({ enum: HttpStatus, description: 'HTTP status code' }) + status_code: HttpStatus; + + @ApiProperty({ description: 'Response message' }) + message: string; + + @ApiProperty({ type: [String], description: 'List of validation errors', required: false }) + validation_errors?: string[]; +} diff --git a/src/modules/email/dto/delete-template-response.dto.ts b/src/modules/email/dto/delete-template-response.dto.ts new file mode 100644 index 000000000..62c9e6c19 --- /dev/null +++ b/src/modules/email/dto/delete-template-response.dto.ts @@ -0,0 +1,10 @@ +import { HttpStatus } from '@nestjs/common'; +import { ApiProperty } from '@nestjs/swagger'; + +export class DeleteTemplateResponseDto { + @ApiProperty({ enum: HttpStatus, description: 'HTTP status code' }) + status_code: HttpStatus; + + @ApiProperty({ description: 'Response message' }) + message: string; +} diff --git a/src/modules/email/dto/email.dto.ts b/src/modules/email/dto/email.dto.ts index c180d6ec6..a8d1b0984 100644 --- a/src/modules/email/dto/email.dto.ts +++ b/src/modules/email/dto/email.dto.ts @@ -1,31 +1,46 @@ import { IsEmail, IsNotEmpty, IsString, IsObject } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; export class SendEmailDto { + @ApiProperty({ description: 'Recipient email address' }) @IsEmail() to: string; + @ApiProperty({ description: 'Email subject' }) @IsNotEmpty() @IsString() subject: string; + @ApiProperty({ description: 'Email template name' }) @IsNotEmpty() @IsString() template: string; + @ApiProperty({ description: 'Context data for the email template', type: Object }) @IsNotEmpty() @IsObject() context: object; } export class createTemplateDto { + @ApiProperty({ description: 'Name of the template' }) @IsString() templateName: string; + @ApiProperty({ description: 'HTML content of the template' }) @IsString() template: string; } +export class UpdateTemplateDto { + @ApiProperty({ description: 'Updated HTML content of the template' }) + @IsString() + @IsNotEmpty() + template: string; +} + export class getTemplateDto { + @ApiProperty({ description: 'Name of the template to retrieve' }) @IsString() templateName: string; } diff --git a/src/modules/email/dto/error-response-dto.ts b/src/modules/email/dto/error-response-dto.ts new file mode 100644 index 000000000..dddf26c4a --- /dev/null +++ b/src/modules/email/dto/error-response-dto.ts @@ -0,0 +1,10 @@ +import { HttpStatus } from '@nestjs/common'; +import { ApiProperty } from '@nestjs/swagger'; + +export class ErrorResponseDto { + @ApiProperty({ enum: HttpStatus, description: 'HTTP status code' }) + status_code: HttpStatus; + + @ApiProperty({ description: 'Response message' }) + message: string; +} diff --git a/src/modules/email/dto/get-all-template-response.dto.ts b/src/modules/email/dto/get-all-template-response.dto.ts new file mode 100644 index 000000000..81de1e9f5 --- /dev/null +++ b/src/modules/email/dto/get-all-template-response.dto.ts @@ -0,0 +1,21 @@ +import { HttpStatus } from '@nestjs/common'; +import { ApiProperty } from '@nestjs/swagger'; + +export class TemplateDto { + @ApiProperty({ description: 'Template name' }) + template_name: string; + + @ApiProperty({ description: 'Template content' }) + content: string; +} + +export class GetAllTemplatesResponseDto { + @ApiProperty({ enum: HttpStatus, description: 'HTTP status code' }) + status_code: HttpStatus; + + @ApiProperty({ description: 'Response message' }) + message: string; + + @ApiProperty({ type: [TemplateDto], description: 'List of templates', required: false }) + templates?: TemplateDto[]; +} diff --git a/src/modules/email/dto/get-template-response.dto.ts b/src/modules/email/dto/get-template-response.dto.ts new file mode 100644 index 000000000..36d876b2b --- /dev/null +++ b/src/modules/email/dto/get-template-response.dto.ts @@ -0,0 +1,13 @@ +import { HttpStatus } from '@nestjs/common'; +import { ApiProperty } from '@nestjs/swagger'; + +export class GetTemplateResponseDto { + @ApiProperty({ enum: HttpStatus, description: 'HTTP status code' }) + status_code: HttpStatus; + + @ApiProperty({ description: 'Response message' }) + message: string; + + @ApiProperty({ description: 'Template content', required: false }) + template?: string; +} diff --git a/src/modules/email/dto/update-template-response.dto.ts b/src/modules/email/dto/update-template-response.dto.ts new file mode 100644 index 000000000..890bc98ab --- /dev/null +++ b/src/modules/email/dto/update-template-response.dto.ts @@ -0,0 +1,21 @@ +import { HttpStatus } from '@nestjs/common'; +import { ApiProperty } from '@nestjs/swagger'; + +export class UpdateTemplateDataDto { + @ApiProperty({ description: 'Template name' }) + name: string; + + @ApiProperty({ description: 'Template content' }) + content: string; +} + +export class UpdateTemplateResponseDto { + @ApiProperty({ enum: HttpStatus, description: 'HTTP status code' }) + status_code: HttpStatus; + + @ApiProperty({ description: 'Response message' }) + message: string; + + @ApiProperty({ type: () => UpdateTemplateDataDto, description: 'Updated template data', required: false }) + data?: UpdateTemplateDataDto; +} diff --git a/src/modules/email/email.consumer.ts b/src/modules/email/email.consumer.ts new file mode 100644 index 000000000..71e55fbf8 --- /dev/null +++ b/src/modules/email/email.consumer.ts @@ -0,0 +1,124 @@ +import { MailerService } from '@nestjs-modules/mailer'; +import { Process, Processor } from '@nestjs/bull'; +import { MailInterface } from './interfaces/MailInterface'; +import { Job } from 'bull'; +import { Logger } from '@nestjs/common'; + +@Processor('emailSending') +export default class EmailQueueConsumer { + private logger = new Logger(EmailQueueConsumer.name); + constructor(private readonly mailerService: MailerService) {} + + @Process('welcome') + async sendWelcomeEmailJob(job: Job) { + try { + const { + data: { mail }, + } = job; + await this.mailerService.sendMail({ + ...mail, + subject: 'Welcome to My App! Confirm your Email', + template: 'welcome', + }); + } catch (sendWelcomeEmailJobError) { + this.logger.error(`EmailQueueConsumer ~ sendWelcomeEmailJobError: ${sendWelcomeEmailJobError}`); + } + } + + @Process('waitlist') + async sendWaitlistEmailJob(job: Job) { + try { + const { + data: { mail }, + } = job; + + await this.mailerService.sendMail({ + ...mail, + subject: 'Waitlist Confirmation', + template: 'waitlist', + }); + } catch (sendWaitlistEmailJobError) { + this.logger.error(`EmailQueueConsumer ~ sendWaitlistEmailJobError: ${sendWaitlistEmailJobError}`); + } + } + + @Process('reset-password') + async sendResetPasswordEmailJob(job: Job) { + try { + const { + data: { mail }, + } = job; + await this.mailerService.sendMail({ + ...mail, + subject: 'Reset Password', + template: 'reset-password', + }); + } catch (sendResetPasswordEmailJobError) { + this.logger.error(`EmailQueueConsumer ~ sendResetPasswordEmailJobError: ${sendResetPasswordEmailJobError}`); + } + } + + @Process('newsletter') + async sendNewsletterEmailJob(job: Job) { + try { + const { + data: { mail }, + } = job; + await this.mailerService.sendMail({ + ...mail, + subject: 'Monthly Newsletter', + template: 'newsletter', + }); + } catch (sendNewsletterEmailJobError) { + this.logger.error(`EmailQueueConsumer ~ sendNewsletterEmailJobError: ${sendNewsletterEmailJobError}`); + } + } + + @Process('register-otp') + async sendTokenEmailJob(job: Job) { + try { + const { + data: { mail }, + } = job; + await this.mailerService.sendMail({ + ...mail, + subject: 'Welcome to My App! Confirm your Email', + template: 'register-otp', + }); + } catch (sendTokenEmailJobError) { + this.logger.error(`EmailQueueConsumer ~ sendTokenEmailJobError: ${sendTokenEmailJobError}`); + } + } + + @Process('in-app-notification') + async sendLoginOtpEmailJob(job: Job) { + try { + const { + data: { mail }, + } = job; + await this.mailerService.sendMail({ + ...mail, + subject: 'Login with OTP', + template: 'login-otp', + }); + } catch (sendLoginOtpEmailJobError) { + this.logger.error(`EmailQueueConsumer ~ sendLoginOtpEmailJobError: ${sendLoginOtpEmailJobError}`); + } + } + + @Process('login-otp') + async sendNotificationMail(job: Job) { + try { + const { + data: { mail }, + } = job; + await this.mailerService.sendMail({ + ...mail, + subject: 'In-App, Notification', + template: 'notification', + }); + } catch (sendLoginOtpEmailJobError) { + this.logger.error(`EmailQueueConsumer ~ sendLoginOtpEmailJobError: ${sendLoginOtpEmailJobError}`); + } + } +} diff --git a/src/modules/email/email.controller.ts b/src/modules/email/email.controller.ts index 5ae10026c..4c9cc57fa 100644 --- a/src/modules/email/email.controller.ts +++ b/src/modules/email/email.controller.ts @@ -1,32 +1,68 @@ -import { Controller, Post, Get, Body, Res } from '@nestjs/common'; +import { Controller, Post, Get, Body, Res, Patch, Param, UseGuards } from '@nestjs/common'; import { Response } from 'express'; import { EmailService } from './email.service'; -import { skipAuth } from '../../helpers/skipAuth'; -import { SendEmailDto, createTemplateDto, getTemplateDto } from './dto/email.dto'; -import { skip } from 'node:test'; +import { SendEmailDto, UpdateTemplateDto, createTemplateDto, getTemplateDto } from './dto/email.dto'; +import { ApiBearerAuth, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { SuperAdminGuard } from '../../guards/super-admin.guard'; +import { GetTemplateResponseDto } from './dto/get-template-response.dto'; +import { CreateTemplateResponseDto } from './dto/create-template-response.dto'; +import { ErrorResponseDto } from './dto/error-response-dto'; +import { UpdateTemplateResponseDto } from './dto/update-template-response.dto'; +import { DeleteTemplateResponseDto } from './dto/delete-template-response.dto'; +import { GetAllTemplatesResponseDto } from './dto/get-all-template-response.dto'; +@ApiTags('Emails') +@ApiBearerAuth() +@UseGuards(SuperAdminGuard) @Controller('email') export class EmailController { constructor(private emailService: EmailService) {} + @ApiOperation({ summary: 'Store a new email template' }) + @ApiResponse({ status: 201, description: 'Template created successfully', type: CreateTemplateResponseDto }) + @ApiResponse({ status: 400, description: 'Invalid HTML format', type: ErrorResponseDto }) @Post('store-template') async storeTemplate(@Body() body: createTemplateDto, @Res() res: Response): Promise { const response = await this.emailService.createTemplate(body); res.status(response.status_code).send(response); } + @ApiOperation({ summary: 'Update an existing email template' }) + @ApiResponse({ status: 200, description: 'Template updated successfully', type: UpdateTemplateResponseDto }) + @ApiResponse({ status: 400, description: 'Invalid HTML format', type: ErrorResponseDto }) + @ApiResponse({ status: 404, description: 'Template not found', type: ErrorResponseDto }) + @ApiParam({ name: 'templateName', required: true, description: 'The name of the template to update' }) + @Patch('update-template/:templateName') + async updateTemplate( + @Param('templateName') name: string, + @Body() body: UpdateTemplateDto, + @Res() res: Response + ): Promise { + const response = await this.emailService.updateTemplate(name, body); + res.status(response.status_code).send(response); + } + + @ApiOperation({ summary: 'Retrieve an email template' }) + @ApiResponse({ status: 200, description: 'Template retrieved successfully', type: GetTemplateResponseDto }) + @ApiResponse({ status: 404, description: 'Template not found', type: ErrorResponseDto }) + @UseGuards(SuperAdminGuard) @Post('get-template') async getTemplate(@Body() body: getTemplateDto, @Res() res: Response): Promise { const response = await this.emailService.getTemplate(body); res.status(response.status_code).send(response); } + @ApiOperation({ summary: 'Delete an email template' }) + @ApiResponse({ status: 200, description: 'Template deleted successfully', type: DeleteTemplateResponseDto }) + @ApiResponse({ status: 404, description: 'Template not found', type: ErrorResponseDto }) @Post('delete-template') async deleteTemplate(@Body() body: getTemplateDto, @Res() res: Response): Promise { - const response = await this.emailService.getTemplate(body); + const response = await this.emailService.deleteTemplate(body); res.status(response.status_code).send(response); } + @ApiOperation({ summary: 'Retrieve all email templates' }) + @ApiResponse({ status: 200, description: 'Templates retrieved successfully', type: GetAllTemplatesResponseDto }) @Get('get-all-templates') async getAllTemplates(@Res() res: Response): Promise { const response = await this.emailService.getAllTemplates(); diff --git a/src/modules/email/email.module.ts b/src/modules/email/email.module.ts index d53f020b6..ae5bbb44b 100644 --- a/src/modules/email/email.module.ts +++ b/src/modules/email/email.module.ts @@ -1,13 +1,28 @@ import { Module } from '@nestjs/common'; import { BullModule } from '@nestjs/bull'; import { EmailService } from './email.service'; +import QueueService from './queue.service'; +import EmailQueueConsumer from './email.consumer'; import { MailerModule } from '@nestjs-modules/mailer'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter'; import { EmailController } from './email.controller'; +import { SuperAdminGuard } from '../../guards/super-admin.guard'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { User } from '../user/entities/user.entity'; +import { Organisation } from '../organisations/entities/organisations.entity'; +import { OrganisationUserRole } from '../role/entities/organisation-user-role.entity'; +import { Profile } from '../profile/entities/profile.entity'; +import { Role } from '../role/entities/role.entity'; @Module({ + providers: [EmailService, QueueService, EmailQueueConsumer], + exports: [EmailService, QueueService], imports: [ + TypeOrmModule.forFeature([User, Organisation, OrganisationUserRole, Profile, Role]), + BullModule.registerQueueAsync({ + name: 'emailSending', + }), MailerModule.forRootAsync({ imports: [ConfigModule], useFactory: async (configService: ConfigService) => ({ @@ -23,7 +38,7 @@ import { EmailController } from './email.controller'; from: `"Team Remote Bingo" <${configService.get('SMTP_USER')}>`, }, template: { - dir: process.cwd() + '/src/modules/email/templates', + dir: process.cwd() + '/src/modules/email/hng-templates', adapter: new HandlebarsAdapter(), options: { strict: true, @@ -34,8 +49,6 @@ import { EmailController } from './email.controller'; }), ConfigModule, ], - providers: [EmailService], controllers: [EmailController], - exports: [EmailService], }) export class EmailModule {} diff --git a/src/modules/email/email.service.spec.ts b/src/modules/email/email.service.spec.ts index 8b07355aa..88b13a3c8 100644 --- a/src/modules/email/email.service.spec.ts +++ b/src/modules/email/email.service.spec.ts @@ -1,77 +1,123 @@ -import { MailerService } from '@nestjs-modules/mailer'; import { Test, TestingModule } from '@nestjs/testing'; import { EmailService } from './email.service'; -import { SendEmailDto, createTemplateDto, getTemplateDto } from './dto/email.dto'; +import { MailerService } from '@nestjs-modules/mailer'; +import QueueService, { MailSender } from './queue.service'; +import { BullModule, getQueueToken } from '@nestjs/bull'; +import { SendEmailDto, createTemplateDto, getTemplateDto, UpdateTemplateDto } from './dto/email.dto'; import * as Handlebars from 'handlebars'; import * as htmlValidator from 'html-validator'; import * as fs from 'fs'; import { HttpStatus } from '@nestjs/common'; - -// Mock module-level functions -jest.mock('./email_storage.service', () => ({ +import { createFile, deleteFile, getFile } from '../../helpers/fileHelpers'; +import { MailInterface } from './interfaces/MailInterface'; +import { CustomHttpException } from '../../helpers/custom-http-filter'; +import { promisify } from 'util'; +import * as SYS_MSG from '../../helpers/SystemMessages'; +import * as util from 'util'; + +jest.mock('../../helpers/fileHelpers', () => ({ createFile: jest.fn(), deleteFile: jest.fn(), getFile: jest.fn(), })); -jest.mock('handlebars'); -jest.mock('html-validator'); -jest.mock('fs', () => ({ - promises: { - readdir: jest.fn(), - readFile: jest.fn(), - }, +jest.spyOn(util, 'promisify').mockImplementation(fn => { + if (fn === fs.writeFile) { + return jest.fn().mockResolvedValue(undefined); + } + return jest.fn().mockResolvedValue(undefined); +}); + +jest.mock('handlebars', () => ({ + compile: jest.fn(() => + jest.fn(() => 'TestHello, World!') + ), })); +jest.mock('html-validator'); +jest.mock('fs', () => { + const originalModule = jest.requireActual('fs'); + return { + ...originalModule, + existsSync: jest.fn(() => true), + writeFile: jest.fn(), + promises: { + exists: jest.fn(), + readdir: jest.fn(), + readFile: jest.fn(), + }, + }; +}); + describe('EmailService', () => { let service: EmailService; + let queueService: QueueService; let mailerService: MailerService; const mockMailerService = { sendMail: jest.fn().mockResolvedValue({}), }; + const mockQueue = { + add: jest.fn().mockResolvedValue({ id: 'mockJobId' }), + }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ EmailService, + QueueService, { provide: MailerService, useValue: mockMailerService, }, + { + provide: getQueueToken('emailSending'), + useValue: mockQueue, + }, ], }).compile(); service = module.get(EmailService); mailerService = module.get(MailerService); + queueService = module.get(QueueService); }); it('should be defined', () => { expect(service).toBeDefined(); }); - it('should send email', async () => { - const emailData = new SendEmailDto(); - emailData.to = 'test@example.com'; - emailData.subject = 'Test Subject'; - emailData.template = 'test-template'; - emailData.context = { key: 'value' }; + it('should send user confirmation email', async () => { + const email = 'test@example.com'; + const url = 'http://example.com'; + const token = '123456'; + const link = `${url}?token=${token}`; + + const mailSender: MailSender = { + variant: 'welcome', + mail: { + to: 'test@example.com', + subject: 'Welcome!', + body: 'Hello and welcome!', + }, + }; - const result = await service.sendEmail(emailData); + const jobMock = { + id: '12345', + }; - expect(mailerService.sendMail).toHaveBeenCalledWith(emailData); - expect(result).toEqual({ - status_code: HttpStatus.OK, - message: 'Email sent successfully', - }); + (mockQueue.add as jest.Mock).mockResolvedValue(jobMock); + + const result = await queueService.sendMail(mailSender); + + expect(mockQueue.add).toHaveBeenCalledWith(mailSender.variant, { mail: mailSender.mail }); + expect(result).toEqual({ jobId: jobMock.id }); }); describe('createTemplate', () => { it('should create a template if HTML is valid', async () => { const templateInfo: createTemplateDto = { templateName: 'test', template: '
' }; - (Handlebars.compile as jest.Mock).mockReturnValue(() => '
'); (htmlValidator as jest.Mock).mockResolvedValue({ messages: [] }); - (require('./email_storage.service').createFile as jest.Mock).mockResolvedValue(Promise.resolve()); + (createFile as jest.Mock).mockResolvedValue(Promise.resolve()); const result = await service.createTemplate(templateInfo); @@ -80,18 +126,12 @@ describe('EmailService', () => { message: 'Template created successfully', validation_errors: [], }); - expect(require('./email_storage.service').createFile).toHaveBeenCalledWith( - './src/modules/email/templates', - 'test.hbs', - '
' - ); + expect(createFile).toHaveBeenCalledWith('./src/modules/email/hng-templates', 'test.hbs', '
'); }); it('should return validation errors if HTML is invalid', async () => { const templateInfo: createTemplateDto = { templateName: 'test', template: '
' }; - (Handlebars.compile as jest.Mock).mockReturnValue(() => '
'); (htmlValidator as jest.Mock).mockResolvedValue({ messages: [{ message: 'Invalid HTML', type: 'error' }] }); - (require('./email_storage.service').createFile as jest.Mock).mockResolvedValue(Promise.resolve()); const result = await service.createTemplate(templateInfo); @@ -104,7 +144,6 @@ describe('EmailService', () => { it('should handle errors during template creation', async () => { const templateInfo: createTemplateDto = { templateName: 'test', template: '
' }; - (Handlebars.compile as jest.Mock).mockReturnValue(() => '
'); (htmlValidator as jest.Mock).mockRejectedValue(new Error('Validation error')); const result = await service.createTemplate(templateInfo); @@ -116,10 +155,77 @@ describe('EmailService', () => { }); }); + describe('updateTemplate', () => { + it('should update the template successfully', async () => { + const templateName = 'testTemplate'; + const templateInfo: UpdateTemplateDto = { + template: 'TestHello, World!', + }; + + const compiledTemplate = 'TestHello, World!'; + (Handlebars.compile as jest.Mock).mockReturnValue(() => compiledTemplate); + const validationResult = { messages: [] }; + + (htmlValidator as jest.Mock).mockResolvedValue(validationResult); + + const fsWriteFileMock = jest.fn().mockResolvedValue(undefined); + + (util.promisify as unknown as jest.Mock).mockReturnValue(fsWriteFileMock); + (fs.existsSync as jest.Mock).mockReturnValue(true); + + const result = await service.updateTemplate(templateName, templateInfo); + + expect(htmlValidator).toHaveBeenCalledWith({ data: compiledTemplate }); + expect(fsWriteFileMock).toHaveBeenCalledWith( + `./src/modules/email/templates/${templateName}.hbs`, + compiledTemplate, + 'utf-8' + ); + expect(result).toEqual({ + status_code: HttpStatus.OK, + message: SYS_MSG.EMAIL_TEMPLATES.TEMPLATE_UPDATED_SUCCESSFULLY, + data: { + name: templateName, + content: compiledTemplate, + }, + }); + }); + + it('should throw an error if HTML validation fails', async () => { + const templateName = 'testTemplate'; + const templateInfo: UpdateTemplateDto = { + template: 'TestHello, World!', + }; + + const compiledTemplate = Handlebars.compile(templateInfo.template)({}); + const validationResult = { + messages: [{ message: 'Invalid HTML', type: 'error' }], + }; + + (htmlValidator as jest.Mock).mockResolvedValue(validationResult); + + await expect(service.updateTemplate(templateName, templateInfo)).rejects.toThrow(CustomHttpException); + + expect(htmlValidator).toHaveBeenCalledWith({ data: compiledTemplate }); + }); + + it('should throw an error if template does not exist', async () => { + const templateName = 'nonExistentTemplate'; + const templateInfo: UpdateTemplateDto = { + template: 'TestHello, World!', + }; + + (fs.existsSync as jest.Mock).mockReturnValue(false); + + await expect(service.updateTemplate(templateName, templateInfo)).rejects.toThrow(CustomHttpException); + }); + }); + describe('getTemplate', () => { it('should return the content of a template', async () => { const templateInfo: getTemplateDto = { templateName: 'test' }; - (require('./email_storage.service').getFile as jest.Mock).mockResolvedValue('template content'); + + (getFile as jest.Mock).mockResolvedValue('template content'); const result = await service.getTemplate(templateInfo); @@ -132,7 +238,7 @@ describe('EmailService', () => { it('should return NOT_FOUND if template does not exist', async () => { const templateInfo: getTemplateDto = { templateName: 'test' }; - (require('./email_storage.service').getFile as jest.Mock).mockRejectedValue(new Error('File not found')); + (getFile as jest.Mock).mockRejectedValue(new Error('File not found')); const result = await service.getTemplate(templateInfo); @@ -146,7 +252,7 @@ describe('EmailService', () => { describe('deleteTemplate', () => { it('should delete a template', async () => { const templateInfo: getTemplateDto = { templateName: 'test' }; - (require('./email_storage.service').deleteFile as jest.Mock).mockResolvedValue(Promise.resolve()); + (deleteFile as jest.Mock).mockResolvedValue(Promise.resolve()); const result = await service.deleteTemplate(templateInfo); @@ -158,7 +264,7 @@ describe('EmailService', () => { it('should return NOT_FOUND if template does not exist', async () => { const templateInfo: getTemplateDto = { templateName: 'test' }; - (require('./email_storage.service').deleteFile as jest.Mock).mockRejectedValue(new Error('File not found')); + (deleteFile as jest.Mock).mockRejectedValue(new Error('File not found')); const result = await service.deleteTemplate(templateInfo); @@ -188,18 +294,29 @@ describe('EmailService', () => { const support_email = 'support@remotebingo.com'; const recipient_name = 'John Doe'; - await service.sendNotificationMail(email, { message, support_email, recipient_name }); - - expect(mailerService.sendMail).toHaveBeenCalledWith({ - to: email, - subject: 'In-App, Notification', - template: 'notification', - context: { - email, - recipient_name, - message, - support_email, + const mailSender: MailSender = { + variant: 'in-app-notification', + mail: { + to: 'test@example.com', + subject: 'n-App, Notification', + context: { + email, + recipient_name, + message, + support_email, + }, }, - }); + }; + + const jobMock = { + id: '12345', + }; + + (mockQueue.add as jest.Mock).mockResolvedValue(jobMock); + + const result = await queueService.sendMail(mailSender); + + expect(mockQueue.add).toHaveBeenCalledWith(mailSender.variant, { mail: mailSender.mail }); + expect(result).toEqual({ jobId: jobMock.id }); }); }); diff --git a/src/modules/email/email.service.ts b/src/modules/email/email.service.ts index 8e11f8016..f272752ba 100644 --- a/src/modules/email/email.service.ts +++ b/src/modules/email/email.service.ts @@ -1,41 +1,116 @@ import { HttpStatus, Injectable } from '@nestjs/common'; -import { SendEmailDto, createTemplateDto, getTemplateDto } from './dto/email.dto'; import { validateOrReject } from 'class-validator'; import * as Handlebars from 'handlebars'; -import { createFile, deleteFile, getFile } from './email_storage.service'; import * as htmlValidator from 'html-validator'; import * as fs from 'fs'; import { promisify } from 'util'; import * as path from 'path'; import { MailerService } from '@nestjs-modules/mailer'; +import { MailInterface } from './interfaces/MailInterface'; +import QueueService from './queue.service'; import { ArticleInterface } from './interface/article.interface'; +import { createTemplateDto, getTemplateDto, UpdateTemplateDto } from './dto/email.dto'; import { IMessageInterface } from './interface/message.interface'; +import { CustomHttpException } from '../../helpers/custom-http-filter'; +import * as SYS_MSG from '../../helpers/SystemMessages'; +import { getFile, createFile, deleteFile } from '../../helpers/fileHelpers'; @Injectable() export class EmailService { - constructor(private readonly mailerService: MailerService) {} + constructor(private readonly mailerService: QueueService) {} - async sendEmail(emailData: SendEmailDto) { - await validateOrReject(emailData); - try { - await this.mailerService.sendMail(emailData); - return { - status_code: HttpStatus.OK, - message: 'Email sent successfully', - }; - } catch (error) { - return { - status_code: HttpStatus.INTERNAL_SERVER_ERROR, - message: 'There was an error sending the email, please try again', - }; - } + async sendUserConfirmationMail(email: string, url: string, token: string) { + const link = `${url}?token=${token}`; + const mailPayload: MailInterface = { + to: email, + context: { + link, + email, + }, + }; + + await this.mailerService.sendMail({ variant: 'welcome', mail: mailPayload }); + } + + async sendUserEmailConfirmationOtp(email: string, otp: string) { + const mailPayload: MailInterface = { + to: email, + context: { + otp, + email, + }, + }; + + await this.mailerService.sendMail({ variant: 'register-otp', mail: mailPayload }); + } + + async sendForgotPasswordMail(email: string, url: string, token: string) { + const link = `${url}?token=${token}`; + const mailPayload: MailInterface = { + to: email, + context: { + link, + email, + }, + }; + + await this.mailerService.sendMail({ variant: 'reset-password', mail: mailPayload }); + } + + async sendWaitListMail(email: string, url: string) { + const mailPayload: MailInterface = { + to: email, + context: { + url, + email, + }, + }; + + await this.mailerService.sendMail({ variant: 'waitlist', mail: mailPayload }); + } + + async sendNewsLetterMail(email: string, articles: ArticleInterface[]) { + const mailPayload: MailInterface = { + to: email, + context: { + email, + articles, + }, + }; + + await this.mailerService.sendMail({ variant: 'newsletter', mail: mailPayload }); + } + + async sendLoginOtp(email: string, token: string) { + const mailPayload: MailInterface = { + to: email, + context: { + email, + token, + }, + }; + + await this.mailerService.sendMail({ variant: 'login-otp', mail: mailPayload }); + } + + async sendNotificationMail(email: string, notificationMail: IMessageInterface) { + const { recipient_name, message, support_email } = notificationMail; + const mailPayload: MailInterface = { + to: email, + context: { + email, + recipient_name, + message, + support_email, + }, + }; + + await this.mailerService.sendMail({ variant: 'in-app-notification', mail: mailPayload }); } async createTemplate(templateInfo: createTemplateDto) { try { - const html = Handlebars.compile(templateInfo.template)({}); - - const validationResult = await htmlValidator({ data: html }); + const validationResult = await htmlValidator({ data: templateInfo.template }); const filteredMessages = validationResult.messages.filter( message => @@ -58,7 +133,11 @@ export class EmailService { } if (response.status_code === HttpStatus.CREATED) { - await createFile('./src/modules/email/templates', `${templateInfo.templateName}.hbs`, html); + await createFile( + './src/modules/email/hng-templates', + `${templateInfo.templateName}.hbs`, + templateInfo.template + ); } return response; @@ -70,10 +149,50 @@ export class EmailService { } } + async updateTemplate(templateName: string, templateInfo: UpdateTemplateDto) { + const html = Handlebars.compile(templateInfo.template)({}); + + const validationResult = await htmlValidator({ data: html }); + + const filteredMessages = validationResult.messages.filter( + message => + !( + (message.message.includes('Trailing slash on void elements has no effect') && message.type === 'info') || + (message.message.includes('Consider adding a “lang” attribute') && message.subType === 'warning') + ) + ); + + if (filteredMessages.length > 0) { + throw new CustomHttpException( + { + message: SYS_MSG.EMAIL_TEMPLATES.INVALID_HTML_FORMAT, + validation_errors: filteredMessages.map(msg => msg.message), + }, + HttpStatus.BAD_REQUEST + ); + } + + const templatePath = `./src/modules/email/templates/${templateName}.hbs`; + + if (!fs.existsSync(templatePath)) { + throw new CustomHttpException(SYS_MSG.EMAIL_TEMPLATES.TEMPLATE_NOT_FOUND, HttpStatus.NOT_FOUND); + } + + await promisify(fs.writeFile)(templatePath, html, 'utf-8'); + + return { + status_code: HttpStatus.OK, + message: SYS_MSG.EMAIL_TEMPLATES.TEMPLATE_UPDATED_SUCCESSFULLY, + data: { + name: templateName, + content: html, + }, + }; + } + async getTemplate(templateInfo: getTemplateDto) { try { - const template = await getFile(`./src/modules/email/templates/${templateInfo.templateName}.hbs`, 'utf-8'); - console.log(template); + const template = await getFile(`./src/modules/email/hng-templates/${templateInfo.templateName}.hbs`, 'utf-8'); return { status_code: HttpStatus.OK, @@ -90,7 +209,7 @@ export class EmailService { async deleteTemplate(templateInfo: getTemplateDto) { try { - await deleteFile(`./src/modules/email/templates/${templateInfo.templateName}.hbs`); + await deleteFile(`./src/modules/email/hng-templates/${templateInfo.templateName}.hbs`); return { status_code: HttpStatus.OK, message: 'Template deleted successfully', @@ -105,7 +224,7 @@ export class EmailService { async getAllTemplates() { try { - const templatesDirectory = './src/modules/email/templates'; + const templatesDirectory = './src/modules/email/hng-templates'; const files = await promisify(fs.readdir)(templatesDirectory); const templates = await Promise.all( @@ -129,26 +248,10 @@ export class EmailService { templates: validTemplates, }; } catch (error) { - console.log(error); return { status_code: HttpStatus.NOT_FOUND, message: 'Template not found', }; } } - - async sendNotificationMail(email: string, notificationMail: IMessageInterface) { - const { recipient_name, message, support_email } = notificationMail; - await this.mailerService.sendMail({ - to: email, - subject: 'In-App, Notification', - template: 'notification', - context: { - email, - recipient_name, - message, - support_email, - }, - }); - } } diff --git a/src/modules/email/hng-templates/Account-Deactivation.hbs b/src/modules/email/hng-templates/Account-Deactivation.hbs new file mode 100644 index 000000000..57a87a07d --- /dev/null +++ b/src/modules/email/hng-templates/Account-Deactivation.hbs @@ -0,0 +1,218 @@ + + + + + Account Deactivation In Two Days + + + + +
+

+ + + + + + + Boilerplate +

+
+ + +
+
+
+ + + + + + + + + +
+

+ Account Deactivation In + Two Days +

+
+
+

Hi {{name}},

+
+

+ We hope this email finds you well. We noticed that you haven't logged into your Boilerplate account for + quite some time. As part of our ongoing efforts to maintain a secure and efficient service, we will be + deactivating inactive accounts. +

+
+
+

+ Your deactivation details: +

+

+ + + + + + Account Email: + {{email}} +

+

+ + + + + + Last Active: + {{lastActiveDate}} + / + {{lastActiveTime}} +

+

+ + + + + + Deactivation Date: + {{deactivationDateDate}} + / + {{deactivationDateTime}} +

+
+

+ To keep your account active, simply log in before the deactivation date. However, if you no longer wish to + use your account, no further action is needed on your part. +

+ Access my Account +
+
+
+

Regards,

+

Boilerplate

+
+
+
+ + + + + \ No newline at end of file diff --git a/src/modules/email/hng-templates/Account-Inactivity-Deactivation.hbs b/src/modules/email/hng-templates/Account-Inactivity-Deactivation.hbs new file mode 100644 index 000000000..a88d04197 --- /dev/null +++ b/src/modules/email/hng-templates/Account-Inactivity-Deactivation.hbs @@ -0,0 +1,223 @@ + + + + + Account Deactivation Due To Inactivity + + + + +
+

+ + + + + + + Boilerplate +

+
+ + +
+
+
+ + + + + + + + + +
+

Account Deactivation Due To Inactivity

+
+
+

Hi {{name}},

+

+ We hope this email finds you well. We wanted to inform you that your Boilerplate account has been deactivated + due to a prolonged period of inactivity. +

+
+
+

+ Your deactivation details: +

+

+ + + + + + Account Email: {{email}} +

+

+ + + + + + Last Active: + {{lastActiveDate}} + / + {{lastActiveTime}} +

+

+ + + + + + Deactivation Date: + {{deactivationDateDate}} + / + {{deactivationDateTime}} +

+
+

+ If you would like to re-activate your account, you can easily do so by contacting our support team via the + details below. +

+

+ Give us a call at + (+234)-456-7890 + or shoot us an email at + support@llaihng.com +

+

We value your membership and would love to have you back.

+
+

Regards,

+

Boilerplate

+
+
+
+
+ + + + + \ No newline at end of file diff --git a/src/modules/email/hng-templates/Activate-Account.hbs b/src/modules/email/hng-templates/Activate-Account.hbs new file mode 100644 index 000000000..5c558449f --- /dev/null +++ b/src/modules/email/hng-templates/Activate-Account.hbs @@ -0,0 +1,236 @@ + + + + + Activate Your Account + + + + +
+

+ + + + + + + Boilerplate. +

+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Activate Your Account

+
+
+

Hi {{name}},

+

+ We recently detected a login attempt to your account from an unfamiliar device. To ensure the security of your + account, we haven't granted access. +

+

+ To activate your account and secure it, please click the button below: +

+
+ Activate Account + +
+

Regards,

+

Boilerplate

+
+
+
+
+ + + + + \ No newline at end of file diff --git a/src/modules/email/hng-templates/Activation-Link.hbs b/src/modules/email/hng-templates/Activation-Link.hbs new file mode 100644 index 000000000..21d1250de --- /dev/null +++ b/src/modules/email/hng-templates/Activation-Link.hbs @@ -0,0 +1,293 @@ + + + + + Activation Link Expired + + + + +
+

+ + + + + + + Boilerplate. +

+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Activation Link Expired

+
+
+

Hi {{name}},

+

+ We noticed that your account activation link has expired. For your security, activation links are only valid + for a specific time period. +

+
+

+ Don’t worry, you can easily request a new activation link by clicking the button below: +

+ Send Another Activation + Link +
+

Regards,

+

Boilerplate

+
+
+
+
+ + + + + \ No newline at end of file diff --git a/src/modules/email/hng-templates/Active-Account.hbs b/src/modules/email/hng-templates/Active-Account.hbs new file mode 100644 index 000000000..73f28919d --- /dev/null +++ b/src/modules/email/hng-templates/Active-Account.hbs @@ -0,0 +1,1148 @@ + + + + + Account Activation + + + + +
+

+ + + + + + + Boilerplate. +

+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Your Account is Now Active!

+
+
+

Hi {{name}},

+

+ Congratulations! Your account with Boilerplate is now active and ready to use. +

+
+

+ We're thrilled to have you as part of our community and look forward to helping you make the most out of + your experience with us. +

+

+ You can now log in and start exploring all the features and benefits we have to offer. +

+ +

Thank you for joining Boilerplate!

+ +
+

Regards,

+

Boilerplate

+
+
+
+
+ + + + + \ No newline at end of file diff --git a/src/modules/email/hng-templates/Email-Complete.hbs b/src/modules/email/hng-templates/Email-Complete.hbs new file mode 100644 index 000000000..51f7f6158 --- /dev/null +++ b/src/modules/email/hng-templates/Email-Complete.hbs @@ -0,0 +1,211 @@ + + + + + Email Confirmed + + + + +
+

+ + + + + + + Boilerplate. +

+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + +
+

Email Confirmed

+
+
+

Hi {{name}},

+

+ We are thrilled to inform you that your email has been successfully verified and confirmed! +

+
+

+ You can now fully enjoy all the features and benefits we offer, including exclusive access to key features, + special discounts, and personalized content. +

+ + Proceed to Account + +
+

Regards,

+

Boilerplate

+
+
+
+
+ + + + + \ No newline at end of file diff --git a/src/modules/email/hng-templates/Email-Verification.hbs b/src/modules/email/hng-templates/Email-Verification.hbs new file mode 100644 index 000000000..0486f1f9e --- /dev/null +++ b/src/modules/email/hng-templates/Email-Verification.hbs @@ -0,0 +1,179 @@ + + + + + Email Verification + + + +
+

+ + + + + + + Boilerplate. +

+
+ +
+
+
+ + + + + + + + + +
+

Email Verification

+
+
+

Hi {{name}},

+

+ Thanks for registering your account with us Boilerplate. Before we get started, we just need to confirm that + this is you. +

+
+

+ This link will expire in 30 minutes after this email has been sent. If you did not make this request, you + can ignore this email. +

+

To verify your email, please click the button below:

+ Verify Account +

+ Or copy this link: + {{link}} +

+
+

Regards,

+

Boilerplate

+
+
+
+
+ + + + + \ No newline at end of file diff --git a/src/modules/email/hng-templates/Invoice.hbs b/src/modules/email/hng-templates/Invoice.hbs new file mode 100644 index 000000000..4c4157614 --- /dev/null +++ b/src/modules/email/hng-templates/Invoice.hbs @@ -0,0 +1,237 @@ + + + + + New Activation Link Sent + + + + +
+

+ + + + + + + Boilerplate. +

+
+ + +
+
+

Invoice

+
+
+

Hi {{name}},

+ +
+

+ We hope you are doing well. Thank you for your recent purchase from Boilerplate. Please find your invoice + attached to this email. +

+
+
+
+

Invoice Details

+
+
+

Invoice Number

+

{{invoiceNumber}}

+
+
+

Date of Issue

+

{{dateOfIssue}}

+
+
+

Due Date

+

{{dueDate}}

+
+
+
+
+

Order Summary

+
+
+

Item(s)

+

{{noOfItems}}

+

${{costOfItems}}

+
+
+

VAT

+

{{vat}}%

+

${{vatCost}}

+
+
+

TOTAL

+

{{totalPercent}}%

+

${{totalAmount}}

+
+
+
+ +
+

Payment Details

+

+ Please ensure to pay into this account within two days from today +

+
+
+

Amount

+

${{totalAmount}}

+
+
+

Payment Method

+

{{paymentMethod}}

+
+
+

Bank Name

+

{{bankName}}

+
+
+

Account Number

+

{{accountNumber}}

+
+
+

Account Name

+

{{accountName}}

+
+
+
+
+
+ + Pay Now +
+

Have any questions about your order?

+

+ Give us a call at + (+234)-456-7890 + or Email us at + support@llaihng.com +

+
+ +
+

Regards,

+

Boilerplate

+
+
+
+ + + + + \ No newline at end of file diff --git a/src/modules/email/hng-templates/New-Activation-Link.hbs b/src/modules/email/hng-templates/New-Activation-Link.hbs new file mode 100644 index 000000000..265015174 --- /dev/null +++ b/src/modules/email/hng-templates/New-Activation-Link.hbs @@ -0,0 +1,212 @@ + + + + + New Activation Link Sent + + + + +
+

+ + + + + + + Boilerplate. +

+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
+

New Activation Link Sent

+
+
+

Hi {{name}},

+

+ We have sent you a new activation link for your Boilerplate account. Please click the button below to activate + your account: +

+ + Send Another Activation + Link + +
+

Regards,

+

Boilerplate

+
+
+
+ + + + + \ No newline at end of file diff --git a/src/modules/email/hng-templates/New-Feature-Announcement.hbs b/src/modules/email/hng-templates/New-Feature-Announcement.hbs new file mode 100644 index 000000000..a3dbade37 --- /dev/null +++ b/src/modules/email/hng-templates/New-Feature-Announcement.hbs @@ -0,0 +1,340 @@ + + + + + New Feature Announcement + + + + + +
+

+ + + + + + + Boilerplate. +

+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Subscription Confirmation

+
+
+

Hi {{name}},

+

+ Your payment was processed successfully. Thank you for subscribing to our Bi-monthly feature! We’re excited to + have you on board. You’ll receive a separate receipt via email. Below are the details of your subscription: +

+ +
    +
  • + + + + Email Address:   + {{email}} +
  • +
  • + + + + Subscription Plan:  + {{subscriptionPlan}} +
  • +
  • + + + + Cost:   + {{cost}} +
  • +
  • + + + + Duration:   + {{duration}} +
  • +
  • + + + + Subscription Start Date:  + {{subscriptionStartDate}} +
  • +
+
+
+ +
+

The estimated expiry date

+

{{expiryDate}}

+
+ +
+
+
+

+ If you have any questions or need further assistance, please don’t hesitate to reach out to our customer + support + team or send a mail to us on + boilerplate@gmail.com. +
We look forward to providing you with an exceptional experience. Thank you for choosing our product! +

+
+ + Unsubscribe +
+

Regards,

+

Boilerplate

+
+
+
+ + + + \ No newline at end of file diff --git a/src/modules/email/hng-templates/Newsletter.hbs b/src/modules/email/hng-templates/Newsletter.hbs new file mode 100644 index 000000000..70022f7ad --- /dev/null +++ b/src/modules/email/hng-templates/Newsletter.hbs @@ -0,0 +1,686 @@ + + + + + Newsletter + + + + + +
+

+ + + + + + + Boilerplate. +

+
+ +
+
+
+ + + + + + + + + +
+

+ Stay Ahead: Exclusive Offer on Cutting-Edge Tech! +

+
+
+

Hi {{name}},

+

+ We’re excited to bring you an exclusive offer that will keep you ahead of the curve! At Boilerplate, we pride + ourselves on providing the latest and greatest in technology, and this time, we have something truly special + for you. +

+

What’s in Store

+
    +
  • + + + + The Ultimate Smartwatch: Experience the future with this state-of-the-art device, designed + to enhance your daily life. +
  • +
  • + + + + The High-Performance Laptop:  + A perfect blend of innovation and performance, ideal for both work and play. +
  • +
  • + + + + The Wireless Noise-Cancelling Headphones:  + Sleek, powerful, and user-friendly, this product is a must-have for any tech enthusiast. +
  • +
+ +

Exclusive Savings

+

+ For a limited time, enjoy 25% off your purchase of any of these groundbreaking products. Simply use the code + TECHSAVVY at checkout to unlock your savings. +

+ +

Why Choose Boilerplate?

+
    +
  • + + + + Cutting-Edge Technology:  + We source the most innovative products to ensure you have access to the best. +
  • +
  • + + + + Unmatched Quality:  + Our products undergo rigorous testing to meet the highest standards. +
  • +
  • + + + + Exceptional Customer Support:   + Our team is always here to assist you with any questions or concerns. +
  • +
+ +

+ Follow + our Blog + for more information. +

+ +

How To Redeem Your Offer

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
    +
      + 1. Visit our website at + https://www.boilerplate.com +
    +
      + 2. Browse our selection of cutting-edge tech. +
    +
      + 3. Add your desired products to the cart. +
    +
      + 4. Enter the code TECHSAVVY at checkout to apply your discount. Don’t miss out on this opportunity to + upgrade your tech and stay ahead of the game. This exclusive offer is valid until 30th July, so act fast! +
    +
+
+ +
+

+ Thank you for being a valued customer. We look forward to helping you enhance your tech experience. Should + you have any questions or concerns during this process, do not hesitate to reach out to us via email at + support@boilerplate.com +

+
+ + Learn More +
+

Regards,

+

Boilerplate

+
+
+
+ + + + \ No newline at end of file diff --git a/src/modules/email/hng-templates/Password-Reset-Complete-Template.hbs b/src/modules/email/hng-templates/Password-Reset-Complete-Template.hbs new file mode 100644 index 000000000..08fdc1bee --- /dev/null +++ b/src/modules/email/hng-templates/Password-Reset-Complete-Template.hbs @@ -0,0 +1,188 @@ + + + + + Password Reset Complete + + + + +
+

+ + + + + + + Boilerplate. +

+
+ + +
+
+
+ + + + + + + + + +
+

Password Reset Complete

+
+
+

Hi {{name}},

+

+ The password for your Boilerplate account has been successfully changed. You can now continue to access your + account as usual. +

+
+

+ If this wasn't done by you, please immediately reset the password to your Boilerplate account by following + the steps below: +

+
+
+

+ 1. Here's your OTP: +

{{otp}}

+

+
+

+ 2. Review your phone numbers and email addresses and remove the ones that don’t belong to you once you + gain access to your account. +

+
+
+
+

Regards,

+

Boilerplate

+
+
+
+ + + + + \ No newline at end of file diff --git a/src/modules/email/hng-templates/Payment-receipt.hbs b/src/modules/email/hng-templates/Payment-receipt.hbs new file mode 100644 index 000000000..f066dbcce --- /dev/null +++ b/src/modules/email/hng-templates/Payment-receipt.hbs @@ -0,0 +1,329 @@ + + + + + Payment receipt + + + + + +
+

+ + + + + + + Boilerplate. +

+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+

We have received your payment of

+

${{paymentAmount}}

+
+ +
+

Order Summary

+ + + + + + + + + + + + + + + + +
Item{{itemQuantity}}${{itemPrice}}
VAT{{vatPercent}}%${{vatPrice}}
TOTAL{{totalPercent}}%${{totalPrice}}
+
+
+

Payment Details

+ + + + + + + + + + + + + + + + + +
Amount${{totalAmount}}
Payment MethodMastercard ending in {{cardEnding}}
Reference ID{{referenceId}}
Date{{date}}
+
+
+ +
+
+

Have any questions about your order?

+
+

+ Give us a call at + (+234)-456-7890 + or Email us at + support@llaihng.com +

+
+
+

Regards,

+

Boilerplate

+
+
+
+
+ + + + + \ No newline at end of file diff --git a/src/modules/email/hng-templates/Reset-Password-Template.hbs b/src/modules/email/hng-templates/Reset-Password-Template.hbs new file mode 100644 index 000000000..4e3a10b93 --- /dev/null +++ b/src/modules/email/hng-templates/Reset-Password-Template.hbs @@ -0,0 +1,294 @@ + + + + + Reset Your Password + + +
+

+ + + + + + + Boilerplate. +

+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Reset Your Password

+
+
+

Hi {{name}},

+

+ You recently requested to reset your password. If you did not make this request, you can ignore this email. +

+

+ To reset your password, please click the button below. +

+ Reset Password +
+

Regards,

+

Boilerplate

+
+
+
+ + + \ No newline at end of file diff --git a/src/modules/email/hng-templates/Subscription-Cancellation-Confirmation.hbs b/src/modules/email/hng-templates/Subscription-Cancellation-Confirmation.hbs new file mode 100644 index 000000000..f29c6adab --- /dev/null +++ b/src/modules/email/hng-templates/Subscription-Cancellation-Confirmation.hbs @@ -0,0 +1,322 @@ + + + + + Subscription Cancellation Confirmation + + + + + +
+

+ + + + + + + Boilerplate. +

+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Subscription Cancellation Confirmation

+
+
+

Hi {{name}},

+

+ We regret to inform you that your subscription to Bi-monthly features has been cancelled. We appreciate your + support and hope you’ve enjoyed our services during your subscription period. +

+ +
    +
  • + + + + Subscription ID:  + {{subscriptionId}} +
  • +
  • + + + + Subscription Plan:  + {{subscriptionPlan}} +
  • +
  • + + + + Cancellation Date:  + {{cancellationDate}} +
  • +
+ +
+

+ If you have any questions or need further assistance, please don’t hesitate to reach out to our customer + support + team or send a mail to us on + boilerplate@gmail.com. Thank you for being a + part of our community. We hope to serve you again in the future! +

+
+ + Proceed to Account +
+

Regards,

+

Boilerplate

+
+
+
+ + + + \ No newline at end of file diff --git a/src/modules/email/hng-templates/Subscription-Confirmation.hbs b/src/modules/email/hng-templates/Subscription-Confirmation.hbs new file mode 100644 index 000000000..78009c01e --- /dev/null +++ b/src/modules/email/hng-templates/Subscription-Confirmation.hbs @@ -0,0 +1,272 @@ + + + + + New Feature Announcement + + + + + +
+

+ + + + + + + Boilerplate. +

+
+ +
+
+
+ + + + + + + + + + + + + + + + + + +
+

+ Introducing Our Latest Feature: +
+ Boilerplate Pro +

+
+
+

Hi {{name}},

+

+ We’re thrilled to announce the launch of our newest feature: Boilerplate Pro!   Boilerplate Pro is designed to + help you create shared spaces for collaboration. Invite team members, share files, and work together + seamlessly. From project planning to brainstorming sessions, collaborative spaces foster productivity.. + Whether you’re a seasoned user or just getting started, this enhancement will transform your experience. +

+ +

Benefits of Boilerplate Pro:

+
    +
  • + + + + Efficient Team Collaboration: Collaborative Spaces allow you to create dedicated areas for teamwork. Whether + it’s a project, brainstorming session, or ongoing discussion, team members can collaborate seamlessly within + these spaces. +
  • +
  • + + + + Improved Accountability: With Collaborative Spaces, accountability becomes clearer. Each team member’s + contributions are visible within the shared space. +
  • +
+
+
+ +
+

Boilerplate Pro is now live!

+

You can start using it immediately.

+
+ +
+
+
+

+ Want to explore all the details? Click the button below to dive into our comprehensive guide +

+
+ + Learn More +
+

Regards,

+

Boilerplate

+
+
+
+ + + + \ No newline at end of file diff --git a/src/modules/email/hng-templates/Subscription-Renewal-Disabled.hbs b/src/modules/email/hng-templates/Subscription-Renewal-Disabled.hbs new file mode 100644 index 000000000..2bd05f610 --- /dev/null +++ b/src/modules/email/hng-templates/Subscription-Renewal-Disabled.hbs @@ -0,0 +1,334 @@ + + + + + Subscription Renewal Disabled + + + + + +
+

+ + + + + + + Boilerplate. +

+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Subscription Renewal Disabled

+
+
+

Hi {{name}},

+

+ As requested, your next subscription renewal for + {{subscriptionPlan}} + has been disabled. You will continue to enjoy benefits of this subscription until + {{date}}. +

+

+ We are so sad to see you go. However, if you change your mind, you can always reactivate your subscription or + upgrade your subscription plan. +

+ +
+

Regards,

+

Boilerplate

+
+
+

+ If you have questions, please visit our + FAQs, or email us at + help@boilerplate.com. Our team can answer + questions about your subscription status. To unsubscribe from future subscription renewal reminders, + click here. +

+
+
+
+ + + + \ No newline at end of file diff --git a/src/modules/email/hng-templates/Subscription-Renewal-Failed.hbs b/src/modules/email/hng-templates/Subscription-Renewal-Failed.hbs new file mode 100644 index 000000000..0d7fcdf15 --- /dev/null +++ b/src/modules/email/hng-templates/Subscription-Renewal-Failed.hbs @@ -0,0 +1,356 @@ + + + + + Subscription Renewal Failed + + + + +
+

+ + + + + + + Boilerplate. +

+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Subscription Renewal Failed

+
+
+

Hi {{name}},

+

+ We are having some trouble processing your subscription renewal payment for your + {{subscriptionPlan}}. This could be because of either of the following reasons: +

+
    +
  • + + + + Your payment card has been blocked by your bank. +
  • +
  • + + + + Your payment card has expired. +
  • +
  • + + + + You have insufficient funds in your account. +
  • +
+ +

+ To keep enjoying your + {{subscriptionPlan}}, please check your bank or update your payment details. +

+ Update Payment Details +
+

Regards,

+

Boilerplate

+
+
+

+ If you have questions, please visit our + FAQs, or email us at + help@boilerplate.com. Our team can answer + questions about your subscription status. To unsubscribe from future subscription renewal reminders, + click here. +

+
+
+
+ + + + \ No newline at end of file diff --git a/src/modules/email/hng-templates/Subscription-Renewal-Reminder.hbs b/src/modules/email/hng-templates/Subscription-Renewal-Reminder.hbs new file mode 100644 index 000000000..505fe604f --- /dev/null +++ b/src/modules/email/hng-templates/Subscription-Renewal-Reminder.hbs @@ -0,0 +1,742 @@ + + + + + Subscription Renewal Reminder + + + + + +
+

+ + + + + + + Boilerplate. +

+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Subscription Renewal Reminder

+
+
+

Hi {{name}},

+

+ We hope you are enjoying your subscription, which will renew soon. +

+
+
+ +
+

Your Renewal Date

+

{{date}}

+
+ +
+
+
+

+ Your subscription for + ${{subscriptionAmount}}/{{subscriptionPlan}} + will automatically renew on + {{date}}. To avoid being charged, you should cancel at least a day before the renewal date. To learn more or + cancel, + review subscription. +

+
+

+ To keep your subscription, you can renew your plan for the next month. +

+
+ + Renew Subscription +
+

Regards,

+

Boilerplate

+
+ +
+

+ If you have questions, please visit our + FAQs, or email us at + help@boilerplate.com. Our team can answer + questions about your subscription status. To unsubscribe from future subscription renewal reminders, + click here. +

+
+
+
+ + + + \ No newline at end of file diff --git a/src/modules/email/hng-templates/Welcome-Template.hbs b/src/modules/email/hng-templates/Welcome-Template.hbs new file mode 100644 index 000000000..eee600e7e --- /dev/null +++ b/src/modules/email/hng-templates/Welcome-Template.hbs @@ -0,0 +1,216 @@ + + + + + Welcome to HNG Boilerplate + + +
+

+ + + + + + + Boilerplate. +

+
+ +
+
+
+ + + + + + + + + +
+

Welcome to Boilerplate

+

Thanks for signing up

+
+
+

Hi {{name}},

+

+ We're thrilled to have you join us. Experience quality and innovation like never before. Our product is made + to fit your needs and make your life easier. +

+
+

+ Here’s what you can look forward to +

+
+

+ + + + + + Exclusive Offers: + Enjoy special promotions and discounts available only to our members. + +

+

+ + + + + + Exclusive Offers: + Enjoy special promotions and discounts available only to our members. +

+

+ + + + + Exclusive Offers: + Enjoy special promotions and discounts available only to our members. +

+
+
+ Learn More About Us +
+

Regards,

+

Boilerplate

+
+
+
+ + + \ No newline at end of file diff --git a/src/modules/email/hng-templates/contact-inquiry.hbs b/src/modules/email/hng-templates/contact-inquiry.hbs new file mode 100644 index 000000000..c5dd275c8 --- /dev/null +++ b/src/modules/email/hng-templates/contact-inquiry.hbs @@ -0,0 +1,56 @@ + + + + + Contact Inquiry + + + +
+ +
+

Contact Inquiry Received

+
+ + +
+

Hello,

+

+ You have received a new contact inquiry from your website. Here are the details: +

+

+ Name: + {{name}}
+ Email: + {{email}}
+ Phone Number: + {{phonenumber}}
+ Message: + {{message}}
+ Date: + {{date}} +

+

+ Please follow up with the inquirer as soon as possible. If you have any questions, feel free to reach out to + the support team. +

+
+ + +
+

+ Thank you for using our service.
+ © Your Company Name. All rights reserved. +

+
+
+ + \ No newline at end of file diff --git a/src/modules/email/hng-templates/index.hbs b/src/modules/email/hng-templates/index.hbs new file mode 100644 index 000000000..702cb8e4e --- /dev/null +++ b/src/modules/email/hng-templates/index.hbs @@ -0,0 +1,106 @@ + + + + + HNG Boilerplate Email Templates + + + +
+

HNG Boilerplate Email Templates

+
+
+
+

About the Project

+

+ This project contains a collection of boilerplate email templates designed for various use cases within the + HNG 12 boilerplate project. These templates facilitate the seamless integration of email functionality for + tasks such as account activation, password reset, subscription management, and more. +

+

+ Below you will find links to each of the email templates included in this project. Click on a link to view the + corresponding template. +

+
+
+

Email Templates

+ +
+
+
+

+ © + + HNG. All rights reserved. +

+
+ + \ No newline at end of file diff --git a/src/modules/email/hng-templates/login-otp.hbs b/src/modules/email/hng-templates/login-otp.hbs new file mode 100644 index 000000000..3dd04aea2 --- /dev/null +++ b/src/modules/email/hng-templates/login-otp.hbs @@ -0,0 +1,21 @@ +Email Confirmation
Company Logo

Confirm Your Email Address

Hello {{email}}

Thank you for + registering. Please confirm your email address by entering the otp: + {{otp}}

If you didn't create an account with us, you can safely ignore this email.

\ No newline at end of file diff --git a/src/modules/email/hng-templates/waitlist-confirmation.hbs b/src/modules/email/hng-templates/waitlist-confirmation.hbs new file mode 100644 index 000000000..678aad54e --- /dev/null +++ b/src/modules/email/hng-templates/waitlist-confirmation.hbs @@ -0,0 +1,22 @@ + + + + + + Waitlist Confirmation + + + + +
+

Waitlist Confirmation

+

Hello {{recipientName}},

+

Thank you for signing up for our waitlist! We will notify you once you are selected.

+
+ + + \ No newline at end of file diff --git a/src/modules/email/interfaces/MailInterface.ts b/src/modules/email/interfaces/MailInterface.ts new file mode 100644 index 000000000..79cdfb4d4 --- /dev/null +++ b/src/modules/email/interfaces/MailInterface.ts @@ -0,0 +1,13 @@ +export interface MailInterface { + from?: string; + + to: string; + + subject?: string; + + text?: string; + + context?: any; + + [key: string]: any; +} diff --git a/src/modules/email/queue.service.ts b/src/modules/email/queue.service.ts new file mode 100644 index 000000000..593ab11a1 --- /dev/null +++ b/src/modules/email/queue.service.ts @@ -0,0 +1,29 @@ +import { InjectQueue } from '@nestjs/bull'; +import { Queue } from 'bull'; +import { MailInterface } from './interfaces/MailInterface'; +import { Injectable } from '@nestjs/common'; + +Injectable(); +export default class QueueService { + constructor( + @InjectQueue('emailSending') + private readonly emailQueue: Queue + ) {} + + async sendMail({ variant, mail }: MailSender) { + const mailJob = await this.emailQueue.add(variant, { mail }); + return { jobId: mailJob.id }; + } +} + +export interface MailSender { + mail: MailInterface; + variant: + | 'welcome' + | 'waitlist' + | 'newsletter' + | 'reset-password' + | 'login-otp' + | 'register-otp' + | 'in-app-notification'; +} diff --git a/src/modules/email/templates/confirmation.hbs b/src/modules/email/templates/confirmation.hbs deleted file mode 100644 index 3b0e92082..000000000 --- a/src/modules/email/templates/confirmation.hbs +++ /dev/null @@ -1,40 +0,0 @@ - - - - - Email Confirmation - - - -
-
- Company Logo -
-
-

Confirm Your Email Address

-

Hello {{email}},

-

Thank you for registering. Please confirm your email address by clicking the button below:

- Confirm Email -

If you didn't create an account with us, you can safely ignore this email.

-
- -
- - \ No newline at end of file diff --git a/src/modules/email/templates/contact-inquiry.hbs b/src/modules/email/templates/contact-inquiry.hbs deleted file mode 100644 index 9988a8daf..000000000 --- a/src/modules/email/templates/contact-inquiry.hbs +++ /dev/null @@ -1,23 +0,0 @@ - - - - New Contact Inquiry - - - -
-

New Contact Inquiry

-
-

Name: {{name}}

-

Email: {{email}}

-

Message:

-

{{message}}

-
-

This inquiry was received on {{date}} via the contact form.

-
- - \ No newline at end of file diff --git a/src/modules/email/templates/newsletter.hbs b/src/modules/email/templates/newsletter.hbs deleted file mode 100644 index 7e1c5f5b4..000000000 --- a/src/modules/email/templates/newsletter.hbs +++ /dev/null @@ -1,43 +0,0 @@ - - - - - Newsletter - - - -
-
- Company Logo -
-
-

Monthly Newsletter

-

Hello {{email}},

- {{#each articles}} -
-

{{this.title}}

-

{{this.description}} Read more

-
- {{/each}} -
- -
- - \ No newline at end of file diff --git a/src/modules/email/templates/notification.hbs b/src/modules/email/templates/notification.hbs deleted file mode 100644 index a8a72cc8a..000000000 --- a/src/modules/email/templates/notification.hbs +++ /dev/null @@ -1,32 +0,0 @@ - - - - - -
-
- Logo -

Remote Bingo App Notification

-
-
-

Hello {{recipient_name}},

-
-

{{message}}

-
- -
- - \ No newline at end of file diff --git a/src/modules/email/templates/organization-invitation.hbs b/src/modules/email/templates/organization-invitation.hbs new file mode 100644 index 000000000..0ca51e543 --- /dev/null +++ b/src/modules/email/templates/organization-invitation.hbs @@ -0,0 +1,20 @@ + + + + + +
+
+

Welcome to {{organizationName}}

+
+

Hi {{recipientName}},

+

We’re excited to have you at {{organizationName}}!

+

Join Now

+

Best,
The {{organizationName}} Team

+
+ + \ No newline at end of file diff --git a/src/modules/email/templates/register-otp.hbs b/src/modules/email/templates/register-otp.hbs deleted file mode 100644 index 78e6c3aa9..000000000 --- a/src/modules/email/templates/register-otp.hbs +++ /dev/null @@ -1,40 +0,0 @@ - - - - - Email Confirmation - - - -
-
- Company Logo -
-
-

Confirm Your Email Address

-

Hello {{email}},

-

Thank you for registering. Please confirm your email address by entering the otp: - {{otp}}

-

If you didn't create an account with us, you can safely ignore this email.

-
- -
- - \ No newline at end of file diff --git a/src/modules/email/templates/reset-password.hbs b/src/modules/email/templates/reset-password.hbs deleted file mode 100644 index e7a5b49d2..000000000 --- a/src/modules/email/templates/reset-password.hbs +++ /dev/null @@ -1,39 +0,0 @@ - - - - - Forgot Password - - - -
-
- Company Logo -
-
-

Forgot Password

-

Hello {{email}},

-

It seems like you forgot your password. use this otp to reset your password: {{token}}

-

If you didn't request a password reset, please ignore this email.

-
- -
- - \ No newline at end of file diff --git a/src/modules/email/templates/waitlist.hbs b/src/modules/email/templates/waitlist.hbs deleted file mode 100644 index 22d0e98bd..000000000 --- a/src/modules/email/templates/waitlist.hbs +++ /dev/null @@ -1,39 +0,0 @@ - - - - - Waitlist Confirmation - - - -
-
- Company Logo -
-
-

You're on the Waitlist!

-

Hello {{email}},

-

Thank you for your interest! You've been added to our waitlist. We'll notify you as soon as we launch.

- Visit Our Website -
- -
- - \ No newline at end of file diff --git a/src/modules/faq/faq.controller.ts b/src/modules/faq/faq.controller.ts index 95e538426..a5274eac8 100644 --- a/src/modules/faq/faq.controller.ts +++ b/src/modules/faq/faq.controller.ts @@ -1,5 +1,5 @@ -import { Controller, Post, Body, UsePipes, ValidationPipe, Get, Put, Param, Delete, UseGuards } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger'; +import { Controller, Post, Body, Get, Put, Param, Delete, UseGuards, Req, Query } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBearerAuth } from '@nestjs/swagger'; import { FaqService } from './faq.service'; import { CreateFaqDto } from './dto/create-faq.dto'; import { Faq } from './entities/faq.entity'; @@ -29,9 +29,13 @@ export class FaqController { status: 500, description: 'Internal Server Error if an unexpected error occurs.', }) - @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) - async create(@Body() createFaqDto: CreateFaqDto): Promise { - const faq: IFaq = await this.faqService.create(createFaqDto); + @ApiBearerAuth() + async create( + @Body() createFaqDto: CreateFaqDto, + @Req() req: any + ): Promise { + const language = req.language; + const faq: IFaq = await this.faqService.create(createFaqDto, language); return { status_code: 201, success: true, @@ -42,20 +46,28 @@ export class FaqController { @skipAuth() @Get() @ApiOperation({ summary: 'Get all frequently asked questions' }) - async findAll() { - return this.faqService.findAllFaq(); + async findAll(@Req() req: any) { + const language = req.language; + return this.faqService.findAllFaq(language); } + @ApiBearerAuth() @UseGuards(SuperAdminGuard) @Put(':id') @ApiOperation({ summary: 'Update an existing FAQ' }) @ApiParam({ name: 'id', type: 'string' }) @ApiResponse({ status: 200, description: 'The FAQ has been successfully updated.', type: Faq }) @ApiResponse({ status: 400, description: 'Bad Request.' }) - async update(@Param('id') id: string, @Body() updateFaqDto: UpdateFaqDto) { - return this.faqService.updateFaq(id, updateFaqDto); + async update( + @Param('id') id: string, + @Body() updateFaqDto: UpdateFaqDto, + @Req() req: any + ) { + const language = req.language; + return this.faqService.updateFaq(id, updateFaqDto, language); } + @ApiBearerAuth() @UseGuards(SuperAdminGuard) @Delete(':id') @ApiOperation({ summary: 'Delete an FAQ' }) diff --git a/src/modules/faq/faq.module.ts b/src/modules/faq/faq.module.ts index 897b79c27..ae307ac31 100644 --- a/src/modules/faq/faq.module.ts +++ b/src/modules/faq/faq.module.ts @@ -4,10 +4,14 @@ import { FaqController } from './faq.controller'; import { FaqService } from './faq.service'; import { Faq } from './entities/faq.entity'; import { User } from '../user/entities/user.entity'; +import { Organisation } from '../organisations/entities/organisations.entity'; +import { OrganisationUserRole } from '../role/entities/organisation-user-role.entity'; +import { Role } from '../role/entities/role.entity'; +import { TextService } from '../translation/translation.service'; @Module({ - imports: [TypeOrmModule.forFeature([Faq, User])], + imports: [TypeOrmModule.forFeature([Faq, User, Organisation, OrganisationUserRole, Role])], controllers: [FaqController], - providers: [FaqService], + providers: [FaqService, TextService], }) export class FaqModule {} diff --git a/src/modules/faq/faq.service.ts b/src/modules/faq/faq.service.ts index a31c8b716..20fa4125d 100644 --- a/src/modules/faq/faq.service.ts +++ b/src/modules/faq/faq.service.ts @@ -1,43 +1,61 @@ -import { BadRequestException, Injectable, InternalServerErrorException, UnauthorizedException } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Faq } from './entities/faq.entity'; import { CreateFaqDto } from './dto/create-faq.dto'; import { IFaq } from './faq.interface'; import { UpdateFaqDto } from './dto/update-faq.dto'; +import { TextService } from '../translation/translation.service'; @Injectable() export class FaqService { constructor( @InjectRepository(Faq) - private faqRepository: Repository + private faqRepository: Repository, + private readonly textService: TextService ) {} - async create(createFaqDto: CreateFaqDto): Promise { - const faq = this.faqRepository.create(createFaqDto); + private async translateContent(content: string, lang: string) { + return this.textService.translateText(content, lang); + } + + async create(createFaqDto: CreateFaqDto, language?: string): Promise { + const { question, answer, category } = createFaqDto; + + const translatedQuestion = await this.translateContent(question, language); + const translatedAnswer = await this.translateContent(answer, language); + const translatedCategory = await this.translateContent(category, language); + + const faq = this.faqRepository.create({ + ...createFaqDto, + question: translatedQuestion, + answer: translatedAnswer, + category: translatedCategory, + }); + return this.faqRepository.save(faq); } - async findAllFaq() { - try { - const faqs = await this.faqRepository.find(); - return { - message: 'Faq fetched successfully', - status_code: 200, - data: faqs, - }; - } catch (error) { - if (error instanceof BadRequestException) { - return { - message: 'Invalid request', - status_code: 400, - }; - } else if (error instanceof InternalServerErrorException) { - throw error; - } - } + + async findAllFaq(language?: string) { + const faqs = await this.faqRepository.find(); + + const translatedFaqs = await Promise.all( + faqs.map(async faq => { + faq.question = await this.translateContent(faq.question, language); + faq.answer = await this.translateContent(faq.answer, language); + faq.category = await this.translateContent(faq.category, language); + return faq; + }) + ); + + return { + message: 'Faq fetched successfully', + status_code: 200, + data: translatedFaqs, + }; } - async updateFaq(id: string, updateFaqDto: UpdateFaqDto) { + async updateFaq(id: string, updateFaqDto: UpdateFaqDto, language?: string) { const faq = await this.faqRepository.findOne({ where: { id } }); if (!faq) { throw new BadRequestException({ @@ -45,28 +63,32 @@ export class FaqService { status_code: 400, }); } - try { - Object.assign(faq, updateFaqDto); - const updatedFaq = await this.faqRepository.save(faq); - return { - id: updatedFaq.id, - question: updatedFaq.question, - answer: updatedFaq.answer, - category: updatedFaq.category, - }; - } catch (error) { - if (error instanceof UnauthorizedException) { - return { - message: 'Unauthorized access', - status_code: 401, - }; - } else if (error instanceof BadRequestException) { - return { - message: 'Invalid request data', - status_code: 400, - }; - } + + const updatedFaq = { + ...faq, + ...updateFaqDto, + }; + + if (updateFaqDto.question) { + updatedFaq.question = await this.translateContent(updateFaqDto.question, language); } + + if (updateFaqDto.answer) { + updatedFaq.answer = await this.translateContent(updateFaqDto.answer, language); + } + + if (updateFaqDto.category) { + updatedFaq.category = await this.translateContent(updateFaqDto.category, language); + } + + await this.faqRepository.save(updatedFaq); + + return { + id: updatedFaq.id, + question: updatedFaq.question, + answer: updatedFaq.answer, + category: updatedFaq.category, + }; } async removeFaq(id: string) { diff --git a/src/modules/faq/test/faq.controller.spec.ts b/src/modules/faq/test/faq.controller.spec.ts deleted file mode 100644 index 14f8efc04..000000000 --- a/src/modules/faq/test/faq.controller.spec.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import * as request from 'supertest'; -import { FaqService } from '../faq.service'; -import { FaqController } from '../faq.controller'; -import { CreateFaqDto } from '../dto/create-faq.dto'; -import { IFaq, ICreateFaqResponse } from '../faq.interface'; -import { User, UserType } from '../../user/entities/user.entity'; -import { getRepositoryToken } from '@nestjs/typeorm'; -import { SuperAdminGuard } from '../../../guards/super-admin.guard'; - -describe('FaqController (e2e)', () => { - let app; - let server; - let faqService: FaqService; - - const mockFaqService = { - create: jest.fn().mockImplementation((createFaqDto: CreateFaqDto): IFaq => { - return { - id: 'some-uuid', - ...createFaqDto, - createdBy: 'ADMIN', - created_at: new Date(), - updated_at: new Date(), - }; - }), - }; - - const mockUserRepository = { - findOne: jest.fn().mockImplementation(({ where: { id } }) => { - return Promise.resolve({ - id, - user_type: UserType.SUPER_ADMIN, - }); - }), - }; - - const mockUser = { - sub: 'user-id', - }; - - const mockSuperAdminGuard = { - canActivate: jest.fn().mockImplementation(() => true), - }; - - beforeAll(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - controllers: [FaqController], - providers: [ - { - provide: FaqService, - useValue: mockFaqService, - }, - { - provide: getRepositoryToken(User), - useValue: mockUserRepository, - }, - ], - }) - .overrideGuard(SuperAdminGuard) - .useClass(SuperAdminGuard) - .compile(); - - app = moduleFixture.createNestApplication(); - faqService = moduleFixture.get(FaqService); - - app.use((req, res, next) => { - req.user = mockUser; - next(); - }); - - await app.init(); - server = app.getHttpServer(); - }); - - afterAll(async () => { - if (app) { - await app.close(); - } - }); - - describe('POST /faqs', () => { - it('should create a new FAQ', async () => { - const createFaqDto: CreateFaqDto = { - question: 'What is the return policy?', - answer: 'Our return policy allows returns within 30 days of purchase.', - category: 'Policies', - }; - - const response = await request(server).post('/faqs').send(createFaqDto).expect(201); - - const expectedResponse: ICreateFaqResponse = { - status_code: 201, - success: true, - data: { - id: expect.any(String), - question: createFaqDto.question, - answer: createFaqDto.answer, - category: createFaqDto.category, - createdBy: 'ADMIN', - created_at: expect.any(String), - updated_at: expect.any(String), - }, - }; - - expect(response.body).toEqual(expectedResponse); - }); - - it('should return 400 if question is missing', async () => { - const createFaqDto = { - answer: 'Our return policy allows returns within 30 days of purchase.', - category: 'Policies', - }; - - const response = await request(server).post('/faqs').send(createFaqDto).expect(400); - - expect(response.body.message).toContain('Question is required'); - }); - - it('should return 400 if answer is missing', async () => { - const createFaqDto = { - question: 'What is the return policy?', - category: 'Policies', - }; - - const response = await request(server).post('/faqs').send(createFaqDto).expect(400); - - expect(response.body.message).toContain('Answer is required'); - }); - - it('should return 500 if there is an unexpected error', async () => { - const createFaqDto: CreateFaqDto = { - question: 'What is the return policy?', - answer: 'Our return policy allows returns within 30 days of purchase.', - category: 'Policies', - }; - - jest.spyOn(faqService, 'create').mockImplementation(() => { - throw new Error('Unexpected error'); - }); - - const response = await request(server).post('/faqs').send(createFaqDto).expect(500); - - expect(response.body).toEqual({ - statusCode: 500, - message: 'Internal server error', - }); - }); - }); -}); diff --git a/src/modules/faq/test/faq.create.service.spec.ts b/src/modules/faq/test/faq.create.service.spec.ts index 3135e421d..038ef30a4 100644 --- a/src/modules/faq/test/faq.create.service.spec.ts +++ b/src/modules/faq/test/faq.create.service.spec.ts @@ -4,6 +4,13 @@ import { Repository } from 'typeorm'; import { Faq } from '../entities/faq.entity'; import { CreateFaqDto } from '../dto/create-faq.dto'; import { getRepositoryToken } from '@nestjs/typeorm'; +import { TextService } from '../../translation/translation.service'; + +class MockTextService { + translateText(text: string, targetLang: string) { + return Promise.resolve(text); + } +} describe('FaqService', () => { let service: FaqService; @@ -24,6 +31,10 @@ describe('FaqService', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ FaqService, + { + provide: TextService, + useClass: MockTextService, + }, { provide: getRepositoryToken(Faq), useValue: mockFaqRepository, diff --git a/src/modules/faq/test/faq.service.spec.ts b/src/modules/faq/test/faq.service.spec.ts index b7b7a7f7c..9389f9261 100644 --- a/src/modules/faq/test/faq.service.spec.ts +++ b/src/modules/faq/test/faq.service.spec.ts @@ -4,6 +4,13 @@ import { Repository } from 'typeorm'; import { FaqService } from '../faq.service'; import { Faq } from '../entities/faq.entity'; import { BadRequestException } from '@nestjs/common'; +import { TextService } from '../../translation/translation.service'; + +class MockTextService { + translateText(text: string, targetLang: string) { + return Promise.resolve(text); + } +} describe('FaqService', () => { let service: FaqService; @@ -20,6 +27,10 @@ describe('FaqService', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ FaqService, + { + provide: TextService, + useClass: MockTextService, + }, { provide: getRepositoryToken(Faq), useValue: mockRepository, diff --git a/src/modules/flutterwave/dto/create-flutterwave-payment.dto.ts b/src/modules/flutterwave/dto/create-flutterwave-payment.dto.ts new file mode 100644 index 000000000..b91741f3d --- /dev/null +++ b/src/modules/flutterwave/dto/create-flutterwave-payment.dto.ts @@ -0,0 +1,31 @@ +import { IsEmail, IsEmpty, IsNotEmpty, IsString, IsUUID } from 'class-validator'; + +export class CreateFlutterwavePaymentDto { + @IsNotEmpty() + @IsEmail() + email: string; + + @IsNotEmpty() + @IsUUID() + organisation_id: string; + + @IsNotEmpty() + @IsString() + plan_id: string; + + @IsNotEmpty() + @IsString() + first_name: string; + + @IsNotEmpty() + @IsString() + last_name: string; + + @IsNotEmpty() + @IsString() + billing_option: string; + + @IsNotEmpty() + @IsString() + redirect_url: string; +} diff --git a/src/modules/flutterwave/dto/create-payment.dto.ts b/src/modules/flutterwave/dto/create-payment.dto.ts new file mode 100644 index 000000000..c9c4a1489 --- /dev/null +++ b/src/modules/flutterwave/dto/create-payment.dto.ts @@ -0,0 +1,24 @@ +import { IsString, IsNotEmpty, IsNumber, IsUUID, IsEnum } from 'class-validator'; +import { PaymentStatus } from '../entities/payment.entity'; + +export class CreatePaymentDto { + @IsNotEmpty() + @IsUUID() + user_id: string; + + @IsNotEmpty() + @IsString() + transaction_id: string; + + @IsNotEmpty() + @IsNumber() + gateway_id: string; + + @IsNotEmpty() + @IsNumber() + amount: number; + + @IsNotEmpty() + @IsEnum(PaymentStatus) + status: PaymentStatus; +} diff --git a/src/modules/flutterwave/dto/update-flutterwave.dto.ts b/src/modules/flutterwave/dto/update-flutterwave.dto.ts new file mode 100644 index 000000000..518d8c18d --- /dev/null +++ b/src/modules/flutterwave/dto/update-flutterwave.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateFlutterwavePaymentDto } from './create-flutterwave-payment.dto'; + +export class UpdateFlutterwaveDto extends PartialType(CreateFlutterwavePaymentDto) {} diff --git a/src/modules/flutterwave/entities/payment.entity.ts b/src/modules/flutterwave/entities/payment.entity.ts new file mode 100644 index 000000000..e0a2714a3 --- /dev/null +++ b/src/modules/flutterwave/entities/payment.entity.ts @@ -0,0 +1,26 @@ +import { AbstractBaseEntity } from '../../../entities/base.entity'; +import { Column, Entity } from 'typeorm'; + +export enum PaymentStatus { + 'PENDING' = 'pending', + 'APPROVED' = 'approved', + 'FAILED' = 'failed', +} + +@Entity() +export class Payment extends AbstractBaseEntity { + @Column({ nullable: false }) + user_id: string; + + @Column({ nullable: false }) + transaction_id: string; + + @Column({ nullable: true }) + gateway_id: string; + + @Column({ nullable: false }) + amount: number; + + @Column({ nullable: true }) + status: PaymentStatus; +} diff --git a/src/modules/flutterwave/flutterwave.controller.ts b/src/modules/flutterwave/flutterwave.controller.ts new file mode 100644 index 000000000..93ffb3fb6 --- /dev/null +++ b/src/modules/flutterwave/flutterwave.controller.ts @@ -0,0 +1,20 @@ +import { Controller, Get, Post, Body, Patch, Param, Delete, Req } from '@nestjs/common'; +import { FlutterwaveService } from './flutterwave.service'; +import { CreateFlutterwavePaymentDto } from './dto/create-flutterwave-payment.dto'; +import { UserPayload } from '../user/interfaces/user-payload.interface'; + +@Controller('payments/flutterwave') +export class FlutterwaveController { + constructor(private readonly flutterwaveService: FlutterwaveService) {} + + @Post() + initiate(@Body() createFlutterwavePaymentDto: CreateFlutterwavePaymentDto, @Req() req: { user: UserPayload }) { + const userId = req.user.id; + return this.flutterwaveService.initiatePayment(createFlutterwavePaymentDto, userId); + } + + @Get('verify/:id') + verify(@Param() id: string) { + return this.flutterwaveService.verifyPayment(id); + } +} diff --git a/src/modules/flutterwave/flutterwave.module.ts b/src/modules/flutterwave/flutterwave.module.ts new file mode 100644 index 000000000..23489ffb8 --- /dev/null +++ b/src/modules/flutterwave/flutterwave.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { FlutterwaveService } from './flutterwave.service'; +import { FlutterwaveController } from './flutterwave.controller'; +import { HttpModule } from '@nestjs/axios'; +import { Payment } from './entities/payment.entity'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +@Module({ + imports: [HttpModule, TypeOrmModule.forFeature([Payment])], + controllers: [FlutterwaveController], + providers: [FlutterwaveService], +}) +export class FlutterwaveModule {} diff --git a/src/modules/flutterwave/flutterwave.service.spec.ts b/src/modules/flutterwave/flutterwave.service.spec.ts new file mode 100644 index 000000000..518d290b4 --- /dev/null +++ b/src/modules/flutterwave/flutterwave.service.spec.ts @@ -0,0 +1,111 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { FlutterwaveService } from './flutterwave.service'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { Repository } from 'typeorm'; +import { Payment, PaymentStatus } from './entities/payment.entity'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { of } from 'rxjs'; +import { AxiosResponse } from 'axios'; +import { CreateFlutterwavePaymentDto } from './dto/create-flutterwave-payment.dto'; + +describe('FlutterwaveService', () => { + let service: FlutterwaveService; + let httpService: HttpService; + let paymentRepo: Repository; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + FlutterwaveService, + { + provide: HttpService, + useValue: { + get: jest.fn(), + post: jest.fn(), + }, + }, + { + provide: ConfigService, + useValue: { + get: jest.fn().mockReturnValue('mock-base-url'), + }, + }, + { + provide: getRepositoryToken(Payment), + useClass: Repository, + }, + ], + }).compile(); + + service = module.get(FlutterwaveService); + httpService = module.get(HttpService); + paymentRepo = module.get>(getRepositoryToken(Payment)); + }); + + it('should initiate a payment successfully', async () => { + const paymentPlanResponse: AxiosResponse = { + data: { + data: { + amount: 5000, + currency: 'USD', + }, + }, + status: 200, + statusText: 'OK', + headers: {}, + config: { + headers: undefined, + }, + }; + + jest.spyOn(httpService, 'get').mockReturnValue(of(paymentPlanResponse)); + + const paymentInitResponse: AxiosResponse = { + data: { + data: { + link: 'http://payment-link.com', + }, + }, + status: 200, + statusText: 'OK', + headers: {}, + config: { + headers: undefined, + }, + }; + + jest.spyOn(httpService, 'post').mockReturnValue(of(paymentInitResponse)); + jest.spyOn(paymentRepo, 'create').mockImplementation(dto => dto as Payment); + jest.spyOn(paymentRepo, 'save').mockResolvedValue({} as Payment); + + const createFlutterwavePaymentDto: CreateFlutterwavePaymentDto = { + plan_id: 'plan-id', + email: 'test@example.com', + first_name: 'first', + last_name: 'last', + redirect_url: 'http://redirect-url.com', + organisation_id: 'org-id', + billing_option: 'monthly', + }; + const userId = 'jjsjshshs'; + const result = await service.initiatePayment(createFlutterwavePaymentDto, userId); + + expect(result).toEqual({ + status: 200, + message: 'Payment initiated successfully', + data: { + payment_url: 'http://payment-link.com', + }, + }); + expect(httpService.get).toHaveBeenCalledWith('mock-base-url/payment-plans/plan-id', expect.any(Object)); + expect(httpService.post).toHaveBeenCalledWith('mock-base-url/payments', expect.any(Object), expect.any(Object)); + expect(paymentRepo.create).toHaveBeenCalledWith( + expect.objectContaining({ + amount: 5000, + status: PaymentStatus.PENDING, + }) + ); + expect(paymentRepo.save).toHaveBeenCalled(); + }); +}); diff --git a/src/modules/flutterwave/flutterwave.service.ts b/src/modules/flutterwave/flutterwave.service.ts new file mode 100644 index 000000000..5c35dc7c2 --- /dev/null +++ b/src/modules/flutterwave/flutterwave.service.ts @@ -0,0 +1,97 @@ +import { Injectable } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { CreateFlutterwavePaymentDto } from './dto/create-flutterwave-payment.dto'; +import { CreatePaymentDto } from './dto/create-payment.dto'; +import { ConfigService } from '@nestjs/config'; +import { CustomHttpException } from '../../helpers/custom-http-filter'; +import { v4 as uuid4 } from 'uuid'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Payment, PaymentStatus } from './entities/payment.entity'; +import { PAYMENT_NOTFOUND } from '../../helpers/SystemMessages'; + +@Injectable() +export class FlutterwaveService { + private readonly secretkey: string; + private readonly baseUrl: string; + constructor( + private readonly httpService: HttpService, + private readonly configService: ConfigService, + @InjectRepository(Payment) + private readonly paymentRepo: Repository + ) { + this.secretkey = configService.get('FLUTTERWAVE_SECRET_KEY'); + this.baseUrl = configService.get('FLUTTERWAVE_BASE_URL'); + } + + async initiatePayment(createFlutterwavePaymentDto: CreateFlutterwavePaymentDto, userId: string) { + const headers = { + Authorization: `Bearer ${this.secretkey}`, + 'Content-Type': 'application/json', + }; + const payment_plan = await this.httpService + .get(`${this.baseUrl}/payment-plans/${createFlutterwavePaymentDto.plan_id}`, { headers }) + .toPromise(); + if (!payment_plan) { + throw new CustomHttpException(PAYMENT_NOTFOUND, 404); + } + const { amount, currency } = payment_plan.data.data; + const { email, first_name, last_name } = createFlutterwavePaymentDto; + const paymentData = { + tx_ref: uuid4(), + amount: amount, + currency: currency, + redirect_url: createFlutterwavePaymentDto.redirect_url, + customer: { + email: email, + name: `${first_name} ${last_name}`, + }, + customizations: { + title: 'Payment for Goods/Services', + description: 'Payment for the purchase of goods or services', + }, + meta: { + organization_id: createFlutterwavePaymentDto.organisation_id, + plan_id: createFlutterwavePaymentDto.plan_id, + billing_option: createFlutterwavePaymentDto.billing_option, + }, + }; + const response = await this.httpService.post(`${this.baseUrl}/payments`, paymentData, { headers }).toPromise(); + const createPaymentDto: CreatePaymentDto = { + user_id: userId, + transaction_id: uuid4(), + gateway_id: '', + amount: paymentData.amount, + status: PaymentStatus.PENDING, + }; + const newPayment = await this.paymentRepo.create(createPaymentDto); + await this.paymentRepo.save(newPayment); + return { + status: 200, + message: 'Payment initiated successfully', + data: { + payment_url: response.data.data.link, + }, + }; + } + + async verifyPayment(transactionId: string): Promise { + const headers = { + Authorization: `Bearer ${this.secretkey}`, + 'Content-Type': 'application/json', + }; + const response = await this.httpService + .get(`${this.baseUrl}/transactions/${transactionId}/verify`, { headers }) + .toPromise(); + const payment = await this.paymentRepo.findOne({ where: { transaction_id: transactionId } }); + payment.status = PaymentStatus.APPROVED; + await this.paymentRepo.save(payment); + return { + status: 200, + message: 'Payment verified successfully', + data: { + paymentStatus: response.data.data, + }, + }; + } +} diff --git a/src/modules/flutterwave/helper/payment-plan.ts b/src/modules/flutterwave/helper/payment-plan.ts new file mode 100644 index 000000000..ed3a67e48 --- /dev/null +++ b/src/modules/flutterwave/helper/payment-plan.ts @@ -0,0 +1,5 @@ +export class PaymentPlan { + async getPaymentPlanById(plan_id: string) { + return; + } +} diff --git a/src/modules/help-center/Tests/help-center.service.spec.ts b/src/modules/help-center/Tests/help-center.service.spec.ts index e6ac657ea..55929707a 100644 --- a/src/modules/help-center/Tests/help-center.service.spec.ts +++ b/src/modules/help-center/Tests/help-center.service.spec.ts @@ -1,70 +1,198 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { Repository } from 'typeorm'; import { HelpCenterService } from '../help-center.service'; -import { UpdateHelpCenterDto } from '../dto/update-help-center.dto'; +import { Repository } from 'typeorm'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { HelpCenter } from '../interface/help-center.interface'; +import { NotFoundException, BadRequestException } from '@nestjs/common'; import { HelpCenterEntity } from '../entities/help-center.entity'; import { REQUEST_SUCCESSFUL } from '../../../helpers/SystemMessages'; +import { User } from '../../user/entities/user.entity'; +import { TextService } from '../../translation/translation.service'; +import * as SYS_MSG from '../../../helpers/SystemMessages'; + +class MockTextService { + translateText(text: string, targetLang: string) { + return Promise.resolve(text); + } +} describe('HelpCenterService', () => { let service: HelpCenterService; - let repository: Repository; + let helpCenterRepository: Repository; + let userRepository: Repository; - const mockHelpCenterRepository = () => ({ - update: jest.fn(), - findOneBy: jest.fn(), - delete: jest.fn(), - }); + const mockHelpCenter = { + id: '1234', + title: 'Sample Title', + content: 'Sample Content', + author: 'John Doe', + }; + + const mockHelpCenterDto = { + title: 'Sample Title', + content: 'Sample Content', + }; + + const mockUser = { + id: '123', + first_name: 'John', + last_name: 'Doe', + }; + + const mockHelpCenterRepository = { + create: jest.fn().mockImplementation(dto => ({ + ...dto, + id: '1234', + })), + save: jest.fn().mockResolvedValue(mockHelpCenter), + find: jest.fn().mockResolvedValue([mockHelpCenter]), + findOne: jest + .fn() + .mockImplementation(options => + Promise.resolve(options.where.title === mockHelpCenter.title ? mockHelpCenter : null) + ), + createQueryBuilder: jest.fn().mockReturnValue({ + andWhere: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue([mockHelpCenter]), + }), + }; + + const mockUserRepository = { + findOne: jest + .fn() + .mockImplementation(options => Promise.resolve(options.where.id === mockUser.id ? mockUser : null)), + }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ HelpCenterService, + { + provide: TextService, + useClass: MockTextService, + }, { provide: getRepositoryToken(HelpCenterEntity), - useValue: mockHelpCenterRepository(), + useValue: mockHelpCenterRepository, + }, + { + provide: getRepositoryToken(User), + useValue: mockUserRepository, }, ], }).compile(); service = module.get(HelpCenterService); - repository = module.get>(getRepositoryToken(HelpCenterEntity)); + helpCenterRepository = module.get>(getRepositoryToken(HelpCenterEntity)); + userRepository = module.get>(getRepositoryToken(User)); }); - it('should be defined', () => { - expect(service).toBeDefined(); + describe('create', () => { + it('should create a new help center topic with the user as author', async () => { + mockHelpCenterRepository.findOne.mockResolvedValueOnce(null); + mockUserRepository.findOne.mockResolvedValue(mockUser); + + const result = await service.create(mockHelpCenterDto, mockUser as unknown as User); + const responseBody = { + status_code: 201, + message: 'Request successful', + data: { ...mockHelpCenterDto, author: 'John Doe', id: '1234' }, + }; + + expect(result).toEqual(responseBody); + expect(helpCenterRepository.create).toHaveBeenCalledWith({ + ...mockHelpCenterDto, + author: 'John Doe', + }); + expect(helpCenterRepository.save).toHaveBeenCalledWith({ + ...mockHelpCenterDto, + author: 'John Doe', + id: '1234', + }); + }); + + it('should throw a BadRequestException if a topic with the same title already exists', async () => { + mockHelpCenterRepository.findOne.mockResolvedValue(mockHelpCenter); + + await expect(service.create(mockHelpCenterDto, mockUser as unknown as User)).rejects.toThrow( + new BadRequestException('This question already exists.') + ); + }); }); - describe('updateTopic', () => { - it('should update and return the help center topic', async () => { - const id = '1'; - const updateHelpCenterDto = { - title: 'Updated Title', - content: 'Updated Content', - author: 'Updated Author', + describe('findAll', () => { + it('should return an array of help center topics', async () => { + const result = await service.findAll(); + const responseBody = { + data: [mockHelpCenter], + status_code: 200, + message: REQUEST_SUCCESSFUL, }; + expect(result).toEqual(responseBody); + expect(helpCenterRepository.find).toHaveBeenCalled(); + }); + }); + + describe('findOne', () => { + it('should return a help center topic by ID', async () => { + const result = await service.findOne('1234'); const responseBody = { + data: mockHelpCenter, status_code: 200, message: REQUEST_SUCCESSFUL, - data: { ...updateHelpCenterDto, id }, }; - const updatedHelpCenter = { id, ...updateHelpCenterDto }; + expect(result).toEqual(responseBody); + expect(helpCenterRepository.findOne).toHaveBeenCalledWith({ where: { id: '1234' } }); + }); - jest.spyOn(repository, 'update').mockResolvedValue(undefined); - jest.spyOn(repository, 'findOneBy').mockResolvedValue(updatedHelpCenter as any); + it('should throw a NotFoundException if topic not found', async () => { + mockHelpCenterRepository.findOne.mockResolvedValueOnce(null); - expect(await service.updateTopic(id, updateHelpCenterDto)).toEqual(responseBody); + await expect(service.findOne('wrong-id')).rejects.toThrow( + new NotFoundException('Help center topic with ID wrong-id not found') + ); }); }); - describe('removeTopic', () => { - it('should remove a help center topic', async () => { - jest.spyOn(repository, 'delete').mockResolvedValue(undefined); + describe('search', () => { + it('should return an array of help center topics matching search criteria', async () => { + const mockQueryBuilder = { + andWhere: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue([mockHelpCenter]), + }; + + jest.spyOn(helpCenterRepository, 'createQueryBuilder').mockReturnValue(mockQueryBuilder as any); + + const result = await service.search({ title: 'Sample' }); + const responseBody = { + status_code: 200, + message: REQUEST_SUCCESSFUL, + data: [mockHelpCenter], + }; + expect(result).toEqual(responseBody); + expect(helpCenterRepository.createQueryBuilder).toHaveBeenCalledWith('help_center'); + expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('help_center.title LIKE :title', { title: '%Sample%' }); + }); + + it('should return an array of help center topics matching multiple search criteria', async () => { + const mockQueryBuilder = { + andWhere: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue([mockHelpCenter]), + }; - await service.removeTopic('1'); + jest.spyOn(helpCenterRepository, 'createQueryBuilder').mockReturnValue(mockQueryBuilder as any); - expect(repository.delete).toHaveBeenCalledWith('1'); + const result = await service.search({ title: 'Sample', content: 'Sample Content' }); + const responseBody = { + status_code: 200, + message: REQUEST_SUCCESSFUL, + data: [mockHelpCenter], + }; + expect(result).toEqual(responseBody); + expect(helpCenterRepository.createQueryBuilder).toHaveBeenCalledWith('help_center'); + expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('help_center.title LIKE :title', { title: '%Sample%' }); + expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('help_center.content LIKE :content', { + content: '%Sample Content%', + }); }); }); }); diff --git a/src/modules/help-center/docs/helpCenter-swagger.ts b/src/modules/help-center/docs/helpCenter-swagger.ts new file mode 100644 index 000000000..5f7eb7672 --- /dev/null +++ b/src/modules/help-center/docs/helpCenter-swagger.ts @@ -0,0 +1,56 @@ +import { applyDecorators } from '@nestjs/common'; +import { ApiTags, ApiBearerAuth, ApiResponse, ApiOperation } from '@nestjs/swagger'; + +export function CreateHelpCenterDocs() { + return applyDecorators( + ApiOperation({ summary: 'Create a new help center topic' }), + ApiResponse({ status: 201, description: 'The topic has been successfully created.' }), + ApiResponse({ status: 400, description: 'Invalid input data.' }), + ApiResponse({ status: 400, description: 'This question already exists.' }) + ); +} + +export function GetAllHelpCenterDocs() { + return applyDecorators( + ApiOperation({ summary: 'Get all help center topics' }), + ApiResponse({ status: 200, description: 'The found records' }) + ); +} + +export function GetByIdHelpCenterDocs() { + return applyDecorators( + ApiOperation({ summary: 'Get a help center topic by ID' }), + ApiResponse({ status: 200, description: 'The found record' }), + ApiResponse({ status: 404, description: 'Topic not found' }) + ); +} + +export function SearchHelpCenterDocs() { + return applyDecorators( + ApiOperation({ summary: 'Search help center topics' }), + ApiResponse({ status: 200, description: 'The found records' }), + ApiResponse({ status: 422, description: 'Invalid search criteria.' }) + ); +} + +export function UpdateHelpCenterDocs() { + return applyDecorators( + ApiOperation({ summary: 'Update a help center topic by id' }), + ApiResponse({ status: 200, description: 'Topic updated successfully' }), + ApiResponse({ status: 401, description: 'Unauthorized, please provide valid credentials' }), + ApiResponse({ status: 403, description: 'Access denied, only authorized users can access this endpoint' }), + ApiResponse({ status: 404, description: 'Topic not found, please check and try again' }), + ApiResponse({ status: 500, description: 'Internal Server Error' }) + ); +} + +export function DeleteHelpCenterDocs() { + return applyDecorators( + ApiOperation({ summary: 'Delete a help center topic by id' }), + ApiResponse({ status: 200, description: 'Topic deleted successfully' }), + ApiResponse({ status: 401, description: 'Unauthorized, please provide valid credentials' }), + ApiResponse({ status: 403, description: 'Access denied, only authorized users can access this endpoint' }), + ApiResponse({ status: 404, description: 'Topic not found, please check and try again' }), + ApiResponse({ status: 500, description: 'Internal Server Error' }) + ); +} diff --git a/src/modules/help-center/help-center.controller.ts b/src/modules/help-center/help-center.controller.ts index db346634b..a7f9bd5ef 100644 --- a/src/modules/help-center/help-center.controller.ts +++ b/src/modules/help-center/help-center.controller.ts @@ -10,6 +10,9 @@ import { NotFoundException, Post, Query, + UseGuards, + Req, + HttpCode, } from '@nestjs/common'; import { HelpCenterService } from './help-center.service'; import { UpdateHelpCenterDto } from './dto/update-help-center.dto'; @@ -18,11 +21,23 @@ import { CreateHelpCenterDto } from './dto/create-help-center.dto'; import { GetHelpCenterDto } from './dto/get-help-center.dto'; import { SearchHelpCenterDto } from './dto/search-help-center.dto'; import { HelpCenter } from './interface/help-center.interface'; -import { skipAuth } from 'src/helpers/skipAuth'; +import { skipAuth } from '../../helpers/skipAuth'; import { HelpCenterMultipleInstancResponseType, HelpCenterSingleInstancResponseType, } from './dto/help-center.response.dto'; +import { SuperAdminGuard } from '../../guards/super-admin.guard'; +import { User } from '../user/entities/user.entity'; +import { + CreateHelpCenterDocs, + DeleteHelpCenterDocs, + GetAllHelpCenterDocs, + GetByIdHelpCenterDocs, + SearchHelpCenterDocs, + UpdateHelpCenterDocs, +} from './docs/helpCenter-swagger'; +import { CustomHttpException } from '../../helpers/custom-http-filter'; +import * as SYS_MSG from '../../helpers/SystemMessages'; @ApiTags('help-center') @Controller('help-center') @@ -30,155 +45,55 @@ export class HelpCenterController { constructor(private readonly helpCenterService: HelpCenterService) {} @ApiBearerAuth() - @Post('help-center/topics') - @ApiOperation({ summary: 'Create a new help center topic' }) - @ApiResponse({ status: 201, description: 'The topic has been successfully created.' }) - @ApiResponse({ status: 422, description: 'Invalid input data.' }) - async create(@Body() createHelpCenterDto: CreateHelpCenterDto): Promise { - return this.helpCenterService.create(createHelpCenterDto); + @Post('topics') + @UseGuards(SuperAdminGuard) + @CreateHelpCenterDocs() + async create( + @Body() createHelpCenterDto: CreateHelpCenterDto, + @Req() req: { user: User; language: string } + ): Promise { + const user: User = req.user; + const language = req.language; + return this.helpCenterService.create(createHelpCenterDto, user, language); } @skipAuth() @Get('topics') - @ApiOperation({ summary: 'Get all help center topics' }) - @ApiResponse({ status: 200, description: 'The found records' }) - async findAll(): Promise { - return this.helpCenterService.findAll(); + @GetAllHelpCenterDocs() + async findAll(@Req() req: any): Promise { + const language = req.language; + return this.helpCenterService.findAll(language); } @skipAuth() @Get('topics/:id') - @ApiOperation({ summary: 'Get a help center topic by ID' }) - @ApiResponse({ status: 200, description: 'The found record' }) - @ApiResponse({ status: 404, description: 'Topic not found' }) + @GetByIdHelpCenterDocs() async findOne(@Param() params: GetHelpCenterDto): Promise { - const helpCenter = await this.helpCenterService.findOne(params.id); - if (!helpCenter) { - throw new NotFoundException(`Help center topic with ID ${params.id} not found`); - } + const helpCenter = await this.helpCenterService.findHelpCenter(params.id); return helpCenter; } @skipAuth() @Get('topics/search') - @ApiOperation({ summary: 'Search help center topics' }) - @ApiResponse({ status: 200, description: 'The found records' }) - @ApiResponse({ status: 422, description: 'Invalid search criteria.' }) + @SearchHelpCenterDocs() async search(@Query() query: SearchHelpCenterDto): Promise { return this.helpCenterService.search(query); } @ApiBearerAuth() @Patch('topics/:id') - @ApiOperation({ summary: 'Update a help center topic by id' }) - @ApiResponse({ status: 200, description: 'Topic updated successfully' }) - @ApiResponse({ status: 401, description: 'Unauthorized, please provide valid credentials' }) - @ApiResponse({ status: 403, description: 'Access denied, only authorized users can access this endpoint' }) - @ApiResponse({ status: 404, description: 'Topic not found, please check and try again' }) - @ApiResponse({ status: 500, description: 'Internal Server Error' }) + @UseGuards(SuperAdminGuard) + @UpdateHelpCenterDocs() async update(@Param('id') id: string, @Body() updateHelpCenterDto: UpdateHelpCenterDto) { - try { - const updatedHelpCenter = await this.helpCenterService.updateTopic(id, updateHelpCenterDto); - return { - success: true, - message: 'Topic updated successfully', - data: updatedHelpCenter, - status_code: HttpStatus.OK, - }; - } catch (error) { - if (error.status === HttpStatus.UNAUTHORIZED) { - throw new HttpException( - { - success: false, - message: 'Unauthorized, please provide valid credentials', - status_code: HttpStatus.UNAUTHORIZED, - }, - HttpStatus.UNAUTHORIZED - ); - } else if (error.status === HttpStatus.FORBIDDEN) { - throw new HttpException( - { - success: false, - message: 'Access denied, only authorized users can access this endpoint', - status_code: HttpStatus.FORBIDDEN, - }, - HttpStatus.FORBIDDEN - ); - } else if (error.status === HttpStatus.NOT_FOUND) { - throw new HttpException( - { - success: false, - message: 'Topic not found, please check and try again', - status_code: HttpStatus.NOT_FOUND, - }, - HttpStatus.NOT_FOUND - ); - } else { - throw new HttpException( - { - success: false, - message: 'Internal Server Error', - }, - HttpStatus.INTERNAL_SERVER_ERROR - ); - } - } + const updatedHelpCenter = await this.helpCenterService.updateTopic(id, updateHelpCenterDto); + return updatedHelpCenter; } @ApiBearerAuth() @Delete('topics/:id') - //@Roles('superadmin') - @ApiOperation({ summary: 'Delete a help center topic by id' }) - @ApiResponse({ status: 200, description: 'Topic deleted successfully' }) - @ApiResponse({ status: 401, description: 'Unauthorized, please provide valid credentials' }) - @ApiResponse({ status: 403, description: 'Access denied, only authorized users can access this endpoint' }) - @ApiResponse({ status: 404, description: 'Topic not found, please check and try again' }) - @ApiResponse({ status: 500, description: 'Internal Server Error' }) + @UseGuards(SuperAdminGuard) + @DeleteHelpCenterDocs() async remove(@Param('id') id: string) { - try { - await this.helpCenterService.removeTopic(id); - return { - success: true, - message: 'Topic deleted successfully', - status_code: HttpStatus.OK, - }; - } catch (error) { - if (error.status === HttpStatus.UNAUTHORIZED) { - throw new HttpException( - { - success: false, - message: 'Unauthorized, please provide valid credentials', - status_code: HttpStatus.UNAUTHORIZED, - }, - HttpStatus.UNAUTHORIZED - ); - } else if (error.status === HttpStatus.FORBIDDEN) { - throw new HttpException( - { - success: false, - message: 'Access denied, only authorized users can access this endpoint', - status_code: HttpStatus.FORBIDDEN, - }, - HttpStatus.FORBIDDEN - ); - } else if (error.status === HttpStatus.NOT_FOUND) { - throw new HttpException( - { - success: false, - message: 'Topic not found, please check and try again', - status_code: HttpStatus.NOT_FOUND, - }, - HttpStatus.NOT_FOUND - ); - } else { - throw new HttpException( - { - success: false, - message: 'Internal Server Error', - }, - HttpStatus.INTERNAL_SERVER_ERROR - ); - } - } + return await this.helpCenterService.removeTopic(id); } } diff --git a/src/modules/help-center/help-center.module.ts b/src/modules/help-center/help-center.module.ts index 9cbffc3d2..b86bd9c0d 100644 --- a/src/modules/help-center/help-center.module.ts +++ b/src/modules/help-center/help-center.module.ts @@ -3,10 +3,16 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { HelpCenterService } from './help-center.service'; import { HelpCenterController } from './help-center.controller'; import { HelpCenterEntity } from './entities/help-center.entity'; +import { User } from '../user/entities/user.entity'; +import { Role } from '../role/entities/role.entity'; +import { Profile } from '../profile/entities/profile.entity'; +import { OrganisationUserRole } from '../role/entities/organisation-user-role.entity'; +import { Organisation } from '../organisations/entities/organisations.entity'; +import { TextService } from '../translation/translation.service'; @Module({ - imports: [TypeOrmModule.forFeature([HelpCenterEntity])], - providers: [HelpCenterService], + imports: [TypeOrmModule.forFeature([HelpCenterEntity, User, Organisation, OrganisationUserRole, Profile, Role])], + providers: [HelpCenterService, TextService], controllers: [HelpCenterController], exports: [HelpCenterService], }) diff --git a/src/modules/help-center/help-center.service.spec.ts b/src/modules/help-center/help-center.service.spec.ts deleted file mode 100644 index 8cdd5f3cd..000000000 --- a/src/modules/help-center/help-center.service.spec.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { HelpCenterService } from './help-center.service'; -import { Repository } from 'typeorm'; -import { getRepositoryToken } from '@nestjs/typeorm'; -import { NotFoundException } from '@nestjs/common'; -import { HelpCenterEntity } from './entities/help-center.entity'; -import { REQUEST_SUCCESSFUL } from '../../helpers/SystemMessages'; - -describe('HelpCenterService', () => { - let service: HelpCenterService; - let repository: Repository; - - const mockHelpCenter = { - id: '1234', - title: 'Sample Title', - content: 'Sample Content', - author: 'ADMIN', - }; - - const mockHelpCenterDto = { - title: 'Sample Title', - content: 'Sample Content', - }; - - const mockRepository = { - create: jest.fn().mockImplementation(dto => ({ - ...dto, - id: '1234', - })), - save: jest.fn().mockResolvedValue(mockHelpCenter), - find: jest.fn().mockResolvedValue([mockHelpCenter]), - findOne: jest - .fn() - .mockImplementation(options => Promise.resolve(options.where.id === '1234' ? mockHelpCenter : null)), - createQueryBuilder: jest.fn().mockReturnValue({ - andWhere: jest.fn().mockReturnThis(), - getMany: jest.fn().mockResolvedValue([mockHelpCenter]), - }), - }; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - HelpCenterService, - { - provide: getRepositoryToken(HelpCenterEntity), - useValue: mockRepository, - }, - ], - }).compile(); - - service = module.get(HelpCenterService); - repository = module.get>(getRepositoryToken(HelpCenterEntity)); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('create', () => { - it('should create a new help center topic and set author to ADMIN', async () => { - const result = await service.create(mockHelpCenterDto); - const responseBody = { - status_code: 201, - message: REQUEST_SUCCESSFUL, - data: { ...mockHelpCenterDto, author: 'ADMIN', id: '1234' }, - }; - expect(result).toEqual(responseBody); - - expect(repository.create).toHaveBeenCalledWith({ - ...mockHelpCenterDto, - author: 'ADMIN', - }); - expect(repository.save).toHaveBeenCalledWith({ ...mockHelpCenterDto, author: 'ADMIN', id: '1234' }); - }); - }); - - describe('findAll', () => { - it('should return an array of help center topics', async () => { - const result = await service.findAll(); - const responseBody = { - data: [mockHelpCenter], - status_code: 200, - message: REQUEST_SUCCESSFUL, - }; - expect(result).toEqual(responseBody); - expect(repository.find).toHaveBeenCalled(); - }); - }); - - describe('findOne', () => { - it('should return a help center topic by ID', async () => { - const result = await service.findOne('1234'); - const responseBody = { - data: mockHelpCenter, - status_code: 200, - message: REQUEST_SUCCESSFUL, - }; - expect(result).toEqual(responseBody); - expect(repository.findOne).toHaveBeenCalledWith({ where: { id: '1234' } }); - }); - - it('should throw a NotFoundException if topic not found', async () => { - await expect(service.findOne('wrong-id')).rejects.toThrow( - new NotFoundException('Help center topic with ID wrong-id not found') - ); - }); - }); - - describe('search', () => { - it('should return an array of help center topics matching search criteria', async () => { - const mockQueryBuilder = { - andWhere: jest.fn().mockReturnThis(), - getMany: jest.fn().mockResolvedValue([mockHelpCenter]), - }; - - jest.spyOn(repository, 'createQueryBuilder').mockReturnValue(mockQueryBuilder as any); - - const result = await service.search({ title: 'Sample' }); - const responseBody = { - status_code: 200, - message: REQUEST_SUCCESSFUL, - data: [mockHelpCenter], - }; - expect(result).toEqual(responseBody); - expect(repository.createQueryBuilder).toHaveBeenCalledWith('help_center'); - expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('help_center.title LIKE :title', { title: '%Sample%' }); - }); - - it('should return an array of help center topics matching multiple search criteria', async () => { - const mockQueryBuilder = { - andWhere: jest.fn().mockReturnThis(), - getMany: jest.fn().mockResolvedValue([mockHelpCenter]), - }; - - jest.spyOn(repository, 'createQueryBuilder').mockReturnValue(mockQueryBuilder as any); - - const result = await service.search({ title: 'Sample', content: 'Sample Content' }); - const responseBody = { - status_code: 200, - message: REQUEST_SUCCESSFUL, - data: [mockHelpCenter], - }; - expect(result).toEqual(responseBody); - expect(repository.createQueryBuilder).toHaveBeenCalledWith('help_center'); - expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('help_center.title LIKE :title', { title: '%Sample%' }); - expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('help_center.content LIKE :content', { - content: '%Sample Content%', - }); - }); - }); -}); diff --git a/src/modules/help-center/help-center.service.ts b/src/modules/help-center/help-center.service.ts index 97dffedbe..fa49edb94 100644 --- a/src/modules/help-center/help-center.service.ts +++ b/src/modules/help-center/help-center.service.ts @@ -1,39 +1,80 @@ -import { HttpStatus, Injectable, NotFoundException } from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { HelpCenterEntity } from '../help-center/entities/help-center.entity'; // Adjust the path as necessary +import { HelpCenterEntity } from '../help-center/entities/help-center.entity'; import { CreateHelpCenterDto } from './dto/create-help-center.dto'; import { UpdateHelpCenterDto } from './dto/update-help-center.dto'; import { SearchHelpCenterDto } from './dto/search-help-center.dto'; -import { REQUEST_SUCCESSFUL } from '../../helpers/SystemMessages'; +import * as SYS_MSG from '../../helpers/SystemMessages'; +import { CustomHttpException } from '../../helpers/custom-http-filter'; +import { User } from '../user/entities/user.entity'; +import { TextService } from '../translation/translation.service'; @Injectable() export class HelpCenterService { constructor( @InjectRepository(HelpCenterEntity) - private readonly helpCenterRepository: Repository + private readonly helpCenterRepository: Repository, + @InjectRepository(User) + private userRepository: Repository, + private readonly textService: TextService ) {} - async create(createHelpCenterDto: CreateHelpCenterDto) { - let helpCenter = this.helpCenterRepository.create({ + private async translateContent(content: string, lang: string) { + return this.textService.translateText(content, lang); + } + + async create(createHelpCenterDto: CreateHelpCenterDto, user: User, language: string = 'en') { + const existingTopic = await this.helpCenterRepository.findOne({ + where: { title: createHelpCenterDto.title }, + }); + + if (existingTopic) { + throw new CustomHttpException(SYS_MSG.QUESTION_ALREADY_EXISTS, HttpStatus.BAD_REQUEST); + } + + const fullUser = await this.userRepository.findOne({ + where: { id: user.id }, + select: ['first_name', 'last_name'], + }); + + if (!fullUser) { + throw new CustomHttpException(SYS_MSG.USER_NOT_FOUND, HttpStatus.NOT_FOUND); + } + + let translatedContent = createHelpCenterDto.content; + if (language && language !== 'en') { + translatedContent = await this.translateContent(createHelpCenterDto.content, language); + } + + const helpCenter = this.helpCenterRepository.create({ ...createHelpCenterDto, - author: 'ADMIN', + content: translatedContent, + author: `${fullUser.first_name} ${fullUser.last_name}`, }); const newEntity = await this.helpCenterRepository.save(helpCenter); + return { status_code: HttpStatus.CREATED, - message: REQUEST_SUCCESSFUL, + message: 'Request successful', data: newEntity, }; } - async findAll(): Promise { + async findAll(language?: string): Promise { const centres = await this.helpCenterRepository.find(); + const translatedhCTopics = await Promise.all( + centres.map(async topic => { + topic.title = await this.translateContent(topic.title, language); + topic.content = await this.translateContent(topic.content, language); + return topic; + }) + ); return { - data: centres, + data: translatedhCTopics, status_code: HttpStatus.OK, - message: REQUEST_SUCCESSFUL, + message: SYS_MSG.REQUEST_SUCCESSFUL, }; } @@ -44,11 +85,19 @@ export class HelpCenterService { } return { status_code: HttpStatus.OK, - message: REQUEST_SUCCESSFUL, + message: SYS_MSG.REQUEST_SUCCESSFUL, data: helpCenter, }; } + async findHelpCenter(id: string) { + const helpCenter = await this.findOne(id); + if (!helpCenter) { + throw new CustomHttpException(`Help center topic with ID ${id} not found`, HttpStatus.NOT_FOUND); + } + return helpCenter; + } + async search(criteria: SearchHelpCenterDto) { const queryBuilder = this.helpCenterRepository.createQueryBuilder('help_center'); if (criteria.title) { @@ -60,22 +109,46 @@ export class HelpCenterService { const query = await queryBuilder.getMany(); return { status_code: HttpStatus.OK, - message: REQUEST_SUCCESSFUL, + message: SYS_MSG.REQUEST_SUCCESSFUL, data: query, }; } async updateTopic(id: string, updateHelpCenterDto: UpdateHelpCenterDto) { + const existingTopic = await this.helpCenterRepository.findOneBy({ id }); + if (!existingTopic) { + throw new HttpException( + { + status: 'error', + message: SYS_MSG.TOPIC_NOT_FOUND, + status_code: HttpStatus.NOT_FOUND, + }, + HttpStatus.NOT_FOUND + ); + } + await this.helpCenterRepository.update(id, updateHelpCenterDto); - const query = await this.helpCenterRepository.findOneBy({ id }); + const updatedTopic = await this.helpCenterRepository.findOneBy({ id }); + return { status_code: HttpStatus.OK, - message: REQUEST_SUCCESSFUL, - data: query, + message: SYS_MSG.REQUEST_SUCCESSFUL, + data: updatedTopic, }; } async removeTopic(id: string): Promise { + const existingTopic = await this.helpCenterRepository.findOneBy({ id }); + if (!existingTopic) { + throw new HttpException( + { + status: 'error', + message: SYS_MSG.TOPIC_NOT_FOUND, + status_code: HttpStatus.NOT_FOUND, + }, + HttpStatus.NOT_FOUND + ); + } await this.helpCenterRepository.delete(id); } } diff --git a/src/modules/invite/dto/all-invitations-response.dto.ts b/src/modules/invite/dto/all-invitations-response.dto.ts new file mode 100644 index 000000000..8631f00d5 --- /dev/null +++ b/src/modules/invite/dto/all-invitations-response.dto.ts @@ -0,0 +1,13 @@ +import { InviteDto } from './invite.dto'; +import { ApiProperty } from '@nestjs/swagger'; + +export class FindAllInvitationsResponseDto { + @ApiProperty({ example: 200 }) + status_code: number; + + @ApiProperty({ example: 'Successfully fetched invites' }) + message: string; + + @ApiProperty({ type: [InviteDto] }) + data: InviteDto[]; +} diff --git a/src/modules/invite/dto/creat-invite-response.dto.ts b/src/modules/invite/dto/creat-invite-response.dto.ts new file mode 100644 index 000000000..4a05bdb55 --- /dev/null +++ b/src/modules/invite/dto/creat-invite-response.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateInviteResponseDto { + @ApiProperty({ example: 200 }) + status_code: number; + + @ApiProperty({ example: 'Invite link generated successfully' }) + message: string; + + @ApiProperty({ example: 'https://frontend.example.com/invite?token=12345678-1234-1234-1234-123456789012' }) + link: string; +} diff --git a/src/modules/invite/dto/create-invite.dto.ts b/src/modules/invite/dto/create-invite.dto.ts new file mode 100644 index 000000000..d9f289a84 --- /dev/null +++ b/src/modules/invite/dto/create-invite.dto.ts @@ -0,0 +1,12 @@ +import { IsArray, IsEmail, IsNotEmpty, IsUUID, ArrayNotEmpty } from 'class-validator'; + +export class CreateInvitationDto { + @IsArray() + @ArrayNotEmpty() + @IsEmail({}, { each: true }) + emails: string[]; + + @IsUUID() + @IsNotEmpty() + org_id: string; +} diff --git a/src/modules/invite/dto/invite-error-response.dto.ts b/src/modules/invite/dto/invite-error-response.dto.ts new file mode 100644 index 000000000..6401ff67d --- /dev/null +++ b/src/modules/invite/dto/invite-error-response.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class ErrorResponseDto { + @ApiProperty({ example: 400 }) + status_code: number; + + @ApiProperty({ example: 'Validation failed: email must be a valid email address.' }) + message: string; +} diff --git a/src/modules/invite/dto/pending-invitations.ts b/src/modules/invite/dto/pending-invitations.ts new file mode 100644 index 000000000..6ca80b7ec --- /dev/null +++ b/src/modules/invite/dto/pending-invitations.ts @@ -0,0 +1,13 @@ +import { InviteDto } from './invite.dto'; +import { ApiProperty } from '@nestjs/swagger'; + +export class FindAllPendingInvitationsResponseDto { + @ApiProperty({ example: 200 }) + status_code: number; + + @ApiProperty({ example: 'Successfully fetched pending invites' }) + message: string; + + @ApiProperty({ type: [InviteDto] }) + data: InviteDto[]; +} diff --git a/src/modules/invite/dto/send-invitations-response.dto.ts b/src/modules/invite/dto/send-invitations-response.dto.ts new file mode 100644 index 000000000..0cca18a23 --- /dev/null +++ b/src/modules/invite/dto/send-invitations-response.dto.ts @@ -0,0 +1,19 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class SendInvitationsResponseDto { + @ApiProperty({ example: 'Invitation(s) sent successfully' }) + message: string; + + @ApiProperty({ + example: [ + { + email: 'user1@example.com', + inviteLink: 'https://frontend.example.com/invite?token=12345678-1234-1234-1234-123456789012', + }, + ], + }) + invitations: { + email: string; + inviteLink: string; + }[]; +} diff --git a/src/modules/invite/invite.controller.ts b/src/modules/invite/invite.controller.ts index 96eaadfa4..98dc7df9c 100644 --- a/src/modules/invite/invite.controller.ts +++ b/src/modules/invite/invite.controller.ts @@ -6,10 +6,25 @@ import { HttpException, HttpStatus, NotFoundException, + Post, + Body, + Res, + UseGuards, } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { InviteService } from './invite.service'; +import { CreateInvitationDto } from './dto/create-invite.dto'; +import { Response } from 'express'; +import { CreateInviteResponseDto } from './dto/creat-invite-response.dto'; +import { FindAllInvitationsResponseDto } from './dto/all-invitations-response.dto'; +import { FindAllPendingInvitationsResponseDto } from './dto/pending-invitations'; +import { ErrorResponseDto } from './dto/invite-error-response.dto'; +import { SendInvitationsResponseDto } from './dto/send-invitations-response.dto'; + +import { AcceptInviteDto } from './dto/accept-invite.dto'; +import { OwnershipGuard } from '../../guards/authorization.guard'; +import * as SYS_MSG from '../../helpers/SystemMessages'; @ApiBearerAuth() @ApiTags('Organisation Invites') @Controller('organizations') @@ -19,14 +34,52 @@ export class InviteController { @ApiOperation({ summary: 'Get All Invitations' }) @ApiResponse({ status: 200, - description: 'The found record', + description: 'Successfully fetched all invitations', + type: FindAllInvitationsResponseDto, + }) + @ApiResponse({ + status: 500, + description: 'Internal server error', + type: ErrorResponseDto, }) @Get('invites') async findAllInvitations() { const allInvites = await this.inviteService.findAllInvitations(); return allInvites; } - + @ApiOperation({ summary: 'Get All Pending Invitations' }) + @ApiResponse({ + status: 200, + description: 'Successfully fetched all pending invitations', + type: FindAllPendingInvitationsResponseDto, + }) + @ApiResponse({ + status: 500, + description: 'Internal server error', + type: ErrorResponseDto, + }) + @Get('invites/pending') + async findAllPendingInvitations() { + const allPendingInvites = await this.inviteService.getPendingInvites(); + return allPendingInvites; + } + @ApiOperation({ summary: 'Generate Invite Link for an Organization' }) + @ApiResponse({ + status: 200, + description: 'Invite link generated successfully', + type: CreateInviteResponseDto, + }) + @ApiResponse({ + status: 404, + description: 'Organization not found', + type: ErrorResponseDto, + }) + @ApiResponse({ + status: 500, + description: 'Internal server error', + type: ErrorResponseDto, + }) + @UseGuards(OwnershipGuard) @Get(':org_id/invite') async generateInviteLink(@Param('org_id') organizationId: string): Promise<{ link: string }> { try { @@ -41,4 +94,48 @@ export class InviteController { } } } + + @ApiOperation({ summary: 'Send Invitations to Multiple Emails' }) + @ApiResponse({ + status: 200, + description: 'Invitation(s) sent successfully', + type: SendInvitationsResponseDto, + }) + @ApiResponse({ + status: 400, + description: 'Bad Request', + type: ErrorResponseDto, + }) + @ApiResponse({ + status: 404, + description: 'Organization not found', + type: ErrorResponseDto, + }) + @ApiResponse({ + status: 500, + description: 'Internal server error', + type: ErrorResponseDto, + }) + @Post('send-invite') + async sendInvitations(@Body() createInvitationDto: CreateInvitationDto, @Res() res: Response): Promise { + await this.inviteService.sendInvitations(createInvitationDto); + } + + @ApiOperation({ summary: 'Accept Invite To Organisation' }) + @ApiResponse({ + status: 201, + description: SYS_MSG.MEMBER_ALREADY_SUCCESSFULLY, + }) + @ApiResponse({ + status: 409, + description: SYS_MSG.MEMBER_ALREADY_EXISTS, + }) + @ApiResponse({ + status: 404, + description: SYS_MSG.ORG_NOT_FOUND, + }) + @Post('accept-invite') + async acceptInvite(@Body() acceptInviteDto: AcceptInviteDto) { + return await this.inviteService.acceptInvite(acceptInviteDto); + } } diff --git a/src/modules/invite/invite.module.ts b/src/modules/invite/invite.module.ts index c2e181b29..08921775e 100644 --- a/src/modules/invite/invite.module.ts +++ b/src/modules/invite/invite.module.ts @@ -4,10 +4,29 @@ import { InviteController } from './invite.controller'; import { Invite } from './entities/invite.entity'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Organisation } from '../organisations/entities/organisations.entity'; +import { OrganisationsService } from '../organisations/organisations.service'; +import { User } from '../user/entities/user.entity'; +import { EmailService } from '../email/email.service'; +import QueueService from '../email/queue.service'; +import { BullModule } from '@nestjs/bull'; +import { Profile } from '../profile/entities/profile.entity'; +import { Role } from '../role/entities/role.entity'; +import { OrganisationUserRole } from '../role/entities/organisation-user-role.entity'; +import UserService from '../user/user.service'; +import { Permissions } from '../../modules/permissions/entities/permissions.entity'; +import { ConfigModule } from '@nestjs/config'; @Module({ - imports: [TypeOrmModule.forFeature([Invite, Organisation])], + imports: [ + TypeOrmModule.forFeature([Invite, Organisation, User, Profile, Role, Permissions, OrganisationUserRole]), + BullModule.registerQueue({ + name: 'emailSending', + }), + ConfigModule.forRoot({ + isGlobal: true, + }), + ], controllers: [InviteController], - providers: [InviteService], + providers: [InviteService, EmailService, QueueService, OrganisationsService, UserService], }) export class InviteModule {} diff --git a/src/modules/invite/invite.service.ts b/src/modules/invite/invite.service.ts index 676d6f989..7f5c79509 100644 --- a/src/modules/invite/invite.service.ts +++ b/src/modules/invite/invite.service.ts @@ -1,18 +1,62 @@ -import { HttpStatus, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'; +import { + BadRequestException, + HttpStatus, + Injectable, + InternalServerErrorException, + NotFoundException, +} from '@nestjs/common'; import { InviteDto } from './dto/invite.dto'; import { Invite } from './entities/invite.entity'; import { Organisation } from '../../modules/organisations/entities/organisations.entity'; import { Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; import { v4 as uuidv4 } from 'uuid'; +import { AcceptInviteDto } from './dto/accept-invite.dto'; +import * as SYS_MSG from '../../helpers/SystemMessages'; +import { User } from '../user/entities/user.entity'; +import { MailerService } from '@nestjs-modules/mailer'; +import { OrganisationsService } from '../organisations/organisations.service'; +import { CreateInvitationDto } from './dto/create-invite.dto'; +import { EmailService } from '../email/email.service'; +import { CustomHttpException } from '../../helpers/custom-http-filter'; +import { ConfigService } from '@nestjs/config'; @Injectable() export class InviteService { constructor( @InjectRepository(Invite) private inviteRepository: Repository, - @InjectRepository(Organisation) private organisationRepository: Repository + @InjectRepository(Organisation) private organisationRepository: Repository, + @InjectRepository(User) private userRepository: Repository, + private readonly mailerService: MailerService, + private readonly emailService: EmailService, + private readonly configService: ConfigService, + private readonly OrganisationService: OrganisationsService ) {} + async getPendingInvites(): Promise<{ message: string; data: InviteDto[] }> { + try { + const pendingInvites = await this.inviteRepository.find({ + where: { isAccepted: false }, + }); + + const pendingInvitesDto: InviteDto[] = pendingInvites.map(invite => { + return { + token: invite.token, + id: invite.id, + isAccepted: invite.isAccepted, + isGeneric: invite.isGeneric, + organisation: invite.organisation, + email: invite.email, + }; + }); + return { + message: 'Successfully fetched pending Invites', + data: pendingInvitesDto, + }; + } catch (error) { + throw new InternalServerErrorException(`Internal server error: ${error.message}`); + } + } async findAllInvitations(): Promise<{ status_code: number; message: string; data: InviteDto[] }> { try { const invites = await this.inviteRepository.find(); @@ -39,7 +83,6 @@ export class InviteService { throw new InternalServerErrorException(`Internal server error: ${error.message}`); } } - async createInvite(organisationId: string) { const organisation = await this.organisationRepository.findOne({ where: { id: organisationId } }); if (!organisation) { @@ -56,7 +99,8 @@ export class InviteService { await this.inviteRepository.save(invite); - const link = `${process.env.FRONTEND_URL}/invite?token=${token}`; + const frontendUrl = this.configService.get('FRONTEND_URL'); + const link = `${frontendUrl}/invite?token=${token}`; const responseData = { status_code: HttpStatus.OK, @@ -66,4 +110,92 @@ export class InviteService { return responseData; } + + async acceptInvite(acceptInviteDto: AcceptInviteDto) { + const { token, email } = acceptInviteDto; + const invite = await this.inviteRepository.findOne({ where: { token }, relations: ['organisation'] }); + + if (!invite) { + throw new CustomHttpException(SYS_MSG.INVITE_NOT_FOUND, HttpStatus.NOT_FOUND); + } + + if (!invite.isGeneric && invite.email !== email) { + throw new CustomHttpException(SYS_MSG.INVITE_ACCEPTED, HttpStatus.BAD_REQUEST); + } + + if (invite.isAccepted) { + throw new CustomHttpException(SYS_MSG.INVITE_ACCEPTED, HttpStatus.BAD_REQUEST); + } + + const user = await this.userRepository.findOne({ where: { email } }); + + if (!user) { + throw new CustomHttpException(SYS_MSG.USER_NOT_REGISTERED, HttpStatus.NOT_FOUND); + } + + const response = await this.OrganisationService.addOrganisationMember(invite.organisation.id, { + user_id: user.id, + }); + + if (response.status === 'success') { + invite.isAccepted = true; + await this.inviteRepository.save(invite); + return response; + } else { + throw new CustomHttpException(SYS_MSG.MEMBER_NOT_ADDED, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + async sendInvitations(createInvitationDto: CreateInvitationDto): Promise { + if (!Array.isArray(createInvitationDto.emails) || createInvitationDto.emails.length === 0) { + throw new CustomHttpException('Emails field is required and must be an array.', HttpStatus.BAD_REQUEST); + } + + const invalidEmails = createInvitationDto.emails.filter( + email => !/^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$/.test(email) + ); + if (invalidEmails.length > 0) { + throw new CustomHttpException('One or more email addresses are not valid', HttpStatus.BAD_REQUEST); + } + + const organisation = await this.organisationRepository.findOne({ where: { id: createInvitationDto.org_id } }); + if (!organisation) { + throw new CustomHttpException('Organization not found', HttpStatus.NOT_FOUND); + } + + if (createInvitationDto.emails.length > 50) { + throw new CustomHttpException('Cannot send more than 50 invitations at once', HttpStatus.BAD_REQUEST); + } + + const templateResponse = await this.emailService.getTemplate({ templateName: 'organization-invitation' }); + if (templateResponse.status_code !== 200) { + throw new CustomHttpException('Invitation template not found', HttpStatus.BAD_REQUEST); + } + + const template = templateResponse.template.toString(); + + const invitations = []; + for (const email of createInvitationDto.emails) { + const inviteLinkData = await this.createInvite(createInvitationDto.org_id); + const inviteLink = inviteLinkData.link; + + const personalizedContent = template + .replace(`{{organizationName}}`, organisation.name) + .replace('{{recipientName}}', email.split('@')[0]) + .replace('{{invitationLink}}', inviteLink); + + invitations.push({ email, inviteLink }); + + await this.mailerService.sendMail({ + to: email, + subject: 'Invitation to join an organization', + html: personalizedContent, + }); + } + + return { + message: 'Invitation(s) sent successfully', + invitations, + }; + } } diff --git a/src/modules/invite/mocks/mockOrg.ts b/src/modules/invite/mocks/mockOrg.ts index b68c3dc2c..1554ade4c 100644 --- a/src/modules/invite/mocks/mockOrg.ts +++ b/src/modules/invite/mocks/mockOrg.ts @@ -15,10 +15,8 @@ export const mockOrg: Organisation = { state: 'state1', isDeleted: false, owner: mockUser, - creator: mockUser, preferences: [], invites: [], - role: [], - organisationMembers: [], products: [], + members: null, }; diff --git a/src/modules/invite/mocks/mockUser.ts b/src/modules/invite/mocks/mockUser.ts index 592fee51d..6e820f3b5 100644 --- a/src/modules/invite/mocks/mockUser.ts +++ b/src/modules/invite/mocks/mockUser.ts @@ -1,3 +1,4 @@ +import { mockProfile } from '../../../modules/profile/mocks/profileMock'; import { User, UserType } from '../../user/entities/user.entity'; export const mockUser: User = { @@ -6,14 +7,13 @@ export const mockUser: User = { last_name: 'Doe', is_active: true, phone: '+1234567890', + status: 'Hello from the children of planet Earth', id: 'some-uuid-value-here', attempts_left: 2, created_at: new Date(), updated_at: new Date(), - user_type: UserType.ADMIN, backup_codes: [], owned_organisations: [], - created_organisations: [], jobs: [], hashPassword: () => null, password: 'password123', @@ -21,8 +21,11 @@ export const mockUser: User = { secret: 'secret', is_2fa_enabled: true, testimonials: [], - profile: null, - organisationMembers: [], + profile: mockProfile, notification_settings: [], notifications: [], + blogs: [], + comments: [], + cart: [], + organisations: null, }; diff --git a/src/modules/invite/tests/invite.service.spec.ts b/src/modules/invite/tests/invite.service.spec.ts index 36fff73f9..5f7fbf39e 100644 --- a/src/modules/invite/tests/invite.service.spec.ts +++ b/src/modules/invite/tests/invite.service.spec.ts @@ -1,4 +1,4 @@ -import { HttpStatus, InternalServerErrorException, NotFoundException } from '@nestjs/common'; +import { HttpStatus, BadRequestException, NotFoundException, InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; @@ -6,32 +6,136 @@ import { Organisation } from '../../organisations/entities/organisations.entity' import { User } from '../../user/entities/user.entity'; import { Invite } from '../entities/invite.entity'; import { InviteService } from '../invite.service'; -import { v4 as uuidv4 } from 'uuid'; +import { MailerService } from '@nestjs-modules/mailer'; import { mockInvitesResponse } from '../mocks/mockInvitesReponse'; import { mockInvites } from '../mocks/mockInvites'; import { mockOrg } from '../mocks/mockOrg'; +import { v4 as uuidv4 } from 'uuid'; +import { EmailService } from '../../../modules/email/email.service'; +import { CreateInvitationDto } from '../dto/create-invite.dto'; +import { CustomHttpException } from '../../../helpers/custom-http-filter'; + +import { OrganisationsService } from '../../../modules/organisations/organisations.service'; +import { mockUser } from '../mocks/mockUser'; +import { orgMock } from '../../../modules/organisations/tests/mocks/organisation.mock'; +import { Role } from '../../../modules/role/entities/role.entity'; +import { Permissions } from '../../../modules/permissions/entities/permissions.entity'; +import { ConfigModule, ConfigService } from '@nestjs/config'; jest.mock('uuid'); describe('InviteService', () => { let service: InviteService; let repository: Repository; let organisationRepo: Repository; + let emailService: EmailService; + let mailerService: MailerService; + let userRepository: Repository; + let permissionRepository: Repository; + let organisationService: OrganisationsService; + let configService: ConfigService; + let frontendUrl: string; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ + imports: [ConfigModule.forRoot()], providers: [ InviteService, + OrganisationsService, + ConfigService, { provide: getRepositoryToken(Invite), - useClass: Repository, + useValue: { + find: jest.fn(), + findBy: jest.fn(), + findOne: jest.fn(), + create: jest.fn(), + save: jest.fn(), + findOneBy: jest.fn(), + update: jest.fn(), + }, + }, + { + provide: getRepositoryToken(User), + useValue: { + findBy: jest.fn(), + findOne: jest.fn(), + create: jest.fn(), + save: jest.fn(), + findOneBy: jest.fn(), + update: jest.fn(), + }, }, { provide: getRepositoryToken(Organisation), - useClass: Repository, + useValue: { + findBy: jest.fn(), + findOne: jest.fn(), + create: jest.fn(), + save: jest.fn(), + findOneBy: jest.fn(), + update: jest.fn(), + }, + }, + + { + provide: getRepositoryToken(Role), + useValue: { + findBy: jest.fn(), + findOne: jest.fn(), + create: jest.fn(), + save: jest.fn(), + findOneBy: jest.fn(), + update: jest.fn(), + find: jest.fn(), + }, + }, + { + provide: getRepositoryToken(Permissions), + useValue: { + findBy: jest.fn(), + findOne: jest.fn(), + create: jest.fn(), + save: jest.fn(), + findOneBy: jest.fn(), + update: jest.fn(), + find: jest.fn(), + }, + }, + { + provide: getRepositoryToken(Permissions), + useValue: { + findBy: jest.fn(), + findOne: jest.fn(), + create: jest.fn(), + save: jest.fn(), + findOneBy: jest.fn(), + update: jest.fn(), + find: jest.fn(), + }, }, { provide: getRepositoryToken(User), - useClass: Repository, + useValue: { + findOne: jest.fn(), + }, + }, + { + provide: OrganisationsService, + useValue: { + addOrganisationMember: jest.fn(), + }, + }, + { + provide: EmailService, + useValue: { + getTemplate: jest.fn(), + }, + }, + { + provide: MailerService, + useValue: { + sendMail: jest.fn(), + }, }, ], }).compile(); @@ -39,6 +143,13 @@ describe('InviteService', () => { service = module.get(InviteService); repository = module.get>(getRepositoryToken(Invite)); organisationRepo = module.get>(getRepositoryToken(Organisation)); + emailService = module.get(EmailService); + mailerService = module.get(MailerService); + service = module.get(InviteService); + userRepository = module.get>(getRepositoryToken(User)); + organisationService = module.get(OrganisationsService); + configService = module.get(ConfigService); + frontendUrl = configService.get('FRONTEND_URL'); }); it('should fetch all invites', async () => { @@ -56,9 +167,27 @@ describe('InviteService', () => { it('should throw an internal server error if an exception occurs', async () => { jest.spyOn(repository, 'find').mockRejectedValue(new Error('Test error')); - await expect(service.findAllInvitations()).rejects.toThrow(InternalServerErrorException); + await expect(service.getPendingInvites()).rejects.toThrow(InternalServerErrorException); }); + it('should fetch all pending invites where isAccepted is false', async () => { + const pendingInvitesMock = mockInvites.filter(invite => invite.isAccepted === false); + jest.spyOn(repository, 'find').mockResolvedValue(pendingInvitesMock); + + const result = await service.getPendingInvites(); + + expect(result).toEqual({ + message: 'Successfully fetched pending Invites', + data: pendingInvitesMock.map(invite => ({ + token: invite.token, + id: invite.id, + isAccepted: invite.isAccepted, + isGeneric: invite.isGeneric, + organisation: invite.organisation, + email: invite.email, + })), + }); + }); describe('createInvite', () => { it('should create an invite and return a link', async () => { const mockToken = 'mock-uuid'; @@ -74,7 +203,7 @@ describe('InviteService', () => { expect(result).toEqual({ status_code: HttpStatus.OK, message: 'Invite link generated successfully', - link: `${process.env.FRONTEND_URL}/invite?token=${mockToken}`, + link: `${frontendUrl}/invite?token=${mockToken}`, }); expect(organisationRepo.findOne).toHaveBeenCalledWith({ where: { id: '1' } }); @@ -92,4 +221,182 @@ describe('InviteService', () => { await expect(service.createInvite('1')).rejects.toThrow(NotFoundException); }); }); + + describe('sendInvitations', () => { + it('should throw an error if emails field is not an array', async () => { + const dto: CreateInvitationDto = { + emails: [], + org_id: 'valid-org-id', + }; + + await expect(service.sendInvitations(dto)).rejects.toThrow( + new CustomHttpException('Emails field is required and must be an array.', HttpStatus.BAD_REQUEST) + ); + }); + + it('should throw an error if one or more email addresses are invalid', async () => { + const dto: CreateInvitationDto = { + emails: ['invalid-email'], + org_id: 'valid-org-id', + }; + + await expect(service.sendInvitations(dto)).rejects.toThrow( + new CustomHttpException('One or more email addresses are not valid', HttpStatus.BAD_REQUEST) + ); + }); + + it('should throw an error if the organization is not found', async () => { + const dto: CreateInvitationDto = { + emails: ['valid@example.com'], + org_id: 'invalid-org-id', + }; + + jest.spyOn(organisationRepo, 'findOne').mockResolvedValue(null); + + await expect(service.sendInvitations(dto)).rejects.toThrow( + new CustomHttpException('Organization not found', HttpStatus.NOT_FOUND) + ); + }); + + it('should throw an error if more than 50 emails are provided', async () => { + const dto: CreateInvitationDto = { + emails: new Array(51).fill('valid@example.com'), + org_id: 'valid-org-id', + }; + + jest.spyOn(organisationRepo, 'findOne').mockResolvedValue(mockOrg as Organisation); + + await expect(service.sendInvitations(dto)).rejects.toThrow( + new CustomHttpException('Cannot send more than 50 invitations at once', HttpStatus.BAD_REQUEST) + ); + }); + + it('should throw an error if the invitation template is not found', async () => { + const dto: CreateInvitationDto = { + emails: ['valid@example.com'], + org_id: 'valid-org-id', + }; + + jest.spyOn(organisationRepo, 'findOne').mockResolvedValue(mockOrg as Organisation); + jest.spyOn(emailService, 'getTemplate').mockResolvedValue({ status_code: 404, message: 'Template not found' }); + + await expect(service.sendInvitations(dto)).rejects.toThrow( + new CustomHttpException('Invitation template not found', HttpStatus.BAD_REQUEST) + ); + }); + + it('should send invitations successfully', async () => { + const dto: CreateInvitationDto = { + emails: ['valid@example.com'], + org_id: 'valid-org-id', + }; + + const mockToken = 'mock-uuid'; + const inviteLinkData = { link: `${frontendUrl}/invite?token=${mockToken}` }; + const template = '...'; + const mockOrgData = { name: 'Test Org' }; + + jest.spyOn(organisationRepo, 'findOne').mockResolvedValue(mockOrgData as Organisation); + jest + .spyOn(emailService, 'getTemplate') + .mockResolvedValue({ status_code: 200, message: 'Template retrieved successfully', template }); + jest.spyOn(service, 'createInvite').mockResolvedValue({ + status_code: HttpStatus.OK, + message: 'Invite link generated successfully', + link: `${frontendUrl}/invite?token=${mockToken}`, + }); + jest.spyOn(mailerService, 'sendMail').mockResolvedValue({}); + + const result = await service.sendInvitations(dto); + + expect(result).toEqual({ + message: 'Invitation(s) sent successfully', + invitations: [{ email: 'valid@example.com', inviteLink: inviteLinkData.link }], + }); + expect(mailerService.sendMail).toHaveBeenCalledWith({ + to: 'valid@example.com', + subject: 'Invitation to join an organization', + html: expect.any(String), + }); + }); + describe('Accept Invite Service', () => { + it('should throw NotFoundException if invite not found', async () => { + jest.spyOn(repository, 'findOne').mockResolvedValue(null); + + await expect(service.acceptInvite({ token: 'invalid-token', email: 'test@example.com' })).rejects.toThrow( + CustomHttpException + ); + }); + + it('should throw BadRequestException if email does not match non-generic invite', async () => { + const mockInvite = { + id: 'some-id', + token: 'valid-token', + email: 'test@example.com', + isGeneric: false, + isAccepted: true, + organisation: null, + created_at: new Date(), + updated_at: new Date(), + }; + + jest.spyOn(repository, 'findOne').mockResolvedValue(mockInvite); + + await expect(service.acceptInvite({ token: 'valid-token', email: 'wrong@example.com' })).rejects.toThrow( + CustomHttpException + ); + }); + + it('should throw BadRequestException if invite already accepted', async () => { + const mockInvite = { + id: 'some-id', + token: 'valid-token', + email: 'test@example.com', + isGeneric: false, + isAccepted: true, + organisation: null, + created_at: new Date(), + updated_at: new Date(), + }; + + jest.spyOn(repository, 'findOne').mockResolvedValue(mockInvite); + + await expect(service.acceptInvite({ token: 'valid-token', email: 'test@example.com' })).rejects.toThrow( + CustomHttpException + ); + }); + + it('should throw NotFoundException if user not found', async () => { + jest.spyOn(repository, 'findOne').mockResolvedValue(null); + jest.spyOn(userRepository, 'findOne').mockResolvedValue(null); + + await expect(service.acceptInvite({ token: 'valid-token', email: 'test@example.com' })).rejects.toThrow( + CustomHttpException + ); + }); + + it('should throw InternalServerErrorException if adding member fails', async () => { + const mockInvite = { + id: 'some-id', + token: 'valid-token', + email: 'test@example.com', + isGeneric: false, + isAccepted: false, + organisation: orgMock, + created_at: new Date(), + updated_at: new Date(), + }; + + jest.spyOn(repository, 'findOne').mockResolvedValue(mockInvite); + jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser); + jest + .spyOn(organisationService, 'addOrganisationMember') + .mockResolvedValue({ status: 'error', message: 'Member added', member: mockUser }); + + await expect(service.acceptInvite({ token: 'valid-token', email: 'test@example.com' })).rejects.toThrow( + CustomHttpException + ); + }); + }); + }); }); diff --git a/src/modules/jobs/docs/jobs-swagger.ts b/src/modules/jobs/docs/jobs-swagger.ts new file mode 100644 index 000000000..2c60ac25e --- /dev/null +++ b/src/modules/jobs/docs/jobs-swagger.ts @@ -0,0 +1,79 @@ +import { applyDecorators } from '@nestjs/common'; +import { + ApiTags, + ApiQuery, + ApiInternalServerErrorResponse, + ApiBearerAuth, + ApiBadRequestResponse, + ApiUnprocessableEntityResponse, + ApiResponse, + ApiCreatedResponse, + ApiBody, + ApiOperation, +} from '@nestjs/swagger'; +import { JobApplicationDto } from '../dto/job-application.dto'; +import { JobApplicationResponseDto } from '../dto/job-application-response.dto'; +import { JobApplicationErrorDto } from '../dto/job-application-error.dto'; + +export function SubmitJobApplicationDocs() { + return applyDecorators( + ApiOperation({ summary: 'Submit job application' }), + ApiBody({ + type: JobApplicationDto, + description: 'Job application request body', + }), + ApiCreatedResponse({ + status: 201, + description: 'Job application submitted successfully', + type: JobApplicationResponseDto, + }), + ApiUnprocessableEntityResponse({ + description: 'Job application deadline passed', + status: 422, + }), + ApiBadRequestResponse({ status: 400, description: 'Invalid request body', type: JobApplicationErrorDto }), + ApiInternalServerErrorResponse({ status: 500, description: 'Internal server error', type: JobApplicationErrorDto }) + ); +} +export function CreateNewJobDocs() { + return applyDecorators( + ApiOperation({ summary: 'Create a new job' }), + ApiResponse({ status: 201, description: 'Job created successfully' }), + ApiResponse({ status: 404, description: 'User not found' }) + ); +} + +export function SearchForJoblistingsDocs() { + return applyDecorators( + ApiOperation({ summary: 'Search for job listings' }), + ApiQuery({ name: 'page', required: false, type: Number }), + ApiQuery({ name: 'limit', required: false, type: Number }), + ApiResponse({ status: 200, description: 'Successful response' }), + ApiResponse({ status: 400, description: 'Bad request' }) + ); +} + +export function GetsAllJobsDocs() { + return applyDecorators( + ApiOperation({ summary: 'Gets all jobs' }), + ApiResponse({ status: 200, description: 'Jobs returned successfully' }), + ApiResponse({ status: 404, description: 'Job not found' }) + ); +} + +export function GetAJobByIDDocs() { + return applyDecorators( + ApiOperation({ summary: 'Gets a job by ID' }), + ApiResponse({ status: 200, description: 'Job returned successfully' }), + ApiResponse({ status: 404, description: 'Job not found' }) + ); +} + +export function DeleteAJobDocs() { + return applyDecorators( + ApiOperation({ summary: 'Delete a job' }), + ApiResponse({ status: 200, description: 'Job deleted successfully' }), + ApiResponse({ status: 403, description: 'You do not have permission to perform this action' }), + ApiResponse({ status: 404, description: 'Job not found' }) + ); +} diff --git a/src/modules/jobs/dto/find-job-response.dto.ts b/src/modules/jobs/dto/find-job-response.dto.ts new file mode 100644 index 000000000..210de4cb4 --- /dev/null +++ b/src/modules/jobs/dto/find-job-response.dto.ts @@ -0,0 +1,7 @@ +import { Job } from '../entities/job.entity'; + +export class FindJobResponseDto { + message: string; + status_code: number; + data: Job; +} diff --git a/src/modules/jobs/dto/job-application-error.dto.ts b/src/modules/jobs/dto/job-application-error.dto.ts new file mode 100644 index 000000000..333f9d1de --- /dev/null +++ b/src/modules/jobs/dto/job-application-error.dto.ts @@ -0,0 +1,5 @@ +export class JobApplicationErrorDto { + 'error': 'string'; + 'message': 'string' | ['string']; + 'status_code': 'string'; +} diff --git a/src/modules/jobs/dto/job-application-response.dto.ts b/src/modules/jobs/dto/job-application-response.dto.ts new file mode 100644 index 000000000..75bc30800 --- /dev/null +++ b/src/modules/jobs/dto/job-application-response.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class JobApplicationResponseDto { + @ApiProperty({ + example: 'success', + }) + status: string; + + @ApiProperty({ + example: 'Application submitted successfully', + }) + message: string; + + @ApiProperty({ + example: 200, + }) + status_code: number; +} diff --git a/src/modules/jobs/dto/job-application.dto.ts b/src/modules/jobs/dto/job-application.dto.ts new file mode 100644 index 000000000..4d1413be7 --- /dev/null +++ b/src/modules/jobs/dto/job-application.dto.ts @@ -0,0 +1,28 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; + +export class JobApplicationDto { + @IsNotEmpty() + @IsString() + @ApiProperty({ + example: 'John Doe', + }) + applicant_name: string; + + @IsNotEmpty() + @IsEmail() + @ApiProperty({ + example: 'johndoe@example.com', + }) + email: string; + + @IsNotEmpty() + resume: string; + + @IsNotEmpty() + @IsString() + @ApiProperty({ + example: 'Software Engineer who has worked with Node.js, React, and Angular', + }) + cover_letter: string; +} diff --git a/src/modules/jobs/dto/job.dto.ts b/src/modules/jobs/dto/job.dto.ts index 6cb932aa8..3d4849a27 100644 --- a/src/modules/jobs/dto/job.dto.ts +++ b/src/modules/jobs/dto/job.dto.ts @@ -1,7 +1,6 @@ import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; -import { IsString, IsEnum, IsBoolean, IsNotEmpty, IsOptional, IsDateString } from 'class-validator'; +import { IsString, IsEnum, IsBoolean, IsNotEmpty, IsOptional, IsDateString, IsArray } from 'class-validator'; -// Enum definitions for clarity export enum SalaryRange { 'below_30k' = 'below_30k', '30k_to_50k' = '30k_to_50k', @@ -63,8 +62,8 @@ export class JobDto { @ApiProperty({ description: 'The salary range for the job', enum: SalaryRange, - type: SalaryRange, - example: SalaryRange['30K_to_50K'], + type: String, + example: SalaryRange['30k_to_50k'], required: true, }) @IsEnum(SalaryRange) @@ -74,23 +73,23 @@ export class JobDto { @ApiProperty({ description: 'The type of job', enum: JobType, - type: JobType, - example: JobType['full_time'], + type: String, + example: JobType.FullTime, required: true, }) @IsEnum(JobType) - @IsOptional() + @IsNotEmpty() job_type: string; @ApiProperty({ description: 'The mode of the job (e.g., remote, onsite)', enum: JobMode, - type: JobMode, - example: JobMode['remote'], + type: String, + example: JobMode.Remote, required: true, }) @IsEnum(JobMode) - @IsOptional() + @IsNotEmpty() job_mode: string; @ApiProperty({ @@ -102,6 +101,45 @@ export class JobDto { @IsNotEmpty() company_name: string; + @ApiProperty({ + description: 'List of qualifications required for the job', + example: ["Bachelor's Degree in Computer Science", '5+ years of experience in software development'], + type: [String], + nullable: true, + }) + @IsArray() + @IsOptional() + qualifications?: string[]; + + @ApiProperty({ + description: 'List of key responsibilities for the job', + example: ['Develop and maintain web applications', 'Collaborate with cross-functional teams'], + type: [String], + nullable: true, + }) + @IsArray() + @IsOptional() + key_responsibilities?: string[]; + + @ApiProperty({ + description: 'List of benefits associated with the job', + example: ['Health insurance', '401(k) matching', 'Paid time off'], + type: [String], + nullable: true, + }) + @IsArray() + @IsOptional() + benefits?: string[]; + + @ApiProperty({ + description: 'Required or preferred experience level for the job', + example: 'Senior', + nullable: true, + }) + @IsString() + @IsOptional() + experience_level?: string; + @ApiHideProperty() @IsBoolean() @IsOptional() diff --git a/src/modules/jobs/dto/jobSearch.dto.ts b/src/modules/jobs/dto/jobSearch.dto.ts new file mode 100644 index 000000000..63fc6aafb --- /dev/null +++ b/src/modules/jobs/dto/jobSearch.dto.ts @@ -0,0 +1,31 @@ +import { IsOptional, IsString, IsEnum, IsNumber } from 'class-validator'; +import { JobMode, JobType, SalaryRange } from '../dto/job.dto'; +import { Type } from 'class-transformer'; + +export class JobSearchDto { + @IsOptional() + @IsString() + location?: string; + + @IsOptional() + @IsEnum(SalaryRange) + salary_range?: SalaryRange; + + @IsOptional() + @IsEnum(JobType) + job_type?: JobType; + + @IsOptional() + @IsEnum(JobMode) + job_mode?: JobMode; + + @IsOptional() + @IsNumber() + @Type(() => Number) + page?: number; + + @IsOptional() + @IsNumber() + @Type(() => Number) + limit?: number; +} diff --git a/src/modules/jobs/entities/job-application.entity.ts b/src/modules/jobs/entities/job-application.entity.ts new file mode 100644 index 000000000..17748d1bd --- /dev/null +++ b/src/modules/jobs/entities/job-application.entity.ts @@ -0,0 +1,27 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Column, Entity, ManyToOne } from 'typeorm'; +import { AbstractBaseEntity } from '../../../entities/base.entity'; +import { Job } from './job.entity'; + +@Entity() +export class JobApplication extends AbstractBaseEntity { + @ApiProperty() + @Column({ nullable: false }) + applicant_name: string; + + @ApiProperty() + @Column({ nullable: false }) + email: string; + + @ApiProperty() + @Column({ nullable: false }) + resume: string; + + @ApiProperty() + @Column() + cover_letter: string; + + @ApiProperty() + @ManyToOne(() => Job, job => job.job_application) + job: Job; +} diff --git a/src/modules/jobs/entities/job.entity.ts b/src/modules/jobs/entities/job.entity.ts index 3b4625370..3b28106dc 100644 --- a/src/modules/jobs/entities/job.entity.ts +++ b/src/modules/jobs/entities/job.entity.ts @@ -1,23 +1,29 @@ import { AbstractBaseEntity } from './../../../entities/base.entity'; import { User } from '../../user/entities/user.entity'; -import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; -import { ApiHideProperty } from '@nestjs/swagger'; +import { Column, Entity, ManyToOne, OneToMany } from 'typeorm'; +import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; import { JobMode, JobType, SalaryRange } from '../dto/job.dto'; +import { JobApplication } from './job-application.entity'; @Entity() export class Job extends AbstractBaseEntity { + @ApiProperty({ description: 'The title of the job', example: 'Software Engineer' }) @Column('text', { nullable: false }) title: string; + @ApiProperty({ description: 'A brief description of the job', example: 'Develop and maintain web applications.' }) @Column('text', { nullable: false }) description: string; + @ApiProperty({ description: 'Location where the job is based', example: 'New York, NY' }) @Column('text', { nullable: false }) location: string; + @ApiProperty({ description: 'Deadline for the job application', example: '2024-12-31T23:59:59Z' }) @Column({ type: 'timestamp', nullable: false }) deadline: string; + @ApiProperty({ description: 'Salary range for the job', example: '50,000 - 70,000' }) @Column({ type: 'enum', enum: SalaryRange, @@ -25,6 +31,7 @@ export class Job extends AbstractBaseEntity { }) salary_range: string; + @ApiProperty({ description: 'Type of the job', example: 'full-time', enum: JobType }) @Column({ type: 'enum', enum: JobType, @@ -33,9 +40,11 @@ export class Job extends AbstractBaseEntity { }) job_type: string; + @ApiProperty({ description: 'Mode of the job', example: 'remote', enum: JobMode }) @Column({ type: 'enum', enum: JobMode, default: 'remote', nullable: false }) job_mode: string; + @ApiProperty({ description: 'Name of the company offering the job', example: 'Tech Innovations Inc.' }) @Column('text', { nullable: false }) company_name: string; @@ -46,4 +55,41 @@ export class Job extends AbstractBaseEntity { @ApiHideProperty() @ManyToOne(() => User, user => user.jobs, { nullable: false }) user: User; + + @ApiProperty({ + description: 'List of qualifications required for the job', + example: ["Bachelor's Degree in Computer Science", '5 years of experience in software development'], + type: [String], + nullable: true, + }) + @Column('text', { array: true, nullable: true }) + qualifications: string[]; + + @ApiProperty({ + description: 'List of key responsibilities for the job', + example: ['Develop software solutions', 'Collaborate with cross-functional teams'], + type: [String], + nullable: true, + }) + @Column('text', { array: true, nullable: true }) + key_responsibilities: string[]; + + @ApiProperty({ + description: 'List of benefits associated with the job', + example: ['Health insurance', '401(k) matching', 'Paid time off'], + type: [String], + nullable: true, + }) + @Column('text', { array: true, nullable: true }) + benefits: string[]; + + @ApiProperty({ + description: 'Required or preferred experience level for the job', + example: 'Senior Developer', + nullable: true, + }) + @Column('text', { nullable: true }) + experience_level: string; + @OneToMany(() => JobApplication, job_application => job_application.job) + job_application: JobApplication[]; } diff --git a/src/modules/jobs/guards/job.guard.ts b/src/modules/jobs/guards/job.guard.ts deleted file mode 100644 index 68f9615ba..000000000 --- a/src/modules/jobs/guards/job.guard.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Injectable, CanActivate, ExecutionContext, ForbiddenException, NotFoundException } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Job } from '../entities/job.entity'; -import { Repository } from 'typeorm'; - -@Injectable() -export class JobGuard implements CanActivate { - constructor( - @InjectRepository(Job) - private readonly jobRepository: Repository - ) {} - - async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); - const user = request.user; - const jobId = request.params.id; - const job = await this.jobRepository.findOne({ - where: { id: jobId }, - relations: ['user'], - }); - - if (job.user.id === user.id) { - if (!job || job.is_deleted === true) throw new NotFoundException('Job not found'); - return true; - } else { - throw new ForbiddenException('You do not have permission to perform this action'); - } - } -} diff --git a/src/modules/jobs/jobs.controller.ts b/src/modules/jobs/jobs.controller.ts index ea5715df9..90f9ee59e 100644 --- a/src/modules/jobs/jobs.controller.ts +++ b/src/modules/jobs/jobs.controller.ts @@ -1,18 +1,67 @@ -import { Body, Controller, Delete, Get, Param, Patch, Post, Query, Request, UseGuards } from '@nestjs/common'; -import { JobsService } from './jobs.service'; -import { JobDto } from './dto/job.dto'; -import { PaginationDto } from './dto/pagination.dto'; -import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { JobGuard } from './guards/job.guard'; +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Query, + Request, + UseGuards, + ValidationPipe, + ParseUUIDPipe, +} from '@nestjs/common'; +import { + ApiBadRequestResponse, + ApiBearerAuth, + ApiBody, + ApiCreatedResponse, + ApiInternalServerErrorResponse, + ApiOperation, + ApiQuery, + ApiResponse, + ApiTags, + ApiUnprocessableEntityResponse, +} from '@nestjs/swagger'; import { skipAuth } from '../../helpers/skipAuth'; +import { JobApplicationErrorDto } from './dto/job-application-error.dto'; +import { JobApplicationResponseDto } from './dto/job-application-response.dto'; +import { JobApplicationDto } from './dto/job-application.dto'; +import { JobDto } from './dto/job.dto'; +import { JobsService } from './jobs.service'; +import { SuperAdminGuard } from '../../guards/super-admin.guard'; +import { JobSearchDto } from './dto/jobSearch.dto'; @ApiTags('Jobs') -@ApiBearerAuth() @Controller('jobs') export class JobsController { constructor(private readonly jobService: JobsService) {} + @skipAuth() + @ApiOperation({ summary: 'Submit job application' }) + @ApiBody({ + type: JobApplicationDto, + description: 'Job application request body', + }) + @ApiCreatedResponse({ + status: 201, + description: 'Job application submitted successfully', + type: JobApplicationResponseDto, + }) + @ApiUnprocessableEntityResponse({ + description: 'Job application deadline passed', + status: 422, + }) + @ApiBadRequestResponse({ status: 400, description: 'Invalid request body', type: JobApplicationErrorDto }) + @ApiInternalServerErrorResponse({ status: 500, description: 'Internal server error', type: JobApplicationErrorDto }) + @Post('/:id/applications') + async applyForJob(@Param('id') id: string, @Body() jobApplicationDto: JobApplicationDto) { + return this.jobService.applyForJob(id, jobApplicationDto); + } + + @UseGuards(SuperAdminGuard) @Post('/') + @ApiBearerAuth() @ApiOperation({ summary: 'Create a new job' }) @ApiResponse({ status: 201, description: 'Job created successfully' }) @ApiResponse({ status: 404, description: 'User not found' }) @@ -21,11 +70,29 @@ export class JobsController { return this.jobService.create(createJobDto, user.sub); } + @skipAuth() + @Get('search') + @ApiOperation({ summary: 'Search for job listings' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'limit', required: false, type: Number }) + @ApiResponse({ status: 200, description: 'Successful response' }) + @ApiResponse({ status: 400, description: 'Bad request' }) + async searchJobs( + @Query(new ValidationPipe({ transform: true, forbidNonWhitelisted: true })) + searchDto: JobSearchDto + ) { + const page = searchDto.page || 1; + const limit = searchDto.limit || 10; + const { page: _, limit: __, ...otherSearchParams } = searchDto; + + return this.jobService.searchJobs(otherSearchParams, page, limit); + } + @skipAuth() @Get('/') @ApiOperation({ summary: 'Gets all jobs' }) @ApiResponse({ status: 200, description: 'Jobs returned successfully' }) - @ApiResponse({ status: 404, description: 'User not found' }) + @ApiResponse({ status: 404, description: 'Job not found' }) async getAllJobs() { return this.jobService.getJobs(); } @@ -35,17 +102,18 @@ export class JobsController { @ApiOperation({ summary: 'Gets a job by ID' }) @ApiResponse({ status: 200, description: 'Job returned successfully' }) @ApiResponse({ status: 404, description: 'Job not found' }) - async getJob(@Param('id') id: string) { + async getJob(@Param('id', ParseUUIDPipe) id) { return this.jobService.getJob(id); } - @UseGuards(JobGuard) + @UseGuards(SuperAdminGuard) @Delete('/:id') + @ApiBearerAuth() @ApiOperation({ summary: 'Delete a job' }) @ApiResponse({ status: 200, description: 'Job deleted successfully' }) @ApiResponse({ status: 403, description: 'You do not have permission to perform this action' }) @ApiResponse({ status: 404, description: 'Job not found' }) - async delete(@Param('id') id: string) { + async delete(@Param('id', ParseUUIDPipe) id) { return this.jobService.delete(id); } } diff --git a/src/modules/jobs/jobs.module.ts b/src/modules/jobs/jobs.module.ts index fe1c3cc75..b9af045d3 100644 --- a/src/modules/jobs/jobs.module.ts +++ b/src/modules/jobs/jobs.module.ts @@ -1,13 +1,21 @@ import { Module } from '@nestjs/common'; -import { JobsService } from './jobs.service'; -import { JobsController } from './jobs.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { Job } from './entities/job.entity'; import { User } from '../user/entities/user.entity'; import { UserModule } from '../user/user.module'; +import { JobApplication } from './entities/job-application.entity'; +import { Job } from './entities/job.entity'; +import { JobsController } from './jobs.controller'; +import { JobsService } from './jobs.service'; +import { Role } from '../role/entities/role.entity'; +import { Profile } from '../profile/entities/profile.entity'; +import { OrganisationUserRole } from '../role/entities/organisation-user-role.entity'; +import { Organisation } from '../organisations/entities/organisations.entity'; @Module({ - imports: [TypeOrmModule.forFeature([Job, User]), UserModule], + imports: [ + TypeOrmModule.forFeature([Job, User, JobApplication, Organisation, OrganisationUserRole, Profile, Role]), + UserModule, + ], providers: [JobsService], controllers: [JobsController], }) diff --git a/src/modules/jobs/jobs.service.ts b/src/modules/jobs/jobs.service.ts index b212e937f..728b3a7e1 100644 --- a/src/modules/jobs/jobs.service.ts +++ b/src/modules/jobs/jobs.service.ts @@ -1,11 +1,18 @@ -import { Injectable, NotFoundException, UnauthorizedException, UseGuards } from '@nestjs/common'; +import { HttpStatus, Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { User } from '../user/entities/user.entity'; import { Repository } from 'typeorm'; -import { Job } from './entities/job.entity'; -import { JobDto } from './dto/job.dto'; +import { CustomHttpException } from '../../helpers/custom-http-filter'; import { pick } from '../../helpers/pick'; -import { PaginationDto } from './dto/pagination.dto'; +import * as SYS_MSG from '../../helpers/SystemMessages'; +import { User } from '../user/entities/user.entity'; +import { FindJobResponseDto } from './dto/find-job-response.dto'; +import { JobApplicationResponseDto } from './dto/job-application-response.dto'; +import { JobApplicationDto } from './dto/job-application.dto'; +import { JobDto } from './dto/job.dto'; +import { JobApplication } from './entities/job-application.entity'; +import { Job } from './entities/job.entity'; +import { isPassed } from './utils/helpers'; +import { JobSearchDto } from './dto/jobSearch.dto'; @Injectable() export class JobsService { @@ -13,32 +20,60 @@ export class JobsService { @InjectRepository(User) private readonly userRepository: Repository, @InjectRepository(Job) - private readonly jobRepository: Repository + private readonly jobRepository: Repository, + @InjectRepository(JobApplication) + private readonly jobApplicationRepository: Repository ) {} + async applyForJob(jobId: string, jobApplicationDto: JobApplicationDto): Promise { + const job: FindJobResponseDto = await this.getJob(jobId); + + const { is_deleted, deadline } = job.data; + + if (is_deleted) { + throw new CustomHttpException('Job deleted', HttpStatus.NOT_FOUND); + } + + if (isPassed(deadline)) { + throw new CustomHttpException(SYS_MSG.DEADLINE_PASSED, HttpStatus.UNPROCESSABLE_ENTITY); + } + + const { resume, applicant_name, ...others } = jobApplicationDto; + + // TODO: Upload resume to the cloud and grab URL + + const resumeUrl = `https://example.com/${applicant_name.split(' ').join('_')}.pdf`; + + const createJobApplication = this.jobApplicationRepository.create({ + ...others, + applicant_name, + resume: resumeUrl, + ...job, + }); + + await this.jobApplicationRepository.save(createJobApplication); + + return { + status: 'success', + message: 'Application submitted successfully', + status_code: HttpStatus.CREATED, + }; + } + async create(createJobDto: JobDto, userId: string) { - // Check if the user exists const user = await this.userRepository.findOne({ where: { id: userId }, }); - if (!user) - throw new NotFoundException({ - status_code: 404, - status: 'Not found Exception', - message: 'User not found', - }); + if (!user) throw new CustomHttpException(SYS_MSG.USER_NOT_FOUND, HttpStatus.NOT_FOUND); const newJob = this.jobRepository.create(Object.assign(new Job(), { ...createJobDto, user })); - // Save the new Job entity to the database await this.jobRepository.save(newJob); - - // Return a success response return { status: 'success', status_code: 201, - message: 'Job listing created successfully', + message: SYS_MSG.JOB_CREATION_SUCCESSFUL, data: pick( newJob, Object.keys(newJob).filter(x => !['user', 'created_at', 'updated_at', 'is_deleted'].includes(x)) @@ -47,22 +82,22 @@ export class JobsService { } async getJobs() { - const jobs = await this.jobRepository.find(); + const jobs = await this.jobRepository.find({ where: { is_deleted: false } }); + + jobs.map(x => delete x.is_deleted); return { - message: 'Jobs listing fetched successfully', + message: SYS_MSG.JOB_LISTING_RETRIEVAL_SUCCESSFUL, status_code: 200, data: jobs, }; } async getJob(id: string) { - const job = await this.jobRepository.findOne({ where: { id } }); - if (!job) - throw new NotFoundException({ - status_code: 404, - status: 'Not found Exception', - message: 'Job not found', - }); + const job = await this.jobRepository.findOne({ where: { id, is_deleted: false } }); + + if (!job) throw new CustomHttpException(SYS_MSG.JOB_NOT_FOUND, HttpStatus.NOT_FOUND); + + delete job.is_deleted; return { message: 'Job fetched successfully', status_code: 200, @@ -70,7 +105,6 @@ export class JobsService { }; } async delete(jobId: string) { - // Check if listing exists const job = await this.jobRepository.findOne({ where: { id: jobId }, }); @@ -78,14 +112,43 @@ export class JobsService { job.is_deleted = true; const deleteJobEntityInstance = this.jobRepository.create(job); - // Save the new Job entity to the database await this.jobRepository.save(deleteJobEntityInstance); - // Return a success response return { status: 'success', - message: 'Job details deleted successfully', + message: SYS_MSG.JOB_DELETION_SUCCESSFUL, status_code: 200, }; } + + async searchJobs(searchDto: JobSearchDto, page: number, limit: number) { + const query = this.jobRepository.createQueryBuilder('job'); + query.where('job.is_deleted = :isDeleted', { isDeleted: false }); + + if (searchDto.location) { + query.andWhere('job.location ILIKE :location', { location: `%${searchDto.location}%` }); + } + + if (searchDto.salary_range) { + query.andWhere('job.salary_range = :salaryRange', { salaryRange: searchDto.salary_range }); + } + + if (searchDto.job_type) { + query.andWhere('job.job_type = :jobType', { jobType: searchDto.job_type }); + } + + if (searchDto.job_mode) { + query.andWhere('job.job_mode = :jobMode', { jobMode: searchDto.job_mode }); + } + page = Math.max(1, Math.floor(Number(page))); + limit = Math.max(1, Math.floor(Number(limit))); + + query.skip((page - 1) * limit).take(limit); + const jobs = await query.getMany(); + + return { + status_code: HttpStatus.OK, + data: jobs, + }; + } } diff --git a/src/modules/jobs/tests/job.guard.spec.ts b/src/modules/jobs/tests/job.guard.spec.ts deleted file mode 100644 index adf2b8aa9..000000000 --- a/src/modules/jobs/tests/job.guard.spec.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { ExecutionContext, ForbiddenException, NotFoundException } from '@nestjs/common'; -import { JobGuard } from '../guards/job.guard'; -import { Repository } from 'typeorm'; -import { getRepositoryToken } from '@nestjs/typeorm'; -import { Job } from '../entities/job.entity'; - -describe('JobGuard', () => { - let guard: JobGuard; - let jobRepository: Repository; - - const mockJobRepository = { - findOne: jest.fn(), - }; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - { - provide: getRepositoryToken(Job), - useValue: mockJobRepository, - }, - ], - }).compile(); - - jobRepository = module.get>(getRepositoryToken(Job)); - guard = new JobGuard(jobRepository); - }); - - it('should allow access if the job belongs to the user', async () => { - const mockJob = { id: '1', user: { id: 'user123' }, is_deleted: false } as Job; - mockJobRepository.findOne.mockResolvedValue(mockJob); - - const mockContext = { - switchToHttp: jest.fn().mockReturnValue({ - getRequest: jest.fn().mockReturnValue({ - user: { id: 'user123' }, - params: { id: 1 }, - }), - }), - } as unknown as ExecutionContext; - - const result = await guard.canActivate(mockContext); - - expect(result).toBe(true); - }); - - it('should throw NotFoundException if the job is deleted', async () => { - const mockJob = { id: '1', user: { id: 'user123' }, is_deleted: true } as Job; - mockJobRepository.findOne.mockResolvedValue(mockJob); - - const mockContext = { - switchToHttp: jest.fn().mockReturnValue({ - getRequest: jest.fn().mockReturnValue({ - user: { id: 'user123' }, - params: { id: 1 }, - }), - }), - } as unknown as ExecutionContext; - - await expect(guard.canActivate(mockContext)).rejects.toThrow(NotFoundException); - }); - - it('should throw ForbiddenException if the job does not belong to the user', async () => { - const mockJob = { id: '1', user: { id: 'user456' }, is_deleted: false } as Job; - mockJobRepository.findOne.mockResolvedValue(mockJob); - - const mockContext = { - switchToHttp: jest.fn().mockReturnValue({ - getRequest: jest.fn().mockReturnValue({ - user: { id: 'user123' }, - params: { id: 1 }, - }), - }), - } as unknown as ExecutionContext; - - await expect(guard.canActivate(mockContext)).rejects.toThrow(ForbiddenException); - }); - - it('should throw NotFoundException if the job is not found', async () => { - const mockJob = { id: '1', user: { id: 'user456' }, is_deleted: true } as Job; - mockJobRepository.findOne.mockResolvedValue(mockJob); - - const mockContext = { - switchToHttp: jest.fn().mockReturnValue({ - getRequest: jest.fn().mockReturnValue({ - user: { id: 'user456' }, - params: { id: 1 }, - }), - }), - } as unknown as ExecutionContext; - - await expect(guard.canActivate(mockContext)).rejects.toThrow(NotFoundException); - }); -}); diff --git a/src/modules/jobs/tests/job.service.spec.ts b/src/modules/jobs/tests/job.service.spec.ts deleted file mode 100644 index dbf8a4781..000000000 --- a/src/modules/jobs/tests/job.service.spec.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { JobsService } from '../jobs.service'; -import { getRepositoryToken } from '@nestjs/typeorm'; -import { User, UserType } from '../../user/entities/user.entity'; -import { Job } from '../entities/job.entity'; -import { Repository } from 'typeorm'; -import { NotFoundException, UnauthorizedException } from '@nestjs/common'; -import { JobDto } from '../dto/job.dto'; -import { PaginationDto } from '../dto/pagination.dto'; - -describe('JobsService', () => { - let service: JobsService; - let userRepository: Repository; - let jobRepository: Repository; - - const mockUser: User = { - id: 'user1', - first_name: 'John', - last_name: 'Doe', - email: 'john@example.com', - password: 'hashedpassword', - phone: '1234567890', - is_active: true, - attempts_left: 3, - time_left: null, - secret: 'secret', - is_2fa_enabled: false, - user_type: UserType.USER, - owned_organisations: [], - created_organisations: [], - profile: null, - testimonials: [], - backup_codes: [], - organisationMembers: [], - jobs: [], - created_at: new Date(), - updated_at: new Date(), - notification_settings: [], - notifications: [], - hashPassword: () => null, - }; - - const mockJob: Job = { - id: 'job1', - title: 'Software Engineer', - description: 'We are looking for a skilled software engineer', - location: 'Remote', - deadline: '2023-12-31', - salary_range: '70k_to_100k', - job_type: 'full-time', - job_mode: 'remote', - company_name: 'Tech Co', - is_deleted: false, - user: mockUser, - created_at: new Date(), - updated_at: new Date(), - }; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - JobsService, - { - provide: getRepositoryToken(User), - useClass: Repository, - }, - { - provide: getRepositoryToken(Job), - useClass: Repository, - }, - ], - }).compile(); - - service = module.get(JobsService); - userRepository = module.get>(getRepositoryToken(User)); - jobRepository = module.get>(getRepositoryToken(Job)); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('create', () => { - it('should create a new job', async () => { - const createJobDto: JobDto = { - title: 'Software Engineer', - description: 'We are looking for a skilled software engineer', - location: 'Remote', - deadline: '2023-12-31', - salary_range: '70k_to_100k', - job_type: 'full-time', - job_mode: 'remote', - company_name: 'Tech Co', - }; - - jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser); - jest.spyOn(jobRepository, 'create').mockReturnValue(mockJob); - jest.spyOn(jobRepository, 'save').mockResolvedValue(mockJob); - - const result = await service.create(createJobDto, 'user1'); - - expect(result.status).toBe('success'); - expect(result.status_code).toBe(201); - expect(result.message).toBe('Job listing created successfully'); - expect(result.data).toEqual(expect.objectContaining(createJobDto)); - }); - - it('should throw NotFoundException if user is not found', async () => { - jest.spyOn(userRepository, 'findOne').mockResolvedValue(null); - - await expect(service.create({} as JobDto, 'nonexistent')).rejects.toThrow(NotFoundException); - }); - }); -}); diff --git a/src/modules/jobs/tests/jobs.service.spec.ts b/src/modules/jobs/tests/jobs.service.spec.ts index d6434d480..2d5b8df2c 100644 --- a/src/modules/jobs/tests/jobs.service.spec.ts +++ b/src/modules/jobs/tests/jobs.service.spec.ts @@ -1,12 +1,17 @@ +import { HttpStatus, NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { JobsService } from '../jobs.service'; -import { JobDto } from '../dto/job.dto'; -import { DeleteResult, Repository } from 'typeorm'; -import { Job } from '../entities/job.entity'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { CustomHttpException } from '../../../helpers/custom-http-filter'; import UserResponseDTO from '../../user/dto/user-response.dto'; import { User } from '../../user/entities/user.entity'; -import { getRepositoryToken } from '@nestjs/typeorm'; +import { JobApplicationDto } from '../dto/job-application.dto'; +import { JobDto, SalaryRange, JobType, JobMode } from '../dto/job.dto'; +import { JobApplication } from '../entities/job-application.entity'; +import { Job } from '../entities/job.entity'; +import { JobsService } from '../jobs.service'; import { jobsMock } from './mocks/jobs.mock'; +import { JobSearchDto } from '../dto/jobSearch.dto'; describe('JobsService', () => { let service: JobsService; @@ -15,6 +20,26 @@ describe('JobsService', () => { let userDto: UserResponseDTO; let createJobDto: JobDto; + const mockJob = { + data: { + is_deleted: false, + deadline: new Date(new Date().getTime() + 1000 * 60 * 60 * 24).toISOString(), + }, + }; + + const mockJobApplicationDto: JobApplicationDto = { + applicant_name: 'John Doe', + email: 'johndoe@example.com', + resume: 'resume content', + cover_letter: 'Cover letter text', + }; + + const mockJobApplicationResponse = { + status: 'success', + message: 'Application submitted successfully', + status_code: HttpStatus.CREATED, + }; + beforeEach(async () => { userDto = { id: 'user_id', @@ -46,6 +71,25 @@ describe('JobsService', () => { save: jest.fn(), findOneBy: jest.fn(), update: jest.fn(), + createQueryBuilder: jest.fn().mockReturnValue({ + where: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + getCount: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + take: jest.fn().mockReturnThis(), + getMany: jest.fn().mockReturnThis(), + }), + }, + }, + { + provide: getRepositoryToken(JobApplication), + useValue: { + findBy: jest.fn(), + findOne: jest.fn(), + create: jest.fn(), + save: jest.fn(), + findOneBy: jest.fn(), + update: jest.fn(), }, }, { @@ -85,6 +129,12 @@ describe('JobsService', () => { expect(result.message).toEqual('Job listing created successfully'); expect(result.data).toEqual(createJobDto); }); + + it('should throw CustomHttpException if user is not found', async () => { + jest.spyOn(userRepository, 'findOne').mockResolvedValue(null); + + await expect(service.create({} as JobDto, 'nonexistent')).rejects.toThrow(CustomHttpException); + }); }); describe('lists all jobs', () => { @@ -103,4 +153,122 @@ describe('JobsService', () => { expect(result.message).toEqual('Job details deleted successfully'); }); }); + + describe('applyForJob', () => { + it('should throw error if job is deleted', async () => { + jest.spyOn(service, 'getJob').mockResolvedValue({ + data: { is_deleted: true, deadline: new Date().toISOString() }, + } as any); + + await expect(service.applyForJob('jobId', mockJobApplicationDto)).rejects.toThrow( + new CustomHttpException('Job deleted', HttpStatus.NOT_FOUND) + ); + }); + + it('should throw error if application deadline has passed', async () => { + jest.spyOn(service, 'getJob').mockResolvedValue({ + data: { is_deleted: false, deadline: new Date(new Date().getTime() - 1000 * 60 * 60 * 24).toISOString() }, + } as any); + + await expect(service.applyForJob('jobId', mockJobApplicationDto)).rejects.toThrow( + new CustomHttpException('Job application deadline passed', HttpStatus.UNPROCESSABLE_ENTITY) + ); + }); + + it('should successfully create a job application', async () => { + jest.spyOn(service, 'getJob').mockResolvedValue(mockJob as any); + const createMock = jest.fn().mockReturnValue(mockJobApplicationDto); + const saveMock = jest.fn().mockResolvedValue(mockJobApplicationResponse); + + jest.spyOn(service['jobApplicationRepository'], 'create').mockImplementation(createMock); + jest.spyOn(service['jobApplicationRepository'], 'save').mockImplementation(saveMock); + + const result = await service.applyForJob('jobId', mockJobApplicationDto); + + expect(result).toEqual(mockJobApplicationResponse); + expect(createMock).toHaveBeenCalledWith({ + ...mockJobApplicationDto, + applicant_name: 'John Doe', + resume: `https://example.com/John_Doe.pdf`, + ...mockJob, + }); + expect(saveMock).toHaveBeenCalled(); + }); + }); + + describe('searchJobs', () => { + it('should return jobs based on search criteria', async () => { + const searchDto: JobSearchDto = { + location: 'Boston', + salary_range: '70k_to_100k' as SalaryRange, + job_type: 'full-time' as JobType, + job_mode: 'remote' as JobMode, + page: 1, + limit: 10, + }; + + jest.spyOn(jobRepository, 'createQueryBuilder').mockImplementation(() => { + return { + where: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + getCount: jest.fn().mockResolvedValue(1), + skip: jest.fn().mockReturnThis(), + take: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue([mockJob]), + } as any; + }); + + const result = await service.searchJobs(searchDto, searchDto.page, searchDto.limit); + expect(result.status_code).toBe(200); + expect(result.data).toHaveLength(1); + expect(result.data[0]).toEqual(mockJob); + }); + + it('should return empty array if no jobs match the criteria', async () => { + const searchDto: JobSearchDto = { + location: 'Nowhere', + salary_range: '100k_to_150k' as SalaryRange, + job_type: 'part-time' as JobType, + job_mode: 'onsite' as JobMode, + page: 1, + limit: 10, + }; + + jest.spyOn(jobRepository, 'createQueryBuilder').mockImplementation(() => { + return { + where: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + getCount: jest.fn().mockResolvedValue(0), + skip: jest.fn().mockReturnThis(), + take: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue([]), + } as any; + }); + + const result = await service.searchJobs(searchDto, searchDto.page, searchDto.limit); + expect(result.status_code).toBe(200); + expect(result.data).toHaveLength(0); + }); + + it('should handle pagination correctly', async () => { + const searchDto: JobSearchDto = {}; + const page = 2; + const limit = 5; + + jest.spyOn(jobRepository, 'createQueryBuilder').mockImplementation(() => { + return { + where: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + getCount: jest.fn().mockResolvedValue(12), + skip: jest.fn().mockReturnThis(), + take: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue([mockJob, mockJob]), + } as any; + }); + + const result = await service.searchJobs(searchDto, page, limit); + expect(result.status_code).toBe(200); + expect(result.data).toHaveLength(2); + }); + }); }); diff --git a/src/modules/jobs/tests/mocks/jobs.mock.ts b/src/modules/jobs/tests/mocks/jobs.mock.ts index 34dde71fa..f193a6647 100644 --- a/src/modules/jobs/tests/mocks/jobs.mock.ts +++ b/src/modules/jobs/tests/mocks/jobs.mock.ts @@ -1,4 +1,5 @@ import { orgMock } from '../../../../modules/organisations/tests/mocks/organisation.mock'; +import { JobApplication } from '../../entities/job-application.entity'; import { Job } from '../../entities/job.entity'; export const jobsMock: Job[] = [ @@ -6,15 +7,43 @@ export const jobsMock: Job[] = [ id: '6f33f664-cf5f-441e-a204-316682aef466', created_at: new Date(), updated_at: new Date(), - title: 'senior dev', - description: 'JavaScript senior dev', - location: 'london', + title: 'Senior Developer', + description: 'JavaScript senior developer role with extensive experience in building scalable applications.', + location: 'London', deadline: '2024-07-30T12:34:56.000Z', salary_range: '30k_to_50k', job_type: 'full-time', job_mode: 'remote', - company_name: 'googlge', + company_name: 'Google', is_deleted: false, user: orgMock.owner, + qualifications: [ + "Bachelor's Degree in Computer Science or related field", + '5+ years of experience in software development', + 'Strong proficiency in JavaScript and modern frameworks', + ], + key_responsibilities: [ + 'Develop and maintain web applications', + 'Collaborate with cross-functional teams', + 'Write clean, scalable code', + ], + benefits: ['Health insurance', '401(k) matching', 'Paid time off'], + experience_level: 'Senior', + job_application: [], // Placeholder, will be updated later }, ]; + +export const jobApplicationMock: JobApplication[] = [ + { + id: '6f33g674-cf5f-346e-a204-316682aef466', + created_at: new Date(), + updated_at: new Date(), + applicant_name: 'John Doe', + email: 'johndoe@example.com', + resume: 'https://example.com/resume.pdf', + cover_letter: 'Cover letter text here', + job: jobsMock[0], + }, +]; + +jobsMock[0].job_application = jobApplicationMock; diff --git a/src/modules/jobs/utils/helpers.ts b/src/modules/jobs/utils/helpers.ts new file mode 100644 index 000000000..6e98f903f --- /dev/null +++ b/src/modules/jobs/utils/helpers.ts @@ -0,0 +1,5 @@ +export const isPassed = (date: string): boolean => { + const strToDate: Date = new Date(date); + const today = Date.now(); + return today > strToDate.getTime(); +}; diff --git a/src/modules/newsletter-subscription/dto/newsletter-subscription.response.dto.ts b/src/modules/newsletter-subscription/dto/newsletter-subscription.response.dto.ts new file mode 100644 index 000000000..9fe2a45f4 --- /dev/null +++ b/src/modules/newsletter-subscription/dto/newsletter-subscription.response.dto.ts @@ -0,0 +1,4 @@ +export class NewsletterSubscriptionResponseDto { + id: string; + email: string; +} diff --git a/src/modules/newsletter-subscription/newsletter-subscripion.controller.ts b/src/modules/newsletter-subscription/newsletter-subscripion.controller.ts deleted file mode 100644 index 4cb4d5777..000000000 --- a/src/modules/newsletter-subscription/newsletter-subscripion.controller.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Controller, Get, Post, Body, Param, Delete, HttpCode, HttpStatus, UseGuards } from '@nestjs/common'; -import { NewsletterSubscriptionService } from './newsletter-subscription.service'; -import { CreateNewsletterSubscriptionDto } from './dto/create-newsletter-subscription.dto'; -import { skipAuth } from '../../helpers/skipAuth'; -import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { SuperAdminGuard } from '../../guards/super-admin.guard'; - -@ApiTags('Newsletter Subscription') -@Controller('newsletter-subscription') -export class NewsletterSubscriptionController { - constructor(private readonly newsletterSubscriptionService: NewsletterSubscriptionService) {} - - @skipAuth() - @Post() - @HttpCode(HttpStatus.CREATED) - @ApiOperation({ summary: 'Subscribe to newsletter' }) - @ApiResponse({ status: 201, description: 'Subscriber subscription successful.' }) - create(@Body() createNewsletterDto: CreateNewsletterSubscriptionDto) { - return this.newsletterSubscriptionService.newsletterSubcription(createNewsletterDto); - } - - @UseGuards(SuperAdminGuard) - @Get() - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: 'Fetch all subscribers to newsletter' }) - @ApiResponse({ status: 200, type: [Object] }) - findAll() { - return this.newsletterSubscriptionService.findAll(); - } - - @UseGuards(SuperAdminGuard) - @Delete(':id') - @HttpCode(HttpStatus.OK) - @ApiParam({ name: 'id', required: true, description: 'ID of the subscriber to be deleted' }) - @ApiOperation({ summary: 'Remove subscriber from newsletter' }) - @ApiResponse({ status: 200, description: 'Subscriber with ID {id} has been soft deleted' }) - @ApiResponse({ status: 404, description: 'Subscriber with ID ${id} not found' }) - remove(@Param('id') id: string) { - return this.newsletterSubscriptionService.remove(id); - } - - @UseGuards(SuperAdminGuard) - @Get('deleted') - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: 'Fetch all deleted subscribers' }) - @ApiResponse({ status: 200, type: [Object] }) - findSoftDeleted() { - return this.newsletterSubscriptionService.findSoftDeleted(); - } - - @UseGuards(SuperAdminGuard) - @Post('restore/:id') - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: 'Fetch all deleted subscribers' }) - @ApiResponse({ status: 200, description: 'Subscriber with ID {id} has been restored' }) - @ApiResponse({ status: 404, description: 'Subscriber with ID ${id} not found or already restored' }) - restore(@Param('id') id: string) { - return this.newsletterSubscriptionService.restore(id); - } -} diff --git a/src/modules/newsletter-subscription/newsletter-subscription.controller.ts b/src/modules/newsletter-subscription/newsletter-subscription.controller.ts new file mode 100644 index 000000000..8351e47dd --- /dev/null +++ b/src/modules/newsletter-subscription/newsletter-subscription.controller.ts @@ -0,0 +1,118 @@ +import { Controller, Get, Post, Body, Param, Delete, HttpCode, HttpStatus, UseGuards, Query } from '@nestjs/common'; +import { NewsletterSubscriptionService } from './newsletter-subscription.service'; +import { CreateNewsletterSubscriptionDto } from './dto/create-newsletter-subscription.dto'; +import { skipAuth } from '../../helpers/skipAuth'; +import { ApiBearerAuth, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { SuperAdminGuard } from '../../guards/super-admin.guard'; +import { NewsletterSubscriptionResponseDto } from './dto/newsletter-subscription.response.dto'; + +@ApiTags('Newsletter Subscription') +@Controller('newsletter-subscription') +export class NewsletterSubscriptionController { + constructor(private readonly newsletterSubscriptionService: NewsletterSubscriptionService) {} + + @skipAuth() + @Post() + @HttpCode(HttpStatus.CREATED) + @ApiOperation({ summary: 'Subscribe to newsletter' }) + @ApiResponse({ status: 201, description: 'Subscriber subscription successful.' }) + create(@Body() createNewsletterDto: CreateNewsletterSubscriptionDto) { + return this.newsletterSubscriptionService.newsletterSubscription(createNewsletterDto); + } + + @ApiBearerAuth() + @UseGuards(SuperAdminGuard) + @Get() + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Fetch all subscribers to newsletter' }) + @ApiResponse({ + status: 200, + description: 'Return all team members', + schema: { + properties: { + status: { type: 'string' }, + message: { type: 'string' }, + data: { + type: 'array', + items: { $ref: '#/components/schemas/NewsletterSubscriptionResponseDto' }, + }, + meta: { + type: 'object', + properties: { + total: { type: 'number' }, + page: { type: 'number' }, + limit: { type: 'number' }, + totalPages: { type: 'number' }, + }, + }, + }, + }, + }) + async getAllSubscribers( + @Query('page') page: number = 1, + @Query('limit') limit: number = 10 + ): Promise<{ message: string; data: NewsletterSubscriptionResponseDto[]; meta: any }> { + const { subscribers, total } = await this.newsletterSubscriptionService.findAllSubscribers(page, limit); + return { + message: 'Subscribers list fetched successfully', + data: subscribers, + meta: { + total, + page, + limit, + totalPages: Math.ceil(total / limit), + }, + }; + } + + @ApiBearerAuth() + @UseGuards(SuperAdminGuard) + @Delete(':id') + @HttpCode(HttpStatus.OK) + @ApiParam({ name: 'id', required: true, description: 'ID of the subscriber to be deleted' }) + @ApiOperation({ summary: 'Remove subscriber from newsletter' }) + @ApiResponse({ status: 200, description: 'Subscriber with ID {id} has been soft deleted' }) + @ApiResponse({ status: 404, description: 'Subscriber with ID ${id} not found' }) + removeSubscriber(@Param('id') id: string) { + return this.newsletterSubscriptionService.removeSubscriber(id); + } + + @ApiBearerAuth() + @UseGuards(SuperAdminGuard) + @Get('deleted') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Fetch all deleted subscribers' }) + @ApiResponse({ + status: 200, + description: 'Return all team members', + schema: { + properties: { + status: { type: 'string' }, + message: { type: 'string' }, + data: { + type: 'array', + items: { $ref: '#/components/schemas/NewsletterSubscriptionResponseDto' }, + }, + }, + }, + }) + async findSoftDeleted(): Promise<{ message: string; data: NewsletterSubscriptionResponseDto[] }> { + const deletedSubscribers = await this.newsletterSubscriptionService.findSoftDeleted(); + + return { + message: 'Deleted subscribers list fetched successfully', + data: deletedSubscribers, + }; + } + + @ApiBearerAuth() + @UseGuards(SuperAdminGuard) + @Post('restore/:id') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Fetch all deleted subscribers' }) + @ApiResponse({ status: 200, description: 'Subscriber with ID {id} has been restored' }) + @ApiResponse({ status: 404, description: 'Subscriber with ID ${id} not found or already restored' }) + restore(@Param('id') id: string) { + return this.newsletterSubscriptionService.restore(id); + } +} diff --git a/src/modules/newsletter-subscription/newsletter-subscription.module.ts b/src/modules/newsletter-subscription/newsletter-subscription.module.ts index 44c3e3bce..8af08ef31 100644 --- a/src/modules/newsletter-subscription/newsletter-subscription.module.ts +++ b/src/modules/newsletter-subscription/newsletter-subscription.module.ts @@ -1,12 +1,15 @@ import { Module } from '@nestjs/common'; import { NewsletterSubscriptionService } from './newsletter-subscription.service'; -import { NewsletterSubscriptionController } from './newsletter-subscripion.controller'; +import { NewsletterSubscriptionController } from './newsletter-subscription.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { NewsletterSubscription } from './entities/newsletter-subscription.entity'; import { User } from '../user/entities/user.entity'; +import { Organisation } from '../organisations/entities/organisations.entity'; +import { OrganisationUserRole } from '../role/entities/organisation-user-role.entity'; +import { Role } from '../role/entities/role.entity'; @Module({ - imports: [TypeOrmModule.forFeature([NewsletterSubscription, User])], + imports: [TypeOrmModule.forFeature([NewsletterSubscription, User, Organisation, OrganisationUserRole, Role])], controllers: [NewsletterSubscriptionController], providers: [NewsletterSubscriptionService], }) diff --git a/src/modules/newsletter-subscription/newsletter-subscription.service.ts b/src/modules/newsletter-subscription/newsletter-subscription.service.ts index e0f654138..9099a9002 100644 --- a/src/modules/newsletter-subscription/newsletter-subscription.service.ts +++ b/src/modules/newsletter-subscription/newsletter-subscription.service.ts @@ -1,7 +1,8 @@ import { Injectable, NotFoundException } from '@nestjs/common'; -import { CreateNewsletterSubscriptionDto } from './dto/create-newsletter-subscription.dto'; import { InjectRepository } from '@nestjs/typeorm'; import { IsNull, Not, Repository } from 'typeorm'; +import { CreateNewsletterSubscriptionDto } from './dto/create-newsletter-subscription.dto'; +import { NewsletterSubscriptionResponseDto } from './dto/newsletter-subscription.response.dto'; import { NewsletterSubscription } from './entities/newsletter-subscription.entity'; @Injectable() @@ -11,7 +12,7 @@ export class NewsletterSubscriptionService { private readonly newsletterSubscriptionRepository: Repository ) {} - async newsletterSubcription(createNewsletterSubscriptionDto: CreateNewsletterSubscriptionDto) { + async newsletterSubscription(createNewsletterSubscriptionDto: CreateNewsletterSubscriptionDto) { const { email } = createNewsletterSubscriptionDto; const existingSubscription = await this.newsletterSubscriptionRepository.findOne({ where: { email: email } }); @@ -24,16 +25,22 @@ export class NewsletterSubscriptionService { return response; } - async findAll() { - const subscribers = await this.newsletterSubscriptionRepository.find(); + async findAllSubscribers( + page: number = 1, + limit: number = 10 + ): Promise<{ subscribers: NewsletterSubscriptionResponseDto[]; total: number }> { + const [subscribers, total] = await this.newsletterSubscriptionRepository.findAndCount({ + skip: (page - 1) * limit, + take: limit, + }); - return subscribers.map(newsletter => ({ - id: newsletter.id, - email: newsletter.email, - })); + return { + subscribers: subscribers.map(this.mapSubscriberToResponseDto), + total, + }; } - async remove(id: string) { + async removeSubscriber(id: string) { const subscription = await this.newsletterSubscriptionRepository.findOne({ where: { id } }); if (!subscription) { throw new NotFoundException(`Subscriber with ID ${id} not found`); @@ -56,4 +63,13 @@ export class NewsletterSubscriptionService { } return { message: `Subscriber with ID ${id} has been restored` }; } + + private mapSubscriberToResponseDto( + newsletterSubscription: NewsletterSubscription + ): NewsletterSubscriptionResponseDto { + return { + id: newsletterSubscription.id, + email: newsletterSubscription.email, + }; + } } diff --git a/src/modules/newsletter-subscription/tests/newsletter-subscription.service.spec.ts b/src/modules/newsletter-subscription/tests/newsletter-subscription.service.spec.ts index 34a1e6fe6..6cef50dce 100644 --- a/src/modules/newsletter-subscription/tests/newsletter-subscription.service.spec.ts +++ b/src/modules/newsletter-subscription/tests/newsletter-subscription.service.spec.ts @@ -25,6 +25,7 @@ describe('NewsletterService', () => { create: jest.fn(), save: jest.fn(), find: jest.fn(), + findAndCount: jest.fn(), softDelete: jest.fn(), restore: jest.fn(), }, @@ -51,7 +52,7 @@ describe('NewsletterService', () => { .spyOn(repository, 'findOne') .mockResolvedValue({ id: '1', email: 'test@example.com' } as NewsletterSubscription); - const result = await service.newsletterSubcription(dto); + const result = await service.newsletterSubscription(dto); expect(result).toEqual({ message: 'Subscriber subscription successful' }); }); @@ -61,18 +62,43 @@ describe('NewsletterService', () => { jest.spyOn(repository, 'create').mockReturnValue(dto as NewsletterSubscription); jest.spyOn(repository, 'save').mockResolvedValue(dto as NewsletterSubscription); - const result = await service.newsletterSubcription(dto); + const result = await service.newsletterSubscription(dto); expect(result).toEqual({ status: 'success', message: 'Subscriber subscription successful' }); }); }); - describe('findAll', () => { - it('should return an array of subscribers', async () => { + describe('findAllSubscribers', () => { + it('should return paginated subscribers and total count', async () => { + const subscribers = [ + { id: '1', email: 'test1@example.com' }, + { id: '2', email: 'test2@example.com' }, + ]; + const totalCount = 10; + + jest.spyOn(repository, 'findAndCount').mockResolvedValue([subscribers as NewsletterSubscription[], totalCount]); + + const result = await service.findAllSubscribers(1, 2); + expect(result).toEqual({ + subscribers: subscribers.map(service['mapSubscriberToResponseDto']), + total: totalCount, + }); + expect(repository.findAndCount).toHaveBeenCalledWith({ + skip: 0, + take: 2, + }); + }); + + it('should use default pagination values if not provided', async () => { const subscribers = [{ id: '1', email: 'test@example.com' }]; - jest.spyOn(repository, 'find').mockResolvedValue(subscribers as NewsletterSubscription[]); + const totalCount = 1; + + jest.spyOn(repository, 'findAndCount').mockResolvedValue([subscribers as NewsletterSubscription[], totalCount]); - const result = await service.findAll(); - expect(result).toEqual(subscribers); + await service.findAllSubscribers(); + expect(repository.findAndCount).toHaveBeenCalledWith({ + skip: 0, + take: 10, + }); }); }); @@ -82,7 +108,7 @@ describe('NewsletterService', () => { jest.spyOn(repository, 'findOne').mockResolvedValue({ id, email: 'test@example.com' } as NewsletterSubscription); jest.spyOn(repository, 'softDelete').mockResolvedValue({ affected: 1, raw: {}, generatedMaps: [] }); - const result = await service.remove(id); + const result = await service.removeSubscriber(id); expect(result).toEqual({ message: `Subscriber with ID ${id} has been soft deleted` }); }); @@ -90,7 +116,7 @@ describe('NewsletterService', () => { const id = '1'; jest.spyOn(repository, 'findOne').mockResolvedValue(null); - await expect(service.remove(id)).rejects.toThrow(NotFoundException); + await expect(service.removeSubscriber(id)).rejects.toThrow(NotFoundException); }); }); diff --git a/src/modules/notifications/dtos/mark-all-notifications-as-read.dto.ts b/src/modules/notifications/dtos/mark-all-notifications-as-read.dto.ts index 286a716fa..72c56e920 100644 --- a/src/modules/notifications/dtos/mark-all-notifications-as-read.dto.ts +++ b/src/modules/notifications/dtos/mark-all-notifications-as-read.dto.ts @@ -8,5 +8,5 @@ export class MarkAllNotificationAsReadResponse { type: 'object', properties: { notifications: { type: 'array', items: { type: 'string' }, example: [] } }, }) - data: {}; + data: object; } diff --git a/src/modules/notifications/notifications.controller.ts b/src/modules/notifications/notifications.controller.ts index 53a0120d7..8ac136452 100644 --- a/src/modules/notifications/notifications.controller.ts +++ b/src/modules/notifications/notifications.controller.ts @@ -45,7 +45,7 @@ export class NotificationsController { return this.notificationsService.createGlobalNotifications(dto); } - @Get() + @Get('/all') @ApiResponse({ status: 200, description: 'Notifications retrieved successfully', @@ -107,7 +107,7 @@ export class NotificationsController { return this.notificationsService.markAllNotificationsAsReadForUser(userId); } - @Get() + @Get('/unread') @ApiResponse({ status: 200, description: 'Unread notifications retrieved successfully', diff --git a/src/modules/notifications/notifications.module.ts b/src/modules/notifications/notifications.module.ts index c27c6543d..70a5f64ab 100644 --- a/src/modules/notifications/notifications.module.ts +++ b/src/modules/notifications/notifications.module.ts @@ -10,9 +10,25 @@ import UserService from '../user/user.service'; import { Notification } from './entities/notifications.entity'; import { NotificationsService } from './notifications.service'; import { NotificationsController } from './notifications.controller'; +import QueueService from '../email/queue.service'; +import { EmailModule } from '../email/email.module'; +import { Organisation } from '../organisations/entities/organisations.entity'; +import { OrganisationUserRole } from '../role/entities/organisation-user-role.entity'; +import { Role } from '../role/entities/role.entity'; @Module({ - imports: [TypeOrmModule.forFeature([Notification, User, Profile, NotificationSettings])], + imports: [ + TypeOrmModule.forFeature([ + Notification, + User, + Profile, + NotificationSettings, + Organisation, + OrganisationUserRole, + Role, + ]), + EmailModule, + ], controllers: [NotificationsController], providers: [NotificationsService, Repository, UserService, NotificationSettingsService, EmailService], }) diff --git a/src/modules/notifications/notifications.service.ts b/src/modules/notifications/notifications.service.ts index fb46fa2e0..aa2ed5c1e 100644 --- a/src/modules/notifications/notifications.service.ts +++ b/src/modules/notifications/notifications.service.ts @@ -30,7 +30,7 @@ export class NotificationsService { @InjectRepository(Notification) private readonly notificationRepository: Repository, - private readonly emailService: EmailService, + private emailService: EmailService, private readonly userService: UserService, private readonly notificationSettingsService: NotificationSettingsService, @InjectRepository(User) diff --git a/src/modules/notifications/tests/mocks/notification-repo.mock.ts b/src/modules/notifications/tests/mocks/notification-repo.mock.ts index 141348ac0..bcb055764 100644 --- a/src/modules/notifications/tests/mocks/notification-repo.mock.ts +++ b/src/modules/notifications/tests/mocks/notification-repo.mock.ts @@ -39,21 +39,23 @@ export const mockUser: User = { last_name: 'Smith', email: 'john.smith@example.com', password: 'pass123', + status: 'Hello from the children of planet Earth', hashPassword: async () => {}, is_active: true, attempts_left: 3, time_left: 3600, owned_organisations: [], - created_organisations: [], testimonials: [], - user_type: UserType.ADMIN, secret: 'secret', is_2fa_enabled: false, backup_codes: [], notifications: [], notification_settings: [], - organisationMembers: [], profile: profileMock, phone: '1234-887-09', jobs: [], + blogs: [], + comments: [], + cart: [], + organisations: null, }; diff --git a/src/modules/organisation-permissions/dto/create-organisation-permission.dto.ts b/src/modules/organisation-permissions/dto/create-organisation-permission.dto.ts deleted file mode 100644 index cf56003fd..000000000 --- a/src/modules/organisation-permissions/dto/create-organisation-permission.dto.ts +++ /dev/null @@ -1 +0,0 @@ -export class CreateOrganisationPermissionDto {} diff --git a/src/modules/organisation-permissions/dto/update-organisation-permission.dto.ts b/src/modules/organisation-permissions/dto/update-organisation-permission.dto.ts deleted file mode 100644 index 2a96845f7..000000000 --- a/src/modules/organisation-permissions/dto/update-organisation-permission.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PartialType } from '@nestjs/swagger'; -import { CreateOrganisationPermissionDto } from './create-organisation-permission.dto'; - -export class UpdateOrganisationPermissionDto extends PartialType(CreateOrganisationPermissionDto) {} diff --git a/src/modules/organisation-permissions/dto/update-permission.dto.ts b/src/modules/organisation-permissions/dto/update-permission.dto.ts deleted file mode 100644 index 8fd1e334c..000000000 --- a/src/modules/organisation-permissions/dto/update-permission.dto.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { IsOptional, IsObject, ValidateNested } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; -import { IsPermissionListValid } from '../helpers/custom-validator'; -import { Type } from 'class-transformer'; -import { PermissionListDto } from './permission-list.dto'; - -export class UpdatePermissionDto { - @ApiProperty({ - description: 'Object containing permission categories and their boolean values', - type: PermissionListDto, - }) - @IsObject() - @IsOptional() - @ValidateNested() - @Type(() => PermissionListDto) - @IsPermissionListValid({ message: 'Invalid permission list structure' }) - permission_list: PermissionListDto; -} diff --git a/src/modules/organisation-permissions/entities/permissions.entity.ts b/src/modules/organisation-permissions/entities/permissions.entity.ts deleted file mode 100644 index 803eae0de..000000000 --- a/src/modules/organisation-permissions/entities/permissions.entity.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Entity, Column, ManyToOne } from 'typeorm'; -import { AbstractBaseEntity } from '../../../entities/base.entity'; -import { OrganisationRole } from '../../organisation-role/entities/organisation-role.entity'; - -@Entity() -export class Permissions extends AbstractBaseEntity { - @Column({ type: 'text' }) - category: string; - - @Column({ type: 'boolean', nullable: false }) - permission_list: boolean; - - @ManyToOne(() => OrganisationRole, role => role.permissions, { eager: false }) - role: OrganisationRole; -} diff --git a/src/modules/organisation-permissions/organisation-permissions.controller.ts b/src/modules/organisation-permissions/organisation-permissions.controller.ts deleted file mode 100644 index baedd0ef5..000000000 --- a/src/modules/organisation-permissions/organisation-permissions.controller.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { - Body, - Controller, - HttpException, - HttpStatus, - InternalServerErrorException, - NotFoundException, - Param, - Patch, - UseGuards, -} from '@nestjs/common'; -import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { OwnershipGuard } from '../../guards/authorization.guard'; -import { UpdatePermissionDto } from './dto/update-permission.dto'; -import { OrganisationPermissionsService } from './organisation-permissions.service'; - -@ApiBearerAuth() -@ApiTags('Organisation Permissions') -@Controller('organisations') -export class OrganisationPermissionsController { - constructor(private readonly permissionService: OrganisationPermissionsService) {} - - @ApiOperation({ summary: 'Update Permission' }) - @ApiResponse({ - status: 200, - description: 'The found record', - type: UpdatePermissionDto, - }) - @UseGuards(OwnershipGuard) - @Patch(':org_id/:role_id/permissions') - async updatePermission( - @Param('org_id') org_id: string, - @Param('role_id') role_id: string, - @Body() updatePermissionsDto: UpdatePermissionDto - ) { - try { - return await this.permissionService.handleUpdatePermission(org_id, role_id, updatePermissionsDto); - } catch (error) { - if (error instanceof NotFoundException) { - throw new HttpException(error.message, HttpStatus.NOT_FOUND); - } else if (error instanceof InternalServerErrorException) { - throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR); - } else { - throw new HttpException('An unexpected error occurred', HttpStatus.INTERNAL_SERVER_ERROR); - } - } - } -} diff --git a/src/modules/organisation-permissions/organisation-permissions.service.ts b/src/modules/organisation-permissions/organisation-permissions.service.ts deleted file mode 100644 index 54e3d8015..000000000 --- a/src/modules/organisation-permissions/organisation-permissions.service.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { HttpStatus, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { OrganisationRole } from '../organisation-role/entities/organisation-role.entity'; -import { Organisation } from '../organisations/entities/organisations.entity'; -import { UpdatePermissionDto } from './dto/update-permission.dto'; -import { Permissions } from './entities/permissions.entity'; - -@Injectable() -export class OrganisationPermissionsService { - constructor( - @InjectRepository(Organisation) - private readonly organisationRepository: Repository, - @InjectRepository(OrganisationRole) - private readonly roleRepository: Repository, - @InjectRepository(Permissions) - private readonly permissionRepository: Repository - ) {} - - async handleUpdatePermission(org_id: string, role_id: string, updatePermissionDto: UpdatePermissionDto) { - await this.validateorganisationAndRole(org_id, role_id); - await this.validateAndUpdatePermissions(role_id, updatePermissionDto); - return { message: 'Permissions successfully updated', status_code: HttpStatus.OK }; - } - - private async validateorganisationAndRole(orgId: string, roleId: string): Promise { - const organisation = await this.organisationRepository.findOne({ - where: { id: orgId }, - relations: ['role'], - }); - - if (!organisation) { - throw new NotFoundException(`organisation with ID ${orgId} not found`); - } - - if (!organisation.role.some(role => role.id === roleId)) { - throw new NotFoundException(`Role with ID ${roleId} not found in the specified organisation`); - } - } - - private async validateAndUpdatePermissions(roleId: string, updatePermissionDto: UpdatePermissionDto): Promise { - const role = await this.roleRepository.findOne({ - where: { id: roleId }, - relations: ['permissions'], - }); - - if (!role) { - throw new NotFoundException(`Role with ID ${roleId} not found`); - } - - const rolePermissionCategories = new Set(role.permissions.map(p => p.category.trim())); - const updatePromises = []; - - for (const [category, value] of Object.entries(updatePermissionDto.permission_list)) { - if (!rolePermissionCategories.has(category)) { - throw new NotFoundException(`Permission not found in the specified role`); - } - - updatePromises.push( - this.permissionRepository.update({ role: { id: roleId }, category }, { permission_list: value }) - ); - } - - try { - await Promise.all(updatePromises); - } catch (error) { - throw new InternalServerErrorException(`Failed to update permissions: ${error.message}`); - } - } -} diff --git a/src/modules/organisation-permissions/tests/organisation-permissions.service.spec.ts b/src/modules/organisation-permissions/tests/organisation-permissions.service.spec.ts deleted file mode 100644 index 375699397..000000000 --- a/src/modules/organisation-permissions/tests/organisation-permissions.service.spec.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { HttpStatus, InternalServerErrorException, NotFoundException } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { getRepositoryToken } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { OrganisationPermissionsService } from '../../organisation-permissions/organisation-permissions.service'; -import { OrganisationRole } from '../../organisation-role/entities/organisation-role.entity'; -import { Organisation } from '../../organisations/entities/organisations.entity'; -import { Permissions } from '../entities/permissions.entity'; -import { mockUpdatePermissionDto } from '../mocks/organisation-permissions.mock'; -import { mockOrganisation } from '../mocks/organisation.mock'; -import { mockRole } from '../mocks/role.mock'; -describe('OrganisationPermissionsService', () => { - let service: OrganisationPermissionsService; - let permissionRepository: Repository; - let roleRepository: Repository; - let organisationRepository: Repository; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - OrganisationPermissionsService, - { - provide: getRepositoryToken(Permissions), - useValue: { - update: jest.fn(), - }, - }, - { - provide: getRepositoryToken(OrganisationRole), - useValue: { - findOne: jest.fn(), - }, - }, - { - provide: getRepositoryToken(Organisation), - useValue: { - findOne: jest.fn(), - }, - }, - ], - }).compile(); - - service = module.get(OrganisationPermissionsService); - permissionRepository = module.get>(getRepositoryToken(Permissions)); - roleRepository = module.get>(getRepositoryToken(OrganisationRole)); - organisationRepository = module.get>(getRepositoryToken(Organisation)); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('handleUpdatePermission', () => { - it('should update permission successfully', async () => { - jest.spyOn(organisationRepository, 'findOne').mockResolvedValue(mockOrganisation); - jest.spyOn(roleRepository, 'findOne').mockResolvedValue(mockRole); - jest.spyOn(permissionRepository, 'update').mockResolvedValue({ affected: 1, raw: {}, generatedMaps: [] }); - - const result = await service.handleUpdatePermission('org_123', 'role_456', mockUpdatePermissionDto); - - expect(result).toEqual({ - message: 'Permissions successfully updated', - status_code: HttpStatus.OK, - }); - }); - - it('should throw NotFoundException if organisation is not found', async () => { - jest.spyOn(organisationRepository, 'findOne').mockResolvedValue(null); - - await expect(service.handleUpdatePermission('org_id', 'role_id', mockUpdatePermissionDto)).rejects.toThrow( - new NotFoundException(`organisation with ID org_id not found`) - ); - }); - - it('should throw NotFoundException if role is not found in the organisation', async () => { - jest.spyOn(organisationRepository, 'findOne').mockResolvedValue({ ...mockOrganisation, role: [] }); - jest.spyOn(roleRepository, 'findOne').mockResolvedValue(null); - - await expect(service.handleUpdatePermission('org_id', 'role_id', mockUpdatePermissionDto)).rejects.toThrow( - new NotFoundException(`Role with ID role_id not found in the specified organisation`) - ); - }); - - it('should throw NotFoundException if permission is not found in the role', async () => { - jest.spyOn(organisationRepository, 'findOne').mockResolvedValue(mockOrganisation); - jest.spyOn(roleRepository, 'findOne').mockResolvedValue({ ...mockRole, permissions: [] }); - - await expect(service.handleUpdatePermission('org_123', 'role_456', mockUpdatePermissionDto)).rejects.toThrow( - new NotFoundException(`Permission not found in the specified role`) - ); - }); - - it('should throw InternalServerErrorException if update fails', async () => { - jest.spyOn(organisationRepository, 'findOne').mockResolvedValue(mockOrganisation); - jest.spyOn(roleRepository, 'findOne').mockResolvedValue(mockRole); - jest.spyOn(permissionRepository, 'update').mockRejectedValue(new Error('Update failed')); - - await expect(service.handleUpdatePermission('org_123', 'role_456', mockUpdatePermissionDto)).rejects.toThrow( - new InternalServerErrorException(`Failed to update permissions: Update failed`) - ); - }); - }); -}); diff --git a/src/modules/organisation-role/dto/update-organisation-role.dto.ts b/src/modules/organisation-role/dto/update-organisation-role.dto.ts deleted file mode 100644 index 4a6b39c86..000000000 --- a/src/modules/organisation-role/dto/update-organisation-role.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PartialType } from '@nestjs/swagger'; -import { CreateOrganisationRoleDto } from './create-organisation-role.dto'; - -export class UpdateOrganisationRoleDto extends PartialType(CreateOrganisationRoleDto) {} diff --git a/src/modules/organisation-role/entities/organisation-role.entity.ts b/src/modules/organisation-role/entities/organisation-role.entity.ts deleted file mode 100644 index ab9ae5426..000000000 --- a/src/modules/organisation-role/entities/organisation-role.entity.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { AbstractBaseEntity } from '../../../entities/base.entity'; -import { Column, Entity, ManyToOne, OneToMany } from 'typeorm'; -import { Organisation } from '../../organisations/entities/organisations.entity'; -import { Permissions } from '../../organisation-permissions/entities/permissions.entity'; -import { OrganisationMember } from '../../organisations/entities/org-members.entity'; - -@Entity('roles') -export class OrganisationRole extends AbstractBaseEntity { - @Column({ nullable: false }) - id: string; - - @Column({ length: 50, unique: true, nullable: false }) - name: string; - - @Column({ length: 200, nullable: true }) - description: string; - - @OneToMany(() => Permissions, permission => permission.role, { eager: false }) - permissions: Permissions[]; - - @ManyToOne(() => Organisation, organisation => organisation.role, { eager: false }) - organisation: Organisation; - - @OneToMany(() => OrganisationMember, member => member.role) - organisationMembers: OrganisationMember[]; -} diff --git a/src/modules/organisation-role/entities/role.entity.ts b/src/modules/organisation-role/entities/role.entity.ts deleted file mode 100644 index cf91315a6..000000000 --- a/src/modules/organisation-role/entities/role.entity.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Entity, Column } from 'typeorm'; -import { AbstractBaseEntity } from '../../../entities/base.entity'; - -@Entity() -export class Role extends AbstractBaseEntity { - @Column({ type: 'text' }) - name: string; - - @Column({ type: 'text', nullable: true }) - description: string; -} diff --git a/src/modules/organisation-role/organisation-role.controller.ts b/src/modules/organisation-role/organisation-role.controller.ts deleted file mode 100644 index 5d5875e1d..000000000 --- a/src/modules/organisation-role/organisation-role.controller.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { - BadRequestException, - Body, - Controller, - Get, - HttpCode, - HttpStatus, - NotFoundException, - Param, - Patch, - Post, - UseGuards, -} from '@nestjs/common'; -import { ApiBearerAuth, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { OwnershipGuard } from '../../guards/authorization.guard'; -import { CreateOrganisationRoleDto } from './dto/create-organisation-role.dto'; -import { OrganisationRoleService } from './organisation-role.service'; -import { UpdateOrganisationRoleDto } from './dto/update-organisation-role.dto'; - -@ApiTags('organisation Settings') -@UseGuards(OwnershipGuard) -@ApiBearerAuth() -@Controller('organisations') -export class OrganisationRoleController { - constructor(private readonly organisationRoleService: OrganisationRoleService) {} - - @Post(':id/roles') - @HttpCode(HttpStatus.CREATED) - @ApiOperation({ summary: 'Create a new role in an organisation' }) - @ApiParam({ name: 'organisationId', required: true, description: 'ID of the organisation' }) - @ApiResponse({ status: 201, description: 'The role has been successfully created.' }) - @ApiResponse({ status: 400, description: 'Bad Request.' }) - @ApiResponse({ status: 401, description: 'Unauthorized.' }) - @ApiResponse({ status: 403, description: 'Forbidden.' }) - @ApiResponse({ status: 409, description: 'Conflict - Role with this name already exists.' }) - async create(@Body() createRoleDto: CreateOrganisationRoleDto, @Param('id') organisationId: string) { - const savedRole = await this.organisationRoleService.createOrgRoles(createRoleDto, organisationId); - - return { - id: savedRole.id, - status_code: HttpStatus.CREATED, - name: savedRole.name, - description: savedRole.description, - message: 'Role created successfully', - }; - } - - @Get(':id/roles') - @ApiOperation({ summary: 'Get all organisation roles' }) - @ApiResponse({ status: 200, description: 'Success', type: [Object] }) - @ApiResponse({ status: 404, description: 'Organisation not found' }) - async getRoles(@Param('id') organisationID: string) { - const roles = await this.organisationRoleService.getAllRolesInOrg(organisationID); - return { - status_code: 200, - data: roles, - }; - } - - @Get(':id/roles/:roleId') - @ApiOperation({ summary: 'Fetch a single role within an organization' }) - @ApiParam({ name: 'id', required: true, description: 'ID of the role' }) - @ApiResponse({ status: 200, description: 'The role has been successfully fetched.' }) - @ApiResponse({ status: 400, description: 'Bad Request - Invalid role ID format.' }) - @ApiResponse({ status: 401, description: 'Unauthorized.' }) - @ApiResponse({ status: 403, description: 'Forbidden.' }) - @ApiResponse({ status: 404, description: 'Not Found - Role does not exist.' }) - async findOne(@Param('roleId') roleId: string, @Param('id') organisationId: string) { - try { - const role = await this.organisationRoleService.findSingleRole(roleId, organisationId); - return { - status_code: 200, - data: { - id: role.id, - name: role.name, - description: role.description, - permissions: role.permissions.map(permission => ({ - id: permission.id, - category: permission.category, - permission_list: permission.permission_list, - })), - }, - }; - } catch (error) { - if (error instanceof NotFoundException) { - throw error; - } - throw new BadRequestException('Failed to fetch role'); - } - } - - @Patch(':orgId/roles/:roleId') - @ApiOperation({ summary: 'update a role within an organization' }) - @ApiResponse({ - status: 200, - description: 'The role has been successfully updated', - }) - @ApiResponse({ status: 400, description: 'Invalid role ID format or input data' }) - @ApiResponse({ status: 401, description: 'Unauthorized.' }) - @ApiResponse({ status: 403, description: 'Forbidden.' }) - @ApiResponse({ status: 404, description: 'Role not found' }) - @ApiResponse({ status: 404, description: 'Organisation not found' }) - async updateRole( - updateRoleDto: UpdateOrganisationRoleDto, - @Param('orgId') orgId: string, - @Param('roleId') roleId: string - ) { - const data = await this.organisationRoleService.updateRole(updateRoleDto, orgId, roleId); - - return { - status_code: 200, - data, - }; - } -} diff --git a/src/modules/organisation-role/organisation-role.module.ts b/src/modules/organisation-role/organisation-role.module.ts deleted file mode 100644 index bed784f06..000000000 --- a/src/modules/organisation-role/organisation-role.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Module } from '@nestjs/common'; -import { OrganisationRoleService } from './organisation-role.service'; -import { OrganisationRoleController } from './organisation-role.controller'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { OrganisationRole } from './entities/organisation-role.entity'; -import { Organisation } from '../organisations/entities/organisations.entity'; -import { Permissions } from '../organisation-permissions/entities/permissions.entity'; -import { DefaultPermissions } from '../organisation-permissions/entities/default-permissions.entity'; - -@Module({ - imports: [TypeOrmModule.forFeature([OrganisationRole, Permissions, Organisation, DefaultPermissions])], - controllers: [OrganisationRoleController], - providers: [OrganisationRoleService], -}) -export class OrganisationRoleModule {} diff --git a/src/modules/organisation-role/organisation-role.service.ts b/src/modules/organisation-role/organisation-role.service.ts deleted file mode 100644 index 75c973e64..000000000 --- a/src/modules/organisation-role/organisation-role.service.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { - ConflictException, - HttpStatus, - Injectable, - InternalServerErrorException, - NotFoundException, -} from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { DefaultPermissions } from '../organisation-permissions/entities/default-permissions.entity'; -import { Permissions } from '../organisation-permissions/entities/permissions.entity'; -import { Organisation } from '../organisations/entities/organisations.entity'; -import { CreateOrganisationRoleDto } from './dto/create-organisation-role.dto'; -import { OrganisationRole } from './entities/organisation-role.entity'; -import { UpdateOrganisationRoleDto } from './dto/update-organisation-role.dto'; - -@Injectable() -export class OrganisationRoleService { - constructor( - @InjectRepository(OrganisationRole) - private rolesRepository: Repository, - @InjectRepository(Organisation) - private organisationRepository: Repository, - @InjectRepository(Permissions) - private permissionRepository: Repository, - @InjectRepository(DefaultPermissions) - private defaultPermissionsRepository: Repository - ) {} - - async createOrgRoles(createOrganisationRoleDto: CreateOrganisationRoleDto, organisationId: string) { - try { - const organisation = await this.organisationRepository.findOne({ where: { id: organisationId } }); - if (!organisation) { - throw new NotFoundException({ - status_code: HttpStatus.NOT_FOUND, - error: 'Not Found', - message: 'Organisation not found', - }); - } - - const existingRole = await this.rolesRepository.findOne({ - where: { name: createOrganisationRoleDto.name, organisation: { id: organisationId } }, - }); - if (existingRole) { - throw new ConflictException({ - status_code: HttpStatus.CONFLICT, - error: 'Conflict', - message: 'A role with this name already exists in the organisation', - }); - } - - const role = this.rolesRepository.create({ - ...createOrganisationRoleDto, - organisation, - }); - - const createdRole = await this.rolesRepository.save(role); - - const defaultPermissions = await this.defaultPermissionsRepository.find(); - - const rolePermissions = defaultPermissions.map(defaultPerm => { - const permission = new Permissions(); - permission.category = defaultPerm.category; - permission.permission_list = defaultPerm.permission_list; - permission.role = role; - return permission; - }); - - await this.permissionRepository.save(rolePermissions); - - return createdRole; - } catch (error) { - if (error instanceof NotFoundException || error instanceof ConflictException) { - throw error; - } - throw new InternalServerErrorException({ - status_code: HttpStatus.INTERNAL_SERVER_ERROR, - error: 'Internal Server Error', - message: 'Failed to create organisation role', - }); - } - } - - async getAllRolesInOrg(organisationID: string) { - const organisation = await this.organisationRepository.findOne({ - where: { id: organisationID }, - }); - if (!organisation) { - throw new NotFoundException('Organisation not found'); - } - - return this.rolesRepository - .find({ where: { organisation: { id: organisationID } }, select: ['id', 'name', 'description'] }) - .then(roles => - roles.map(role => ({ - id: role.id, - name: role.name, - description: role.description, - })) - ); - } - - async findSingleRole(id: string, organisationId: string): Promise { - try { - const organisation = await this.organisationRepository.findOne({ where: { id: organisationId } }); - - if (!organisation) { - throw new NotFoundException(`Organisation with ID ${organisationId} not found`); - } - - const role = await this.rolesRepository.findOne({ - where: { id, organisation: { id: organisationId } }, - relations: ['permissions'], - }); - - if (!role) { - throw new NotFoundException(`The role with ID ${id} does not exist in the organisation`); - } - - return role; - } catch (error) { - if (error instanceof NotFoundException) { - throw error; - } - throw new Error(`Failed to fetch role: ${error.message}`); - } - } - - async updateRole(updateRoleDto: UpdateOrganisationRoleDto, orgId: string, roleId: string) { - const organisation = await this.organisationRepository.findOne({ - where: { - id: orgId, - }, - }); - - if (!organisation) { - throw new NotFoundException({ - status_code: HttpStatus.NOT_FOUND, - error: 'Organization not found', - message: `The organization with ID ${orgId} does not exist`, - }); - } - - const role = await this.rolesRepository.findOne({ - where: { - id: roleId, - organisation: organisation, - }, - }); - - if (!role) { - throw new NotFoundException({ - status_code: HttpStatus.NOT_FOUND, - error: 'Role not found', - message: `The role with ID ${roleId} does not exist`, - }); - } - - Object.assign(role, updateRoleDto); - - await this.rolesRepository.save(role); - return role; - } -} diff --git a/src/modules/organisation-role/tests/organisation-role.service.spec.ts b/src/modules/organisation-role/tests/organisation-role.service.spec.ts deleted file mode 100644 index 08eedc559..000000000 --- a/src/modules/organisation-role/tests/organisation-role.service.spec.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { ConflictException, NotFoundException } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { getRepositoryToken } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { DefaultPermissions } from '../../organisation-permissions/entities/default-permissions.entity'; -import { Permissions } from '../../organisation-permissions/entities/permissions.entity'; -import { Organisation } from '../../organisations/entities/organisations.entity'; -import { OrganisationRole } from '../entities/organisation-role.entity'; -import { OrganisationRoleService } from '../organisation-role.service'; -import { UpdateOrganisationRoleDto } from '../dto/update-organisation-role.dto'; - -describe('OrganisationRoleService', () => { - let service: OrganisationRoleService; - let rolesRepository: Repository; - let organisationRepository: Repository; - let permissionRepository: Repository; - let defaultPermissionRepository: Repository; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - OrganisationRoleService, - { - provide: getRepositoryToken(OrganisationRole), - useClass: Repository, - }, - { - provide: getRepositoryToken(Organisation), - useClass: Repository, - }, - { - provide: getRepositoryToken(Permissions), - useClass: Repository, - }, - { - provide: getRepositoryToken(DefaultPermissions), - useClass: Repository, - }, - ], - }).compile(); - - service = module.get(OrganisationRoleService); - rolesRepository = module.get>(getRepositoryToken(OrganisationRole)); - organisationRepository = module.get>(getRepositoryToken(Organisation)); - permissionRepository = module.get>(getRepositoryToken(Permissions)); - }); - - describe('createOrgRoles', () => { - it('should create a role successfully', async () => { - const createRoleDto = { name: 'TestRole', description: 'Test Description', organisation: { id: 'org123' } }; - const organisationId = 'org123'; - const mockOrganisation = { id: organisationId }; - const mockDefaultPermissions = [{ id: 'perm1', category: 'category1', permission_list: true }]; - const mockPermissions = { id: 'perm1', category: 'category1', permission_list: true }; - const mockSavedRole = { id: 'role123', ...createRoleDto, permissions: [] }; - - jest.spyOn(organisationRepository, 'findOne').mockResolvedValue(mockOrganisation as Organisation); - jest.spyOn(rolesRepository, 'findOne').mockResolvedValue(null); - jest - .spyOn(service['defaultPermissionsRepository'], 'find') - .mockResolvedValue(mockDefaultPermissions as DefaultPermissions[]); - jest.spyOn(rolesRepository, 'create').mockReturnValue({ ...createRoleDto } as OrganisationRole); - jest.spyOn(rolesRepository, 'save').mockResolvedValue(mockSavedRole as OrganisationRole); - jest.spyOn(permissionRepository, 'save').mockResolvedValue(mockPermissions as Permissions); - - const result = await service.createOrgRoles(createRoleDto, organisationId); - - expect(result).toEqual(mockSavedRole); - expect(rolesRepository.save).toHaveBeenCalledWith( - expect.objectContaining({ - ...createRoleDto, - organisation: mockOrganisation, - }) - ); - expect(permissionRepository.save).toHaveBeenCalled(); - }); - - it('should throw NotFoundException when organisation is not found', async () => { - jest.spyOn(organisationRepository, 'findOne').mockResolvedValue(null); - - await expect(service.createOrgRoles({ name: 'TestRole' }, 'nonexistent')).rejects.toThrow(NotFoundException); - }); - - it('should throw ConflictException when role already exists', async () => { - jest.spyOn(organisationRepository, 'findOne').mockResolvedValue({ id: 'org123' } as Organisation); - jest.spyOn(rolesRepository, 'findOne').mockResolvedValue({ id: 'existing' } as OrganisationRole); - - await expect(service.createOrgRoles({ name: 'ExistingRole' }, 'org123')).rejects.toThrow(ConflictException); - }); - }); - - describe('getAllRolesInOrganisation', () => { - it('should return an array of roles for an existing organisation', async () => { - const organisationId = '1'; - const mockRoles = [ - { id: '1', name: 'Admin', description: 'Administrator role' }, - { id: '2', name: 'User', description: 'Regular user role' }, - ]; - - jest.spyOn(organisationRepository, 'findOne').mockResolvedValue({ id: organisationId } as Organisation); - jest.spyOn(rolesRepository, 'find').mockResolvedValue(mockRoles as OrganisationRole[]); - - const roles = await service.getAllRolesInOrg(organisationId); - expect(roles).toEqual(mockRoles); - }); - - it('should throw NotFoundException if the organisation is not found', async () => { - const organisationId = '1'; - jest.spyOn(organisationRepository, 'findOne').mockResolvedValue(null); - - await expect(service.getAllRolesInOrg(organisationId)).rejects.toThrow(NotFoundException); - }); - it('should handle cases where roles are not available', async () => { - const organisationId = '1'; - - jest.spyOn(organisationRepository, 'findOne').mockResolvedValue({ id: organisationId } as Organisation); - jest.spyOn(rolesRepository, 'find').mockResolvedValue([]); - - const roles = await service.getAllRolesInOrg(organisationId); - expect(roles).toEqual([]); - }); - - it('should handle database errors gracefully', async () => { - const organisationId = '1'; - - jest.spyOn(organisationRepository, 'findOne').mockRejectedValue(new Error('Database error')); - - await expect(service.getAllRolesInOrg(organisationId)).rejects.toThrowError('Database error'); - }); - }); - - describe('findSingleRole', () => { - it('should find a role successfully', async () => { - const roleId = 'role123'; - const organisationId = 'org123'; - const mockRole = { id: roleId, name: 'TestRole', permissions: [] }; - - jest.spyOn(organisationRepository, 'findOne').mockResolvedValue({ id: organisationId } as Organisation); - jest.spyOn(rolesRepository, 'findOne').mockResolvedValue(mockRole as OrganisationRole); - - const result = await service.findSingleRole(roleId, organisationId); - - expect(result).toEqual(mockRole); - }); - - it('should throw NotFoundException when organisation is not found', async () => { - jest.spyOn(organisationRepository, 'findOne').mockResolvedValue(null); - - await expect(service.findSingleRole('role123', 'nonexistent')).rejects.toThrow(NotFoundException); - }); - - it('should throw NotFoundException when role is not found', async () => { - jest.spyOn(organisationRepository, 'findOne').mockResolvedValue({ id: 'org123' } as Organisation); - jest.spyOn(rolesRepository, 'findOne').mockResolvedValue(null); - - await expect(service.findSingleRole('nonexistent', 'org123')).rejects.toThrow(NotFoundException); - }); - }); - - describe('updateRole', () => { - it('should update the role successfully', async () => { - const updateRoleDto: UpdateOrganisationRoleDto = { name: 'Updated Role', description: 'Updated Description' }; - const orgId = 'org-id'; - const roleId = 'role-id'; - - const organisation = new Organisation(); - organisation.id = orgId; - - const role = new OrganisationRole(); - role.id = roleId; - role.name = 'Original Role'; - role.description = 'Original Description'; - role.organisation = organisation; - - jest.spyOn(organisationRepository, 'findOne').mockResolvedValue(organisation); - jest.spyOn(rolesRepository, 'findOne').mockResolvedValue(role); - jest.spyOn(rolesRepository, 'save').mockResolvedValue(role); - - const result = await service.updateRole(updateRoleDto, orgId, roleId); - - expect(result.name).toBe('Updated Role'); - expect(result.description).toBe('Updated Description'); - expect(rolesRepository.save).toHaveBeenCalledWith(expect.objectContaining(updateRoleDto)); - }); - - it('should throw NotFoundException if the organisation does not exist', async () => { - const updateRoleDto: UpdateOrganisationRoleDto = { name: 'Starlight Role', description: 'Updated Description' }; - const orgId = 'non-existent-org-id'; - const roleId = 'role-id'; - - jest.spyOn(organisationRepository, 'findOne').mockResolvedValue(null); - - await expect(service.updateRole(updateRoleDto, orgId, roleId)).rejects.toThrow(NotFoundException); - }); - - it('should throw NotFoundException if the role does not exist', async () => { - const updateRoleDto: UpdateOrganisationRoleDto = { name: 'Starlight Mentor', description: 'Updated Description' }; - const orgId = 'org-id'; - const roleId = 'non-existent-role-id'; - - const organisation = new Organisation(); - organisation.id = orgId; - - jest.spyOn(organisationRepository, 'findOne').mockResolvedValue(organisation); - jest.spyOn(rolesRepository, 'findOne').mockResolvedValue(null); - - await expect(service.updateRole(updateRoleDto, orgId, roleId)).rejects.toThrow(NotFoundException); - }); - }); -}); diff --git a/src/modules/organisations/dto/add-member.dto.ts b/src/modules/organisations/dto/add-member.dto.ts new file mode 100644 index 000000000..63940fd39 --- /dev/null +++ b/src/modules/organisations/dto/add-member.dto.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsUUID } from 'class-validator'; + +export class AddMemberDto { + @ApiProperty({ + type: String, + }) + @IsUUID() + readonly user_id: string; +} diff --git a/src/modules/organisations/dto/create-organisation-options.ts b/src/modules/organisations/dto/create-organisation-options.ts new file mode 100644 index 000000000..fa1609979 --- /dev/null +++ b/src/modules/organisations/dto/create-organisation-options.ts @@ -0,0 +1,5 @@ +import { OrganisationInterface } from '../interfaces/OrganisationInterface'; + +type CreateOrganisationType = Partial; + +export default CreateOrganisationType; diff --git a/src/modules/organisations/dto/org-member.dto.ts b/src/modules/organisations/dto/org-member.dto.ts new file mode 100644 index 000000000..fd422fb5c --- /dev/null +++ b/src/modules/organisations/dto/org-member.dto.ts @@ -0,0 +1,9 @@ +import { IsUUID, IsNotEmpty } from 'class-validator'; + +export class RemoveOrganisationMemberDto { + @IsUUID() + userId?: string; + + @IsUUID() + organisationId?: string; +} diff --git a/src/modules/organisations/dto/organisation.dto.ts b/src/modules/organisations/dto/organisation.dto.ts index b9090d97a..fd5042967 100644 --- a/src/modules/organisations/dto/organisation.dto.ts +++ b/src/modules/organisations/dto/organisation.dto.ts @@ -1,10 +1,11 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsEmail, IsString } from 'class-validator'; +import { IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator'; export class OrganisationRequestDto { @ApiProperty({ type: String, }) + @IsNotEmpty() @IsString() readonly name: string; @@ -12,6 +13,7 @@ export class OrganisationRequestDto { type: String, }) @IsString() + @IsOptional() readonly description: string; @ApiProperty({ @@ -19,35 +21,41 @@ export class OrganisationRequestDto { description: 'Organisation email must be unique', }) @IsEmail() + @IsOptional() readonly email: string; @ApiProperty({ type: String, }) @IsString() + @IsOptional() readonly industry: string; @ApiProperty({ type: String, }) @IsString() + @IsOptional() readonly type: string; @ApiProperty({ type: String, }) @IsString() + @IsOptional() readonly country: string; @ApiProperty({ type: String, }) @IsString() + @IsOptional() readonly address: string; @ApiProperty({ type: String, }) @IsString() + @IsOptional() readonly state: string; } diff --git a/src/modules/organisations/dto/update-organisation-role.dto.ts b/src/modules/organisations/dto/update-organisation-role.dto.ts new file mode 100644 index 000000000..c605529aa --- /dev/null +++ b/src/modules/organisations/dto/update-organisation-role.dto.ts @@ -0,0 +1,7 @@ +import { IsString, IsNotEmpty } from 'class-validator'; + +export class UpdateMemberRoleDto { + @IsString() + @IsNotEmpty() + role: string; +} diff --git a/src/modules/organisations/dto/update-organisation.dto.ts b/src/modules/organisations/dto/update-organisation.dto.ts index 2eeec72e0..8840e948b 100644 --- a/src/modules/organisations/dto/update-organisation.dto.ts +++ b/src/modules/organisations/dto/update-organisation.dto.ts @@ -1,45 +1,70 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsBoolean, IsEmail, IsOptional, IsString, MinLength } from 'class-validator'; -import { User } from '../../user/entities/user.entity'; +import { PartialType } from '@nestjs/mapped-types'; +import { OrganisationRequestDto } from './organisation.dto'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { User } from '../../../modules/user/entities/user.entity'; -export class UpdateOrganisationDto { - @ApiProperty({ - example: "CodeGhinux's Organisation", - description: 'Name of organisation', +export class UpdateOrganisationDto extends PartialType(OrganisationRequestDto) { + @ApiPropertyOptional({ + type: String, }) + @IsNotEmpty() @IsString() - @MinLength(2, { message: 'organisation name must be at least 2 characters long' }) @IsOptional() - name?: string; + readonly name?: string; - @ApiProperty({ - example: "CodeGhinux's Organisation Description", - description: 'description of organisation', + @ApiPropertyOptional({ + type: String, }) + @IsNotEmpty() @IsString() @IsOptional() - description?: string; + readonly description?: string; + @ApiPropertyOptional({ + type: String, + description: 'Organisation email must be unique', + }) @IsEmail() + @IsNotEmpty() @IsOptional() - email?: string; + readonly email?: string; + @ApiPropertyOptional({ + type: String, + }) @IsString() + @IsNotEmpty() @IsOptional() - industry?: string; + readonly industry?: string; + @ApiPropertyOptional({ + type: String, + }) @IsString() + @IsNotEmpty() @IsOptional() - type?: string; + readonly type?: string; + @ApiPropertyOptional({ + type: String, + }) @IsString() + @IsNotEmpty() @IsOptional() - country?: string; + readonly country?: string; + @ApiPropertyOptional({ + type: String, + }) @IsString() + @IsNotEmpty() @IsOptional() - address?: string; + readonly address?: string; + @ApiPropertyOptional({ + type: String, + }) @IsOptional() owner?: User; @@ -47,9 +72,6 @@ export class UpdateOrganisationDto { @IsOptional() state?: string; - @IsOptional() - creator?: User; - @IsBoolean() @IsOptional() isDeleted?: boolean; diff --git a/src/modules/organisations/dto/user-orgs-response.dto.ts b/src/modules/organisations/dto/user-orgs-response.dto.ts new file mode 100644 index 000000000..d90b29bd3 --- /dev/null +++ b/src/modules/organisations/dto/user-orgs-response.dto.ts @@ -0,0 +1,78 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; +import { OrganisationRequestDto } from './organisation.dto'; + +class RoleDto { + @ApiProperty({ + description: 'The name of the role', + }) + @IsString() + name: string; + + @ApiProperty({ + description: 'List of Permissions', + }) + permissions: string[]; +} + +class MemberOrgDto { + @ApiProperty({ + description: "List of User's Role in Organisation", + }) + role: RoleDto; + + @ApiProperty({ + description: 'Organisation details', + }) + organisation: OrganisationRequestDto; +} + +class DataDto { + @ApiProperty({ + description: "List of User's Created Organisations", + }) + created_organisations: OrganisationRequestDto[]; + + @ApiProperty({ + description: "List of User's Owned Organisations", + }) + owned_organisations: OrganisationRequestDto[]; + + @ApiProperty({ + description: "List of User's Member Organisations", + }) + member_organisations: MemberOrgDto[]; +} + +export class UserOrganizationResponseDto { + @ApiProperty({ + description: 'Response message', + example: 'Organisations successfully retrieved', + }) + @IsString() + message: string; + + @ApiProperty({ + description: 'Response status', + }) + status: number; + + @ApiProperty({ + description: "List of all the user's organisations", + }) + data: DataDto; +} + +export class UserOrganizationErrorResponseDto { + @ApiProperty({ + description: 'Response message', + example: 'User has no organisations', + }) + @IsString() + message: string; + + @ApiProperty({ + description: 'Error response status code', + }) + status: number; +} diff --git a/src/modules/organisations/entities/org-members.entity.ts b/src/modules/organisations/entities/org-members.entity.ts index b66bee689..da441dd92 100644 --- a/src/modules/organisations/entities/org-members.entity.ts +++ b/src/modules/organisations/entities/org-members.entity.ts @@ -3,19 +3,18 @@ import { AbstractBaseEntity } from './../../../entities/base.entity'; import { User } from '../../user/entities/user.entity'; import { Organisation } from './organisations.entity'; import { Profile } from '../../profile/entities/profile.entity'; -import { OrganisationRole } from '../../organisation-role/entities/organisation-role.entity'; -@Entity() -export class OrganisationMember extends AbstractBaseEntity { - @ManyToOne(() => User, user => user.organisationMembers) - user_id: User; +// @Entity() +// export class OrganisationMember extends AbstractBaseEntity { +// @ManyToOne(() => User, user => user.organisationMembers) +// user_id: User; - @ManyToOne(() => Organisation, organisation => organisation.organisationMembers) - organisation_id: Organisation; +// @ManyToOne(() => Organisation, organisation => organisation.organisationMembers) +// organisation_id: Organisation; - @ManyToOne(() => OrganisationRole, role => role.organisationMembers) - role: OrganisationRole; +// @ManyToOne(() => OrganisationRole, role => role.organisationMembers) +// role: OrganisationRole; - @ManyToOne(() => Profile) - profile_id: Profile; -} +// @ManyToOne(() => Profile) +// profile_id: Profile; +// } diff --git a/src/modules/organisations/entities/organisations.entity.ts b/src/modules/organisations/entities/organisations.entity.ts index 50bfdf6e0..918b7e45b 100644 --- a/src/modules/organisations/entities/organisations.entity.ts +++ b/src/modules/organisations/entities/organisations.entity.ts @@ -1,11 +1,9 @@ -import { Entity, Column, ManyToOne, OneToMany } from 'typeorm'; +import { Entity, Column, ManyToOne, OneToMany, ManyToMany } from 'typeorm'; import { User } from '../../user/entities/user.entity'; import { OrganisationPreference } from './org-preferences.entity'; import { AbstractBaseEntity } from '../../../entities/base.entity'; import { Invite } from '../../invite/entities/invite.entity'; -import { OrganisationMember } from './org-members.entity'; import { Product } from '../../../modules/products/entities/product.entity'; -import { OrganisationRole } from '../../organisation-role/entities/organisation-role.entity'; @Entity() export class Organisation extends AbstractBaseEntity { @@ -33,12 +31,12 @@ export class Organisation extends AbstractBaseEntity { @ManyToOne(() => User, user => user.owned_organisations, { nullable: false }) owner: User; + @ManyToMany(() => User, user => user.organisations, { nullable: false }) + members: User[]; + @Column({ nullable: false }) state: string; - @ManyToOne(() => User, user => user.created_organisations, { nullable: false }) - creator: User; - @Column('boolean', { default: false, nullable: false }) isDeleted: boolean; @@ -48,12 +46,6 @@ export class Organisation extends AbstractBaseEntity { @OneToMany(() => OrganisationPreference, preference => preference.organisation) preferences: OrganisationPreference[]; - @OneToMany(() => OrganisationRole, role => role.organisation, { eager: false }) - role: OrganisationRole[]; - @OneToMany(() => Invite, invite => invite.organisation.id) invites: Invite[]; - - @OneToMany(() => OrganisationMember, organisationMember => organisationMember.organisation_id) - organisationMembers: OrganisationMember[]; } diff --git a/src/modules/organisations/interfaces/OrganisationInterface.ts b/src/modules/organisations/interfaces/OrganisationInterface.ts new file mode 100644 index 000000000..49a6a83f2 --- /dev/null +++ b/src/modules/organisations/interfaces/OrganisationInterface.ts @@ -0,0 +1,17 @@ +export interface OrganisationInterface { + name: string; + + description: string; + + email: string; + + industry: string; + + type: string; + + country: string; + + address: string; + + state: string; +} diff --git a/src/modules/organisations/mapper/create-organisation.mapper.ts b/src/modules/organisations/mapper/create-organisation.mapper.ts index fe0dec03b..1f3f82a7c 100644 --- a/src/modules/organisations/mapper/create-organisation.mapper.ts +++ b/src/modules/organisations/mapper/create-organisation.mapper.ts @@ -19,7 +19,6 @@ export class CreateOrganisationMapper { organisation.address = dto.address; organisation.state = dto.state; organisation.owner = owner; - organisation.creator = owner; return organisation; } diff --git a/src/modules/organisations/mapper/member-role.mapper.ts b/src/modules/organisations/mapper/member-role.mapper.ts new file mode 100644 index 000000000..409798024 --- /dev/null +++ b/src/modules/organisations/mapper/member-role.mapper.ts @@ -0,0 +1,18 @@ +// 0d733c90-fbfe-4980-b983-1483e4ac224a +export class MemberRoleMapper { + static mapToResponseFormat(orgRole) { + if (!orgRole) { + throw new Error('Organisation role entity is required'); + } + const permissions = orgRole.permissions.map(permission => { + if (permission.permission_list) { + return permission.category; + } + }); + + return { + name: orgRole.name, + permissions, + }; + } +} diff --git a/src/modules/organisations/mapper/organisation.mapper.ts b/src/modules/organisations/mapper/organisation.mapper.ts index 7b8a6ae7c..f687b9f7e 100644 --- a/src/modules/organisations/mapper/organisation.mapper.ts +++ b/src/modules/organisations/mapper/organisation.mapper.ts @@ -10,15 +10,15 @@ export class OrganisationMapper { id: organisation.id, name: organisation.name, description: organisation.description, - owner_id: organisation.creator.id, + owner_id: organisation?.owner.id, email: organisation.email, industry: organisation.industry, type: organisation.type, country: organisation.country, address: organisation.address, state: organisation.state, - created_at: organisation.created_at.toISOString(), - updated_at: organisation.updated_at.toISOString(), + created_at: organisation.created_at, + updated_at: organisation.updated_at, }; } } diff --git a/src/modules/organisations/organisations.controller.ts b/src/modules/organisations/organisations.controller.ts index 52b573d74..c91dcc5ee 100644 --- a/src/modules/organisations/organisations.controller.ts +++ b/src/modules/organisations/organisations.controller.ts @@ -4,76 +4,74 @@ import { DefaultValuePipe, Delete, Get, + InternalServerErrorException, + NotFoundException, Param, ParseIntPipe, + ParseUUIDPipe, Patch, Post, + Put, Query, Req, Res, UseGuards, + Logger, + HttpStatus, } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { OwnershipGuard } from '../../guards/authorization.guard'; import { OrganisationMembersResponseDto } from './dto/org-members-response.dto'; import { OrganisationRequestDto } from './dto/organisation.dto'; -import { UpdateOrganisationDto } from './dto/update-organisation.dto'; import { OrganisationsService } from './organisations.service'; +import { UpdateOrganisationDto } from './dto/update-organisation.dto'; +import { UpdateMemberRoleDto } from './dto/update-organisation-role.dto'; +import { RemoveOrganisationMemberDto } from './dto/org-member.dto'; +import { UserOrganizationErrorResponseDto, UserOrganizationResponseDto } from './dto/user-orgs-response.dto'; +import { AddMemberDto } from './dto/add-member.dto'; +import { Response } from 'express'; +import { createReadStream } from 'fs'; +import { pipeline } from 'stream/promises'; +import { unlink } from 'fs/promises'; @ApiBearerAuth() -@ApiTags('organization') -@Controller('organizations') +@ApiTags('organisation') +@Controller('organisations') export class OrganisationsController { + private readonly logger = new Logger(OrganisationsController.name); constructor(private readonly organisationsService: OrganisationsService) {} @ApiOperation({ summary: 'Create new Organisation' }) - @ApiResponse({ - status: 201, - description: 'The created organisation', - }) - @ApiResponse({ - status: 409, - description: 'Organisation email already exists', - }) + @ApiResponse({ status: 201, description: 'The created organisation' }) + @ApiResponse({ status: 409, description: 'Organisation email already exists' }) @Post('/') async create(@Body() createOrganisationDto: OrganisationRequestDto, @Req() req) { const user = req['user']; - return this.organisationsService.create(createOrganisationDto, user.sub); + return this.organisationsService.createOrganisation(createOrganisationDto, user.sub); } + @UseGuards(OwnershipGuard) @Delete(':org_id') - async delete(@Param('org_id') id: string, @Res() response: Response) { - this.organisationsService; + async delete(@Param('org_id') id: string) { return this.organisationsService.deleteorganisation(id); } @ApiOperation({ summary: 'Update Organisation' }) - @ApiResponse({ - status: 200, - description: 'The found record', - type: UpdateOrganisationDto, - }) + @ApiResponse({ status: 200, description: 'Organisation updated successfully' }) + @ApiResponse({ status: 400, description: 'Bad request' }) + @ApiResponse({ status: 401, description: 'User is currently not authorized, kindly authenticate to continue' }) + @ApiResponse({ status: 403, description: 'You do not have permission to update this organisation' }) + @ApiResponse({ status: 404, description: 'Organisation not found' }) @UseGuards(OwnershipGuard) - @Patch(':id') - async update(@Param('id') id: string, @Body() updateOrganisationDto: UpdateOrganisationDto) { - const updatedOrg = await this.organisationsService.updateOrganisation(id, updateOrganisationDto); - return { message: 'Organisation successfully updated', org: updatedOrg }; + @Patch(':orgId') + async update(@Param('orgId') orgId: string, @Body() updateOrganisationDto: UpdateOrganisationDto) { + return await this.organisationsService.updateOrganisation(orgId, updateOrganisationDto); } @ApiOperation({ summary: 'Get members of an Organisation' }) - @ApiResponse({ - status: 200, - description: 'The found record', - type: OrganisationMembersResponseDto, - }) - @ApiResponse({ - status: 404, - description: 'Organisation not found', - }) - @ApiResponse({ - status: 403, - description: 'User not a member of the organisation', - }) + @ApiResponse({ status: 200, description: 'The found record', type: OrganisationMembersResponseDto }) + @ApiResponse({ status: 404, description: 'Organisation not found' }) + @ApiResponse({ status: 403, description: 'User not a member of the organisation' }) @Get(':org_id/users') async getMembers( @Req() req, @@ -84,4 +82,54 @@ export class OrganisationsController { const { sub } = req.user; return this.organisationsService.getOrganisationMembers(org_id, page, page_size, sub); } + + @ApiOperation({ summary: "Gets a user's organizations" }) + @ApiResponse({ status: 200, description: 'Organisations retrieved successfully', type: UserOrganizationResponseDto }) + @ApiResponse({ status: 400, description: 'Bad request', type: UserOrganizationErrorResponseDto }) + @Get('/') + async getUserOrganisations(@Req() req) { + const { sub } = req.user; + return this.organisationsService.getUserOrganisations(sub); + } + + @UseGuards(OwnershipGuard) + @ApiOperation({ summary: 'Add member to an organization' }) + @ApiResponse({ status: 201, description: 'Member added successfully' }) + @ApiResponse({ status: 409, description: 'User already added to organization.' }) + @ApiResponse({ status: 404, description: 'Organisation not found' }) + @Post(':org_id/users') + async addMember(@Param('org_id', ParseUUIDPipe) org_id: string, @Body() addMemberDto: AddMemberDto) { + return this.organisationsService.addOrganisationMember(org_id, addMemberDto); + } + + @UseGuards(OwnershipGuard) + @ApiOperation({ summary: 'Assign roles to members of an organisation' }) + @ApiResponse({ + status: 200, + description: 'Assign roles to members of an organisation', + schema: { + properties: { + status: { type: 'string' }, + message: { type: 'string' }, + data: { + type: 'object', + properties: { + user: { type: 'string' }, + org: { type: 'string' }, + role: { type: 'string' }, + }, + }, + }, + }, + }) + @ApiResponse({ status: 409, description: 'User already added to organization.' }) + @ApiResponse({ status: 403, description: 'User not a member of the organisation' }) + @Put(':org_id/users/:user_id/role') + async updateMemberRole( + @Param('user_id') memberId: string, + @Param('org_id') orgId: string, + @Body() updateMemberRoleDto: UpdateMemberRoleDto + ) { + return await this.organisationsService.updateMemberRole(orgId, memberId, updateMemberRoleDto); + } } diff --git a/src/modules/organisations/organisations.module.ts b/src/modules/organisations/organisations.module.ts index bfa5acc0a..24c7771c1 100644 --- a/src/modules/organisations/organisations.module.ts +++ b/src/modules/organisations/organisations.module.ts @@ -5,11 +5,29 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { Organisation } from './entities/organisations.entity'; import { User } from '../user/entities/user.entity'; import { UserModule } from '../user/user.module'; -import { OrganisationMember } from './entities/org-members.entity'; +import { OrganisationUserRole } from '../role/entities/organisation-user-role.entity'; +import { Role } from '../role/entities/role.entity'; +import UserService from '../user/user.service'; +import { InviteModule } from '../invite/invite.module'; +import { Permissions } from '../permissions/entities/permissions.entity'; +import { Profile } from '../profile/entities/profile.entity'; @Module({ - imports: [TypeOrmModule.forFeature([Organisation, User, OrganisationMember]), UserModule], + imports: [ + TypeOrmModule.forFeature([ + Organisation, + User, + OrganisationUserRole, + Role, + Organisation, + User, + Permissions, + Profile, + ]), + UserModule, + InviteModule, + ], controllers: [OrganisationsController], - providers: [OrganisationsService], + providers: [OrganisationsService, UserService], }) export class OrganisationsModule {} diff --git a/src/modules/organisations/organisations.service.ts b/src/modules/organisations/organisations.service.ts index a546b48d7..458907edc 100644 --- a/src/modules/organisations/organisations.service.ts +++ b/src/modules/organisations/organisations.service.ts @@ -10,14 +10,17 @@ import { import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { User } from '../user/entities/user.entity'; -import { OrganisationMembersResponseDto } from './dto/org-members-response.dto'; -import { OrganisationRequestDto } from './dto/organisation.dto'; import { UpdateOrganisationDto } from './dto/update-organisation.dto'; -import { OrganisationMember } from './entities/org-members.entity'; import { Organisation } from './entities/organisations.entity'; -import { CreateOrganisationMapper } from './mapper/create-organisation.mapper'; -import { OrganisationMemberMapper } from './mapper/org-members.mapper'; import { OrganisationMapper } from './mapper/organisation.mapper'; +import { Role } from '../role/entities/role.entity'; +import { OrganisationUserRole } from '../role/entities/organisation-user-role.entity'; +import CreateOrganisationType from './dto/create-organisation-options'; +import { CustomHttpException } from '../../helpers/custom-http-filter'; +import { OrganisationMemberMapper } from './mapper/org-members.mapper'; +import { UpdateMemberRoleDto } from './dto/update-organisation-role.dto'; +import { AddMemberDto } from './dto/add-member.dto'; +import * as SYS_MSG from '../../helpers/SystemMessages'; @Injectable() export class OrganisationsService { @@ -26,60 +29,75 @@ export class OrganisationsService { private readonly organisationRepository: Repository, @InjectRepository(User) private readonly userRepository: Repository, - @InjectRepository(OrganisationMember) - private readonly organisationMemberRepository: Repository + + @InjectRepository(OrganisationUserRole) + private organisationUserRole: Repository, + + @InjectRepository(Role) + private roleRepository: Repository ) {} - async getOrganisationMembers( - orgId: string, - page: number, - page_size: number, - sub: string - ): Promise { + async getOrganisationMembers(orgId: string, page: number, page_size: number, sub: string) { const skip = (page - 1) * page_size; - const orgs = await this.organisationRepository.findOne({ + const organisation = await this.organisationRepository.findOne({ where: { id: orgId }, - relations: ['organisationMembers', 'organisationMembers.user_id'], }); - if (!orgs) throw new NotFoundException('No organisation found'); + if (!organisation) throw new NotFoundException('No organisation found'); - let data = orgs.organisationMembers.map(member => { - return OrganisationMemberMapper.mapToResponseFormat(member.user_id); + const members = await this.organisationUserRole.find({ + where: { organisationId: organisation.id }, + relations: ['user'], }); - const isMember = data.find(member => member.id === sub); + if (!members.length) { + return { status_code: HttpStatus.OK, message: 'members retrieved successfully', data: [] }; + } + const organisationMembers = members.map(instance => instance.user); + + const isMember = organisationMembers.find(member => member.id === sub); if (!isMember) throw new ForbiddenException('User does not have access to the organisation'); - data = data.splice(skip, skip + page_size); + const organisationPayload = organisationMembers.map(member => OrganisationMemberMapper.mapToResponseFormat(member)); + + const data = organisationPayload.splice(skip, skip + page_size); return { status_code: HttpStatus.OK, message: 'members retrieved successfully', data }; } - async create(createOrganisationDto: OrganisationRequestDto, userId: string) { - const emailFound = await this.emailExists(createOrganisationDto.email); - if (emailFound) throw new ConflictException('Organisation with this email already exists'); + async createOrganisation(createOrganisationDto: CreateOrganisationType, userId: string) { + const query = await this.create(createOrganisationDto, userId); + return { status_code: HttpStatus.CREATED, messge: 'Organisation created', data: query }; + } + + async create(createOrganisationDto: CreateOrganisationType, userId: string) { + if (createOrganisationDto.email) { + const emailFound = await this.emailExists(createOrganisationDto.email); + if (emailFound) throw new ConflictException('Organisation with this email already exists'); + } const owner = await this.userRepository.findOne({ where: { id: userId }, }); - const mapNewOrganisation = CreateOrganisationMapper.mapToEntity(createOrganisationDto, owner); - const newOrganisation = this.organisationRepository.create({ - ...mapNewOrganisation, - }); + const vendorRole = await this.roleRepository.findOne({ where: { name: 'admin' } }); - await this.organisationRepository.save(newOrganisation); + const organisationInstance = new Organisation(); + Object.assign(organisationInstance, createOrganisationDto); + organisationInstance.owner = owner; + organisationInstance.members = [owner]; + const newOrganisation = await this.organisationRepository.save(organisationInstance); - const newMember = new OrganisationMember(); - newMember.user_id = owner; - newMember.organisation_id = newOrganisation; + const adminRole = new OrganisationUserRole(); + adminRole.userId = owner.id; + adminRole.organisationId = newOrganisation.id; + adminRole.roleId = vendorRole.id; - await this.organisationMemberRepository.save(newMember); + await this.organisationUserRole.save(adminRole); const mappedResponse = OrganisationMapper.mapToResponseFormat(newOrganisation); - return { status: 'success', message: 'organisation created successfully', data: mappedResponse }; + return mappedResponse; } async deleteorganisation(id: string) { @@ -98,29 +116,143 @@ export class OrganisationsService { throw new InternalServerErrorException(`An internal server error occurred: ${error.message}`); } } + async emailExists(email: string): Promise { const emailFound = await this.organisationRepository.findBy({ email }); return emailFound?.length ? true : false; } - async updateOrganisation( - id: string, - updateOrganisationDto: UpdateOrganisationDto - ): Promise<{ message: string; org: Organisation }> { - try { - const org = await this.organisationRepository.findOneBy({ id }); - if (!org) { - throw new NotFoundException('organisation not found'); - } - await this.organisationRepository.update(id, updateOrganisationDto); - const updatedOrg = await this.organisationRepository.findOneBy({ id }); + // // TO BE UPDATED + async updateOrganisation(orgId: string, updateOrganisationDto: UpdateOrganisationDto) { + const organisation = await this.organisationRepository.findOne({ where: { id: orgId } }); - return { message: 'Organisation successfully updated', org: updatedOrg }; - } catch (error) { - if (error instanceof NotFoundException || error instanceof BadRequestException) { - throw error; - } - throw new InternalServerErrorException(`An internal server error occurred: ${error.message}`); + if (!organisation) { + throw new CustomHttpException(SYS_MSG.ORG_NOT_FOUND, HttpStatus.NOT_FOUND); } + + await this.organisationRepository.update(orgId, updateOrganisationDto); + const updatedOrg = await this.organisationRepository.findOne({ where: { id: orgId } }); + + return { + message: SYS_MSG.ORG_UPDATE, + data: updatedOrg, + }; + } + + async getUserOrganisations(userId: string) { + const organisations = await this.getAllUserOrganisations(userId); + return { + status_code: HttpStatus.OK, + message: 'Organisations retrieved successfully', + data: organisations, + }; + } + + async getAllUserOrganisations(userId: string) { + const user = await this.userRepository.findOne({ where: { id: userId } }); + + if (!user) { + throw new CustomHttpException('Invalid Request', HttpStatus.BAD_REQUEST); + } + const userOrganisations = ( + await this.organisationUserRole.find({ + where: { userId }, + relations: ['organisation', 'organisation.owner', 'role'], + }) + ).map(instance => ({ + organisation_id: instance?.organisation?.id || '', + name: instance?.organisation?.name, + user_role: instance.role.name, + is_owner: instance.organisation ? instance.organisation.owner.id === user.id : '', + })); + + return userOrganisations; + } + + async addOrganisationMember(org_id: string, addMemberDto: AddMemberDto) { + const organisation = await this.organisationRepository.findOneBy({ id: org_id }); + if (!organisation) { + throw new CustomHttpException(SYS_MSG.ORG_NOT_FOUND, HttpStatus.NOT_FOUND); + } + + const user = await this.userRepository.findOne({ + where: { id: addMemberDto.user_id }, + relations: ['organisations'], + }); + + if (!user) { + throw new CustomHttpException(SYS_MSG.USER_NOT_FOUND, HttpStatus.NOT_FOUND); + } + + const existingMember = user.organisations.some(org => org.id === organisation.id); + + if (existingMember) { + throw new CustomHttpException(SYS_MSG.MEMBER_ALREADY_EXISTS, HttpStatus.CONFLICT); + } + + const userRole = await this.roleRepository.findOne({ where: { name: 'user' } }); + + const defaultRole = new OrganisationUserRole(); + defaultRole.userId = user.id; + defaultRole.user = user; + defaultRole.organisation = organisation; + defaultRole.organisationId = organisation.id; + defaultRole.roleId = userRole.id; + + await this.organisationUserRole.save(defaultRole); + + user.organisations = [...user.organisations, organisation]; + await this.userRepository.save(user); + + const responsePayload = { + id: user.id, + first_name: user.first_name, + last_name: user.last_name, + email: user.email, + }; + + return { status: 'success', message: SYS_MSG.MEMBER_ALREADY_SUCCESSFULLY, member: responsePayload }; + } + + async updateMemberRole(org_id: string, member_id: string, updateMemberRoleDto: UpdateMemberRoleDto) { + const organisation = await this.organisationRepository.findOne({ + where: { id: org_id }, + }); + + if (!organisation) { + throw new CustomHttpException(SYS_MSG.ORG_NOT_FOUND, HttpStatus.NOT_FOUND); + } + + const orgUserRole = await this.organisationUserRole.findOne({ + where: { + userId: member_id, + organisationId: org_id, + }, + relations: ['user', 'role', 'organisation'], + }); + + if (!orgUserRole) { + throw new CustomHttpException(SYS_MSG.ORG_MEMBER_DOES_NOT_BELONG, HttpStatus.FORBIDDEN); + } + + const newRole = await this.roleRepository.findOne({ + where: { name: updateMemberRoleDto.role }, + }); + + if (!newRole) { + throw new CustomHttpException(SYS_MSG.ROLE_NOT_FOUND, HttpStatus.NOT_FOUND); + } + + orgUserRole.role = newRole; + await this.organisationUserRole.save(orgUserRole); + + return { + message: `${orgUserRole.user.first_name} ${orgUserRole.user.last_name} has successfully been assigned the ${newRole.name} role`, + data: { + user: orgUserRole.user, + organisation: orgUserRole.organisation, + role: newRole, + }, + }; } } diff --git a/src/modules/organisations/tests/mocks/create-organisation-response.mock.ts b/src/modules/organisations/tests/mocks/create-organisation-response.mock.ts index a13461165..d9e62b9cb 100644 --- a/src/modules/organisations/tests/mocks/create-organisation-response.mock.ts +++ b/src/modules/organisations/tests/mocks/create-organisation-response.mock.ts @@ -9,7 +9,6 @@ export const createOrganisationResponseDtoMock = (overrides?: Partial { + const org = new Organisation(); + org.id = uuidv4(); + const profileMock: Profile = { id: 'some-uuid', username: 'mockuser', @@ -28,28 +30,6 @@ export const createMockOrganisation = (): Organisation => { updated_at: new Date(), }; - // Create a mock object that matches the OrganisationRole interface - const organisationRoleMock: OrganisationRole = { - id: uuidv4(), - name: 'Admin', - description: 'Administrator role with full permissions', - permissions: [], - organisation: null, - organisationMembers: [], - created_at: new Date(), - updated_at: new Date(), - }; - - const orgMemberMock: OrganisationMember = { - id: uuidv4(), - created_at: new Date(), - updated_at: new Date(), - user_id: null, - role: organisationRoleMock, - organisation_id: null, - profile_id: profileMock, - }; - const ownerAndCreator = { id: uuidv4(), created_at: new Date(), @@ -62,27 +42,29 @@ export const createMockOrganisation = (): Organisation => { two_factor_secret: 'some-secret', backup_codes: [], jobs: [], + status: 'Hello from the children of planet Earth', phone: '+1234567890', hashPassword: async () => {}, is_active: true, attempts_left: 3, time_left: 3600, owned_organisations: [], - created_organisations: [], invites: [], testimonials: [], notifications: [], notification_settings: [], - user_type: UserType.ADMIN, secret: 'secret', is_2fa_enabled: false, products: [], profile: profileMock, - organisationMembers: [orgMemberMock], + blogs: [], + comments: [], + cart: [], + organisations: null, }; return { - id: uuidv4(), + ...org, name: 'John & Co', description: 'An imports organisation', email: 'johnCo@example.com', @@ -92,15 +74,13 @@ export const createMockOrganisation = (): Organisation => { address: 'Street 101 Building 26', state: 'Lagos', owner: ownerAndCreator, - creator: { ...ownerAndCreator, user_type: UserType.USER }, created_at: new Date(), updated_at: new Date(), isDeleted: false, preferences: [], invites: [], - role: null, - organisationMembers: [orgMemberMock], products: [], + members: null, }; }; diff --git a/src/modules/organisations/tests/mocks/profile.mock.ts b/src/modules/organisations/tests/mocks/profile.mock.ts new file mode 100644 index 000000000..895d739a5 --- /dev/null +++ b/src/modules/organisations/tests/mocks/profile.mock.ts @@ -0,0 +1,18 @@ +import { Profile } from '../../../profile/entities/profile.entity'; + +export const profileMock: Profile = { + id: 'some-uuid', + username: 'mockuser', + jobTitle: 'Developer', + pronouns: 'They/Them', + department: 'Engineering', + email: 'mockuser@example.com', + bio: 'A mock user for testing purposes', + social_links: [], + language: 'English', + region: 'US', + timezones: 'America/New_York', + profile_pic_url: '', + created_at: new Date(), + updated_at: new Date(), +}; diff --git a/src/modules/organisations/tests/mocks/user.mock.ts b/src/modules/organisations/tests/mocks/user.mock.ts new file mode 100644 index 000000000..776bc3928 --- /dev/null +++ b/src/modules/organisations/tests/mocks/user.mock.ts @@ -0,0 +1,36 @@ +import { UserType } from './organisation.mock'; +import { mockProfile } from '../../../../modules/profile/mocks/profileMock'; + +export const mockUser = { + id: 'user123', + created_at: new Date(), + updated_at: new Date(), + first_name: 'John', + last_name: 'Smith', + email: 'john.smith@example.com', + password: 'pass123', + status: 'Hello from the children of planet Earth', + is_two_factor_enabled: false, + two_factor_secret: 'some-secret', + backup_codes: [], + jobs: [], + phone: '+1234567890', + hashPassword: async () => {}, + is_active: true, + attempts_left: 3, + time_left: 3600, + owned_organisations: [], + invites: [], + testimonials: [], + notifications: [], + notification_settings: [], + user_type: 'admin' as UserType, + secret: 'secret', + is_2fa_enabled: false, + products: [], + profile: mockProfile, + blogs: null, + comments: null, + cart: [], + organisations: null, +}; diff --git a/src/modules/organisations/tests/organisations.service.spec.ts b/src/modules/organisations/tests/organisations.service.spec.ts index 92a155de2..9277c3426 100644 --- a/src/modules/organisations/tests/organisations.service.spec.ts +++ b/src/modules/organisations/tests/organisations.service.spec.ts @@ -15,16 +15,21 @@ import { UnprocessableEntityException, ForbiddenException, ConflictException, + HttpStatus, } from '@nestjs/common'; import { Profile } from '../../profile/entities/profile.entity'; -import { OrganisationMember } from '../entities/org-members.entity'; +import { OrganisationUserRole } from '../../../modules/role/entities/organisation-user-role.entity'; +import { Role } from '../../../modules/role/entities/role.entity'; +import { CustomHttpException } from '../../../helpers/custom-http-filter'; describe('OrganisationsService', () => { let service: OrganisationsService; let userRepository: Repository; let organisationRepository: Repository; + let permisssionsRepository: Repository; let profileRepository: Repository; - let organisationMemberRepository: Repository; + let organisationUserRole: Repository; + let roleRepository: Repository; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -41,15 +46,27 @@ describe('OrganisationsService', () => { update: jest.fn(), }, }, + + UserService, + { + provide: getRepositoryToken(User), + useValue: { + findBy: jest.fn(), + find: jest.fn(), + findOne: jest.fn(), + }, + }, { - provide: getRepositoryToken(OrganisationMember), + provide: getRepositoryToken(OrganisationUserRole), useValue: { + findBy: jest.fn(), + find: jest.fn(), + findOne: jest.fn(), save: jest.fn(), }, }, - UserService, { - provide: getRepositoryToken(User), + provide: getRepositoryToken(Role), useValue: { findBy: jest.fn(), find: jest.fn(), @@ -71,39 +88,43 @@ describe('OrganisationsService', () => { service = module.get(OrganisationsService); userRepository = module.get>(getRepositoryToken(User)); organisationRepository = module.get>(getRepositoryToken(Organisation)); - organisationMemberRepository = module.get>(getRepositoryToken(OrganisationMember)); profileRepository = module.get>(getRepositoryToken(Profile)); + organisationUserRole = module.get(getRepositoryToken(OrganisationUserRole)); + roleRepository = module.get(getRepositoryToken(Role)); }); it('should be defined', () => { expect(service).toBeDefined(); }); + describe('create', () => { + it('should create a new organisation', async () => { + const createOrganisationDto = { name: 'Test Org', email: 'test@example.com' }; + const userId = 'user-id'; + const user = { id: userId }; + const superAdminRole = { id: 'role-id', name: 'super_admin', description: '', permissions: [] }; + const newOrganisation = { ...createOrganisationDto, id: 'org-id', owner: user }; + const adminReponse = { + id: 'some-id', + userId, + roleId: 'role-id', + organisationId: 'org-id', + } as OrganisationUserRole; - describe('create organisation', () => { - beforeEach(async () => { - const errors = await validate(createMockOrganisationRequestDto()); - expect(errors).toHaveLength(0); - }); - - it('should create an organisation', async () => { jest.spyOn(organisationRepository, 'findBy').mockResolvedValue(null); - jest.spyOn(userRepository, 'findOne').mockResolvedValue({ - ...orgMock.owner, - } as User); - jest.spyOn(organisationRepository, 'create').mockReturnValue(orgMock); - jest.spyOn(organisationRepository, 'save').mockResolvedValue({ - ...orgMock, - }); + jest.spyOn(userRepository, 'findOne').mockResolvedValue(user as User); + jest.spyOn(roleRepository, 'findOne').mockResolvedValue(superAdminRole as Role); + jest.spyOn(organisationRepository, 'save').mockResolvedValue(newOrganisation as Organisation); + jest.spyOn(organisationUserRole, 'save').mockResolvedValue(adminReponse); - const result = await service.create(createMockOrganisationRequestDto(), orgMock.owner.id); - expect(result.status).toEqual('success'); - expect(result.message).toEqual('organisation created successfully'); - }); + const result = await service.create(createOrganisationDto, userId); - it('should throw an error if the email already exists', async () => { - organisationRepository.findBy = jest.fn().mockResolvedValue([orgMock]); - await expect(service.create(createMockOrganisationRequestDto(), orgMock.owner.id)).rejects.toThrow( - new ConflictException('Organisation with this email already exists') + expect(result).toEqual( + expect.objectContaining({ + id: 'org-id', + name: 'Test Org', + email: 'test@example.com', + owner_id: 'user-id', // Matching the owner_id instead of nested owner object + }) ); }); }); @@ -114,6 +135,8 @@ describe('OrganisationsService', () => { const updateOrganisationDto = { name: 'New Name', description: 'Updated Description' }; const organisation = new Organisation(); + jest.spyOn(organisationRepository, 'findOne').mockResolvedValue(organisation); + jest.spyOn(organisationRepository, 'update').mockResolvedValue({} as any); jest.spyOn(organisationRepository, 'findOneBy').mockResolvedValueOnce(organisation); jest.spyOn(organisationRepository, 'update').mockResolvedValueOnce({ affected: 1 } as any); jest @@ -122,28 +145,17 @@ describe('OrganisationsService', () => { const result = await service.updateOrganisation(id, updateOrganisationDto); - expect(result).toEqual({ - message: 'Organisation successfully updated', - org: { ...organisation, ...updateOrganisationDto }, - }); + expect(result.message).toEqual('Organisation updated successfully'); + expect(result.data).toBeDefined(); }); - it('should throw NotFoundException if organisation not found', async () => { + it('should throw CustomHttpException if organisation not found', async () => { const id = '1'; const updateOrganisationDto = { name: 'New Name', description: 'Updated Description' }; jest.spyOn(organisationRepository, 'findOneBy').mockResolvedValueOnce(null); - await expect(service.updateOrganisation(id, updateOrganisationDto)).rejects.toThrow(NotFoundException); - }); - - it('should throw InternalServerErrorException if an unexpected error occurs', async () => { - const id = '1'; - const updateOrganisationDto = { name: 'New Name', description: 'Updated Description' }; - - jest.spyOn(organisationRepository, 'findOneBy').mockRejectedValueOnce(new Error('Unexpected error')); - - await expect(service.updateOrganisation(id, updateOrganisationDto)).rejects.toThrow(InternalServerErrorException); + await expect(service.updateOrganisation(id, updateOrganisationDto)).rejects.toThrow(CustomHttpException); }); }); @@ -164,61 +176,94 @@ describe('OrganisationsService', () => { ], } as unknown as Organisation; + const mockOrganisationUserRole = { + orgId: 'new-org', + roleId: 'role-id', + userId: 'user-id', + user: { id: 'user-id' } as User, + }; + organisationRepository.findOne = jest.fn().mockResolvedValue(mockOrganisation); + organisationUserRole.find = jest.fn().mockResolvedValue([mockOrganisationUserRole]); await expect(service.getOrganisationMembers('orgId', 1, 10, 'sub')).rejects.toThrow(ForbiddenException); }); + }); - it('should return paginated members if the user is a member', async () => { - const mockOrganisation = { - id: 'orgId', - organisationMembers: [ - { user_id: { id: 'sub', first_name: 'John', last_name: 'Doe', email: 'john@email.com', phone: '0000' } }, - { - user_id: { - id: 'anotherUserId', - first_name: 'Jane', - last_name: 'Doe', - email: 'jane@email.com', - phone: '1111', - }, - }, - ], - } as unknown as Organisation; + describe('updateMemberRole', () => { + it('should update member role successfully', async () => { + const orgId = 'org-id'; + const memberId = 'member-id'; + const updateMemberRoleDto = { role: 'new-role' }; - organisationRepository.findOne = jest.fn().mockResolvedValue(mockOrganisation); + const mockOrganisation = { id: orgId } as Organisation; + const mockUser = { id: memberId, first_name: 'John', last_name: 'Doe' } as User; + const mockOrgUserRole = { + userId: memberId, + organisationId: orgId, + user: mockUser, + organisation: mockOrganisation, + role: { name: 'old-role' }, + } as OrganisationUserRole; + const mockNewRole = { name: 'new-role' } as Role; + + jest.spyOn(organisationRepository, 'findOne').mockResolvedValue(mockOrganisation); + jest.spyOn(organisationUserRole, 'findOne').mockResolvedValue(mockOrgUserRole); + jest.spyOn(roleRepository, 'findOne').mockResolvedValue(mockNewRole); + jest.spyOn(organisationUserRole, 'save').mockResolvedValue({ ...mockOrgUserRole, role: mockNewRole }); - const result = await service.getOrganisationMembers('orgId', 1, 1, 'sub'); + const result = await service.updateMemberRole(orgId, memberId, updateMemberRoleDto); - expect(result.status_code).toBe(200); - expect(result.data).toEqual([{ id: 'sub', name: 'John Doe', email: 'john@email.com', phone_number: '0000' }]); + expect(result.message).toContain('has successfully been assigned the new-role role'); + expect(result.data).toEqual({ + user: mockUser, + organisation: mockOrganisation, + role: mockNewRole, + }); }); - it('should paginate members correctly', async () => { - const mockOrganisation = { - id: 'orgId', - organisationMembers: [ - { user_id: { id: 'sub', first_name: 'John', last_name: 'Doe', email: 'john@email.com', phone: '0000' } }, - { - user_id: { - id: 'anotherUserId', - first_name: 'Jane', - last_name: 'Doe', - email: 'jane@email.com', - phone: '1111', - }, - }, - ], - } as unknown as Organisation; + it('should throw CustomHttpException if organisation is not found', async () => { + const orgId = 'non-existent-org-id'; + const memberId = 'member-id'; + const updateMemberRoleDto = { role: 'new-role' }; - organisationRepository.findOne = jest.fn().mockResolvedValue(mockOrganisation); + jest.spyOn(organisationRepository, 'findOne').mockResolvedValue(null); + + await expect(service.updateMemberRole(orgId, memberId, updateMemberRoleDto)).rejects.toThrow(CustomHttpException); + }); + + it('should throw CustomHttpException if member does not belong to the organisation', async () => { + const orgId = 'org-id'; + const memberId = 'non-member-id'; + const updateMemberRoleDto = { role: 'new-role' }; + + const mockOrganisation = { id: orgId } as Organisation; + + jest.spyOn(organisationRepository, 'findOne').mockResolvedValue(mockOrganisation); + jest.spyOn(organisationUserRole, 'findOne').mockResolvedValue(null); + + await expect(service.updateMemberRole(orgId, memberId, updateMemberRoleDto)).rejects.toThrow(CustomHttpException); + }); + + it('should throw CustomHttpException if new role is not found', async () => { + const orgId = 'org-id'; + const memberId = 'member-id'; + const updateMemberRoleDto = { role: 'non-existent-role' }; + + const mockOrganisation = { id: orgId } as Organisation; + const mockOrgUserRole = { + userId: memberId, + organisationId: orgId, + user: { id: memberId } as User, + organisation: mockOrganisation, + role: { name: 'old-role' }, + } as OrganisationUserRole; - const result = await service.getOrganisationMembers('orgId', 2, 1, 'sub'); + jest.spyOn(organisationRepository, 'findOne').mockResolvedValue(mockOrganisation); + jest.spyOn(organisationUserRole, 'findOne').mockResolvedValue(mockOrgUserRole); + jest.spyOn(roleRepository, 'findOne').mockResolvedValue(null); - expect(result.status_code).toBe(200); - expect(result.data).toEqual([ - { id: 'anotherUserId', name: 'Jane Doe', email: 'jane@email.com', phone_number: '1111' }, - ]); + await expect(service.updateMemberRole(orgId, memberId, updateMemberRoleDto)).rejects.toThrow(CustomHttpException); }); }); }); diff --git a/src/modules/permissions/dto/create-permission.dto.ts b/src/modules/permissions/dto/create-permission.dto.ts new file mode 100644 index 000000000..6b0324f27 --- /dev/null +++ b/src/modules/permissions/dto/create-permission.dto.ts @@ -0,0 +1,6 @@ +import { IsString } from 'class-validator'; + +export class CreatePermissionDto { + @IsString() + title: string; +} diff --git a/src/modules/organisation-permissions/dto/permission-list.dto.ts b/src/modules/permissions/dto/permission-list.dto.ts similarity index 53% rename from src/modules/organisation-permissions/dto/permission-list.dto.ts rename to src/modules/permissions/dto/permission-list.dto.ts index b641e51c6..12772f31f 100644 --- a/src/modules/organisation-permissions/dto/permission-list.dto.ts +++ b/src/modules/permissions/dto/permission-list.dto.ts @@ -3,31 +3,31 @@ import { ApiProperty } from '@nestjs/swagger'; import { PermissionCategory } from '../helpers/PermissionCategory'; export class PermissionListDto { - @ApiProperty({ enum: PermissionCategory, description: 'Permission category' }) + @ApiProperty({ type: Boolean, description: 'Permission category' }) @IsBoolean() [PermissionCategory.CanViewTransactions]?: boolean; - @ApiProperty({ enum: PermissionCategory, description: 'Permission category' }) + @ApiProperty({ type: Boolean, description: 'Permission category' }) @IsBoolean() [PermissionCategory.CanViewRefunds]?: boolean; - @ApiProperty({ enum: PermissionCategory, description: 'Permission category' }) + @ApiProperty({ type: Boolean, description: 'Permission category' }) @IsBoolean() [PermissionCategory.CanLogRefunds]?: boolean; - @ApiProperty({ enum: PermissionCategory, description: 'Permission category' }) + @ApiProperty({ type: Boolean, description: 'Permission category' }) @IsBoolean() [PermissionCategory.CanViewUsers]?: boolean; - @ApiProperty({ enum: PermissionCategory, description: 'Permission category' }) + @ApiProperty({ type: Boolean, description: 'Permission category' }) @IsBoolean() [PermissionCategory.CanCreateUsers]?: boolean; - @ApiProperty({ enum: PermissionCategory, description: 'Permission category' }) + @ApiProperty({ type: Boolean, description: 'Permission category' }) @IsBoolean() [PermissionCategory.CanEditUsers]?: boolean; - @ApiProperty({ enum: PermissionCategory, description: 'Permission category' }) + @ApiProperty({ type: Boolean, description: 'Permission category' }) @IsBoolean() [PermissionCategory.CanBlacklistWhitelistUsers]?: boolean; } diff --git a/src/modules/permissions/dto/update-permission.dto.ts b/src/modules/permissions/dto/update-permission.dto.ts new file mode 100644 index 000000000..a8fee984e --- /dev/null +++ b/src/modules/permissions/dto/update-permission.dto.ts @@ -0,0 +1,16 @@ +import { IsOptional, IsObject, ValidateNested, IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsPermissionListValid } from '../helpers/custom-validator'; +import { Type } from 'class-transformer'; +import { PermissionListDto } from './permission-list.dto'; + +export class UpdatePermissionDto { + @ApiProperty({ + description: 'The title of the permission to be updated', + }) + @IsString() + title: string; +} + +type UpdatePermissionOption = { id: string; permission: UpdatePermissionDto }; +export default UpdatePermissionOption; diff --git a/src/modules/organisation-permissions/entities/default-permissions.entity.ts b/src/modules/permissions/entities/default-permissions.entity.ts similarity index 71% rename from src/modules/organisation-permissions/entities/default-permissions.entity.ts rename to src/modules/permissions/entities/default-permissions.entity.ts index dc197b931..29e45626f 100644 --- a/src/modules/organisation-permissions/entities/default-permissions.entity.ts +++ b/src/modules/permissions/entities/default-permissions.entity.ts @@ -1,7 +1,7 @@ import { Entity, Column, ManyToOne } from 'typeorm'; import { AbstractBaseEntity } from '../../../entities/base.entity'; import { PermissionCategory } from '../helpers/PermissionCategory'; -import { OrganisationRole } from '../../organisation-role/entities/organisation-role.entity'; +import { Role } from '../../role/entities/role.entity'; @Entity() export class DefaultPermissions extends AbstractBaseEntity { @@ -15,6 +15,6 @@ export class DefaultPermissions extends AbstractBaseEntity { @Column({ type: 'boolean', nullable: false }) permission_list: boolean; - @ManyToOne(() => OrganisationRole, role => role.organisation) - role: OrganisationRole; + @ManyToOne(() => Role) + role: Role; } diff --git a/src/modules/permissions/entities/permissions.entity.ts b/src/modules/permissions/entities/permissions.entity.ts new file mode 100644 index 000000000..d87cd34c4 --- /dev/null +++ b/src/modules/permissions/entities/permissions.entity.ts @@ -0,0 +1,11 @@ +import { Entity, Column, ManyToMany } from 'typeorm'; +import { AbstractBaseEntity } from '../../../entities/base.entity'; +import { Role } from '../../../modules/role/entities/role.entity'; + +@Entity() +export class Permissions extends AbstractBaseEntity { + @Column() + title: string; + @ManyToMany(() => Role, role => role.permissions) + roles: Role[]; +} diff --git a/src/modules/organisation-permissions/helpers/PermissionCategory.ts b/src/modules/permissions/helpers/PermissionCategory.ts similarity index 100% rename from src/modules/organisation-permissions/helpers/PermissionCategory.ts rename to src/modules/permissions/helpers/PermissionCategory.ts diff --git a/src/modules/organisation-permissions/helpers/custom-validator.ts b/src/modules/permissions/helpers/custom-validator.ts similarity index 100% rename from src/modules/organisation-permissions/helpers/custom-validator.ts rename to src/modules/permissions/helpers/custom-validator.ts diff --git a/src/modules/organisation-permissions/mocks/organisation-permissions.mock.ts b/src/modules/permissions/mocks/organisation-permissions.mock.ts similarity index 82% rename from src/modules/organisation-permissions/mocks/organisation-permissions.mock.ts rename to src/modules/permissions/mocks/organisation-permissions.mock.ts index c28ae635f..0a3f26269 100644 --- a/src/modules/organisation-permissions/mocks/organisation-permissions.mock.ts +++ b/src/modules/permissions/mocks/organisation-permissions.mock.ts @@ -1,6 +1,6 @@ import { UpdatePermissionDto } from '../dto/update-permission.dto'; -export const mockUpdatePermissionDto: UpdatePermissionDto = { +export const mockUpdatePermissionDto = { permission_list: { canViewTransactions: true, canViewRefunds: true, diff --git a/src/modules/organisation-permissions/mocks/organisation.mock.ts b/src/modules/permissions/mocks/organisation.mock.ts similarity index 97% rename from src/modules/organisation-permissions/mocks/organisation.mock.ts rename to src/modules/permissions/mocks/organisation.mock.ts index c12a7a9c5..8dc2377de 100644 --- a/src/modules/organisation-permissions/mocks/organisation.mock.ts +++ b/src/modules/permissions/mocks/organisation.mock.ts @@ -18,4 +18,5 @@ export const mockOrganisation = { ], }, ], -} as Organisation; +}; +// as Organisation; diff --git a/src/modules/organisation-permissions/mocks/role.mock.ts b/src/modules/permissions/mocks/role.mock.ts similarity index 83% rename from src/modules/organisation-permissions/mocks/role.mock.ts rename to src/modules/permissions/mocks/role.mock.ts index 3f8bfa641..e9dacb417 100644 --- a/src/modules/organisation-permissions/mocks/role.mock.ts +++ b/src/modules/permissions/mocks/role.mock.ts @@ -1,5 +1,3 @@ -import { OrganisationRole } from '../../organisation-role/entities/organisation-role.entity'; - export const mockRole = { id: 'role_456', name: 'Admin', @@ -12,4 +10,5 @@ export const mockRole = { { id: 'perm_6', category: 'canEditUsers', permission_list: true }, { id: 'perm_7', category: 'canBlacklistWhitelistUsers', permission_list: true }, ], -} as OrganisationRole; +}; +// as OrganisationRole; diff --git a/src/modules/permissions/permissions.controller.ts b/src/modules/permissions/permissions.controller.ts new file mode 100644 index 000000000..4d1e95e68 --- /dev/null +++ b/src/modules/permissions/permissions.controller.ts @@ -0,0 +1,60 @@ +import { + Body, + Controller, + Get, + HttpException, + HttpStatus, + InternalServerErrorException, + NotFoundException, + Param, + Patch, + Post, + UseGuards, +} from '@nestjs/common'; +import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { OwnershipGuard } from '../../guards/authorization.guard'; +import { UpdatePermissionDto } from './dto/update-permission.dto'; +import { OrganisationPermissionsService } from './permissions.service'; +import { CreatePermissionDto } from './dto/create-permission.dto'; + +@ApiBearerAuth() +@ApiTags('Permissions') +@Controller('permissions') +export class OrganisationPermissionsController { + constructor(private readonly permissionService: OrganisationPermissionsService) {} + + @ApiOperation({ summary: 'Create Permission' }) + @ApiResponse({ + status: 200, + description: 'Create a new existing permission', + type: CreatePermissionDto, + }) + // @UseGuards(OwnershipGuard) + @Post('') + async createPermission(@Body() createPermissionDto: CreatePermissionDto) { + return await this.permissionService.createPermission(createPermissionDto.title); + } + + @ApiOperation({ summary: 'Update Permission' }) + @ApiResponse({ + status: 200, + description: 'Update an existing permission', + type: CreatePermissionDto, + }) + @Patch('/:permission_id') + async updatePermission(@Body() updatePermissionDto: UpdatePermissionDto, @Param('permission_id') id: string) { + return await this.permissionService.updatePermission({ id, permission: updatePermissionDto }); + } + + @ApiOperation({ summary: 'Fetch all Permission' }) + @Get('') + async getAllPermissions() { + return await this.permissionService.getAllPermissions(); + } + + @ApiOperation({ summary: 'Fetch a single Permission' }) + @Get('/:permission_id') + async getSinglePermission(@Param('permission_id') id: string) { + return await this.permissionService.getSinglePermission(id); + } +} diff --git a/src/modules/organisation-permissions/organisation-permissions.module.ts b/src/modules/permissions/permissions.module.ts similarity index 50% rename from src/modules/organisation-permissions/organisation-permissions.module.ts rename to src/modules/permissions/permissions.module.ts index de405cbb6..33af12d8a 100644 --- a/src/modules/organisation-permissions/organisation-permissions.module.ts +++ b/src/modules/permissions/permissions.module.ts @@ -1,14 +1,15 @@ import { Module } from '@nestjs/common'; -import { OrganisationPermissionsService } from './organisation-permissions.service'; -import { OrganisationPermissionsController } from './organisation-permissions.controller'; +import { OrganisationPermissionsService } from './permissions.service'; +import { OrganisationPermissionsController } from './permissions.controller'; import { Organisation } from '../organisations/entities/organisations.entity'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { Permissions } from '../organisation-permissions/entities/permissions.entity'; -import { OrganisationRole } from '../organisation-role/entities/organisation-role.entity'; +import { Permissions } from './entities/permissions.entity'; +import { Role } from '../role/entities/role.entity'; +import { OrganisationUserRole } from '../role/entities/organisation-user-role.entity'; @Module({ providers: [OrganisationPermissionsService], controllers: [OrganisationPermissionsController], - imports: [TypeOrmModule.forFeature([Organisation, OrganisationRole, Permissions])], + imports: [TypeOrmModule.forFeature([Organisation, OrganisationUserRole, Permissions, Role])], }) export class OrganisationPermissionsModule {} diff --git a/src/modules/permissions/permissions.service.ts b/src/modules/permissions/permissions.service.ts new file mode 100644 index 000000000..c282f3a6a --- /dev/null +++ b/src/modules/permissions/permissions.service.ts @@ -0,0 +1,74 @@ +import { HttpStatus, Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Permissions } from './entities/permissions.entity'; +import { CustomHttpException } from '../../helpers/custom-http-filter'; +import { RESOURCE_NOT_FOUND } from '../../helpers/SystemMessages'; +import UpdatePermissionOption from './dto/update-permission.dto'; +import { title } from 'process'; + +@Injectable() +export class OrganisationPermissionsService { + constructor( + @InjectRepository(Permissions) + private readonly permissionRepository: Repository + ) {} + + public async createPermission(title: string): Promise { + const permission = new Permissions(); + permission.title = title; + return await this.permissionRepository.save(permission); + } + + public async getPermissions(): Promise { + return await this.permissionRepository.find(); + } + + public async getAllPermissions() { + const permissions = await this.getPermissions(); + return { + status_code: HttpStatus.OK, + message: 'Permissions fetched successfully', + data: permissions, + }; + } + + public async getSinglePermission(identifier: string) { + const permission = await this.getPermissionById(identifier); + if (!permission) { + throw new CustomHttpException(RESOURCE_NOT_FOUND('Permission'), HttpStatus.NOT_FOUND); + } + + return { + status_code: HttpStatus.OK, + message: 'Permission fetched successfully', + data: permission, + }; + } + + public async updatePermission(payload: UpdatePermissionOption) { + const permission = await this.permissionRepository.findOne({ where: { id: payload.id } }); + + if (!permission) { + throw new CustomHttpException(RESOURCE_NOT_FOUND('Permission'), HttpStatus.NOT_FOUND); + } + permission.title = payload.permission.title; + const updatedPermission = await this.permissionRepository.save(permission); + return { + status_code: HttpStatus.OK, + message: 'Permission updated successfully', + data: { + id: updatedPermission.id, + title: updatedPermission.title, + }, + }; + } + + public async getPermissionById(id: string): Promise { + return await this.permissionRepository.findOne({ where: { id } }); + } + + public async getPermissionByTitle(identifier: string): Promise { + return await this.permissionRepository.findOne({ where: { title: identifier } }); + } +} diff --git a/src/modules/permissions/tests/permissions.service.spec.ts b/src/modules/permissions/tests/permissions.service.spec.ts new file mode 100644 index 000000000..3d1f2058c --- /dev/null +++ b/src/modules/permissions/tests/permissions.service.spec.ts @@ -0,0 +1,180 @@ +import { HttpStatus, InternalServerErrorException, NotFoundException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { OrganisationPermissionsService } from '../permissions.service'; + +import { Organisation } from '../../organisations/entities/organisations.entity'; +import { Permissions } from '../entities/permissions.entity'; +import { mockUpdatePermissionDto } from '../mocks/organisation-permissions.mock'; +import { mockOrganisation } from '../mocks/organisation.mock'; +import { mockRole } from '../mocks/role.mock'; +import { Role } from '../../../modules/role/entities/role.entity'; +import { CustomHttpException } from '../../../helpers/custom-http-filter'; +import { RESOURCE_NOT_FOUND } from '../../../helpers/SystemMessages'; +describe('OrganisationPermissionsService', () => { + let service: OrganisationPermissionsService; + let permissionRepository: Repository; + let roleRepository: Repository; + let organisationRepository: Repository; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + OrganisationPermissionsService, + { + provide: getRepositoryToken(Permissions), + useValue: { + update: jest.fn(), + }, + }, + { + provide: getRepositoryToken(Role), + useValue: { + findOne: jest.fn(), + }, + }, + { + provide: getRepositoryToken(Organisation), + useValue: { + findOne: jest.fn(), + }, + }, + { + provide: getRepositoryToken(Permissions), + useClass: Repository, + }, + ], + }).compile(); + + service = module.get(OrganisationPermissionsService); + permissionRepository = module.get>(getRepositoryToken(Permissions)); + roleRepository = module.get>(getRepositoryToken(Role)); + organisationRepository = module.get>(getRepositoryToken(Organisation)); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('createPermission', () => { + it('should create and return a new permission', async () => { + const title = 'Permission 1'; + const permission = { id: '1', title } as Permissions; + + jest.spyOn(permissionRepository, 'save').mockResolvedValueOnce(permission); + + const result = await service.createPermission(title); + + expect(result).toEqual(permission); + }); + }); + + describe('getPermissions', () => { + it('should return all permissions', async () => { + const permissions = [{ id: '1', title: 'Permission 1' }] as Permissions[]; + + jest.spyOn(permissionRepository, 'find').mockResolvedValueOnce(permissions); + + const result = await service.getPermissions(); + + expect(result).toEqual(permissions); + }); + }); + + describe('getAllPermissions', () => { + it('should return a success response with all permissions', async () => { + const permissions = [{ id: '1', title: 'Permission 1' }] as Permissions[]; + + jest.spyOn(service, 'getPermissions').mockResolvedValueOnce(permissions); + + const result = await service.getAllPermissions(); + + expect(result).toEqual({ + status_code: HttpStatus.OK, + message: 'Permissions fetched successfully', + data: permissions, + }); + }); + }); + + describe('getSinglePermission', () => { + it('should throw not found exception if permission does not exist', async () => { + jest.spyOn(service, 'getPermissionById').mockResolvedValueOnce(null); + + await expect(service.getSinglePermission('1')).rejects.toThrow( + new CustomHttpException(RESOURCE_NOT_FOUND('Permission'), HttpStatus.NOT_FOUND) + ); + }); + + it('should return a success response with the permission', async () => { + const permission = { id: '1', title: 'Permission 1' } as Permissions; + + jest.spyOn(service, 'getPermissionById').mockResolvedValueOnce(permission); + + const result = await service.getSinglePermission('1'); + + expect(result).toEqual({ + status_code: HttpStatus.OK, + message: 'Permission fetched successfully', + data: permission, + }); + }); + }); + + describe('updatePermission', () => { + it('should throw not found exception if permission does not exist', async () => { + const payload = { id: '1', permission: { title: 'Updated Permission' } }; + + jest.spyOn(permissionRepository, 'findOne').mockResolvedValueOnce(null); + + await expect(service.updatePermission(payload)).rejects.toThrow( + new CustomHttpException(RESOURCE_NOT_FOUND('Permission'), HttpStatus.NOT_FOUND) + ); + }); + + it('should update and return the permission', async () => { + const payload = { id: '1', permission: { title: 'Updated Permission' } }; + const permission = { id: '1', title: 'Permission 1' } as Permissions; + const updatedPermission = { ...permission, title: payload.permission.title }; + + jest.spyOn(permissionRepository, 'findOne').mockResolvedValueOnce(permission); + jest.spyOn(permissionRepository, 'save').mockResolvedValueOnce(updatedPermission); + + const result = await service.updatePermission(payload); + + expect(result).toEqual({ + status_code: HttpStatus.OK, + message: 'Permission updated successfully', + data: { + id: updatedPermission.id, + title: updatedPermission.title, + }, + }); + }); + }); + + describe('getPermissionById', () => { + it('should return the permission with the given id', async () => { + const permission = { id: '1', title: 'Permission 1' } as Permissions; + + jest.spyOn(permissionRepository, 'findOne').mockResolvedValueOnce(permission); + + const result = await service.getPermissionById('1'); + + expect(result).toEqual(permission); + }); + }); + + describe('getPermissionByTitle', () => { + it('should return the permission with the given title', async () => { + const permission = { id: '1', title: 'Permission 1' } as Permissions; + + jest.spyOn(permissionRepository, 'findOne').mockResolvedValueOnce(permission); + + const result = await service.getPermissionByTitle('Permission 1'); + + expect(result).toEqual(permission); + }); + }); +}); diff --git a/src/modules/product-category/entities/product-category.entity.ts b/src/modules/product-category/entities/product-category.entity.ts index 029274010..033975221 100644 --- a/src/modules/product-category/entities/product-category.entity.ts +++ b/src/modules/product-category/entities/product-category.entity.ts @@ -12,8 +12,4 @@ export class ProductCategory extends AbstractBaseEntity { @ApiProperty() @Column({ type: 'text', nullable: true }) description: string; - - /* To be implemented in another pr */ - // @OneToMany(() => Product, product => product.category) - // products: Product[]; } diff --git a/src/modules/products/dto/create-product.dto.ts b/src/modules/products/dto/create-product.dto.ts index 6b627be52..3f638c8c2 100644 --- a/src/modules/products/dto/create-product.dto.ts +++ b/src/modules/products/dto/create-product.dto.ts @@ -1,5 +1,6 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsNumber, IsOptional, IsString, Min, MinLength } from 'class-validator'; +import { IsEnum, IsNumber, IsOptional, IsString, Min, MinLength } from 'class-validator'; +import { ProductSizeType } from '../entities/product.entity'; export class CreateProductRequestDto { @ApiProperty({ @@ -11,13 +12,41 @@ export class CreateProductRequestDto { @MinLength(3) name: string; + @ApiProperty({ + description: 'The description of the product', + minimum: 0, + example: 10, + }) + @IsString() + @IsOptional() + description?: string; + + @ApiProperty({ + description: 'The size of the product', + minimum: 0, + example: 10, + }) + @IsEnum(ProductSizeType) + @IsString() + @IsOptional() + size?: string; + + @ApiProperty({ + description: 'The image of the product', + minimum: 0, + example: 10, + }) + @IsString() + @IsOptional() + image_url?: string; + @ApiProperty({ description: 'The quantity of the product', minimum: 0, example: 10, }) @IsNumber() - @Min(0) + @IsOptional() quantity: number; @ApiProperty({ @@ -25,7 +54,7 @@ export class CreateProductRequestDto { minimum: 0, example: 99.99, }) - @IsNumber() + @IsNumber({ maxDecimalPlaces: 2 }) @Min(0) price: number; diff --git a/src/modules/products/dto/delete-product.dto.ts b/src/modules/products/dto/delete-product.dto.ts new file mode 100644 index 000000000..3c7776afd --- /dev/null +++ b/src/modules/products/dto/delete-product.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsNumber, IsOptional, IsString, IsUUID, Min, MinLength } from 'class-validator'; + +export class DeleteProductDTO { + @ApiPropertyOptional({ + description: 'User id of current user', + example: 'some-uuid', + }) + @IsUUID() + productId?: string; + + @ApiPropertyOptional({ + description: 'Organisation id of current user', + example: 'some-uuid', + }) + @IsUUID() + organisationId?: string; +} diff --git a/src/modules/products/dto/get-total-products.dto.ts b/src/modules/products/dto/get-total-products.dto.ts new file mode 100644 index 000000000..1cf60dc26 --- /dev/null +++ b/src/modules/products/dto/get-total-products.dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; + +class TotalProductsData { + @ApiProperty({ example: 20 }) + total_products: number; + + @ApiProperty({ example: '+100.00% from last month' }) + percentage_change: string; +} + +export class GetTotalProductsResponseDto { + @ApiProperty() + status: string; + @ApiProperty() + status_code: number; + @ApiProperty() + message: string; + @ApiProperty({ type: TotalProductsData }) + data: TotalProductsData; +} diff --git a/src/modules/products/dto/update-product.dto.ts b/src/modules/products/dto/update-product.dto.ts index 103b45475..6790bb69a 100644 --- a/src/modules/products/dto/update-product.dto.ts +++ b/src/modules/products/dto/update-product.dto.ts @@ -28,7 +28,7 @@ export class UpdateProductDTO { example: 99.99, }) @IsOptional() - @IsNumber() + @IsNumber({ maxDecimalPlaces: 2 }) @Min(0) price: number; diff --git a/src/modules/products/entities/product.entity.ts b/src/modules/products/entities/product.entity.ts index c081144a9..945aca9a6 100644 --- a/src/modules/products/entities/product.entity.ts +++ b/src/modules/products/entities/product.entity.ts @@ -1,6 +1,9 @@ import { AbstractBaseEntity } from '../../../entities/base.entity'; -import { Column, Entity, ManyToOne } from 'typeorm'; +import { Column, DeleteDateColumn, Entity, ManyToOne, OneToMany } from 'typeorm'; +import { Comment } from '../../../modules/comments/entities/comments.entity'; import { Organisation } from '../../../modules/organisations/entities/organisations.entity'; +import { Cart } from '../../dashboard/entities/cart.entity'; +import { OrderItem } from '../../dashboard/entities/order-items.entity'; export enum StockStatusType { IN_STOCK = 'in stock', @@ -28,15 +31,15 @@ export class Product extends AbstractBaseEntity { @Column({ type: 'text', nullable: true }) image: string; - @Column({ type: 'int', nullable: false, default: 0 }) + @Column({ type: 'float', nullable: false, default: 0 }) price: number; + @Column({ type: 'float', nullable: false, default: 0 }) + cost_price: number; + @Column({ type: 'int', nullable: false, default: 0 }) quantity: number; - @Column({ nullable: true, default: false }) - is_deleted: boolean; - @Column({ type: 'enum', enum: ProductSizeType, @@ -53,4 +56,16 @@ export class Product extends AbstractBaseEntity { @ManyToOne(() => Organisation, org => org.products) org: Organisation; + + @DeleteDateColumn() + deletedAt?: Date; + + @OneToMany(() => Comment, comment => comment.product) + comments?: Comment[]; + + @OneToMany(() => OrderItem, orderItem => orderItem.product) + orderItems: OrderItem[]; + + @OneToMany(() => Cart, cart => cart.product) + cart: Cart[]; } diff --git a/src/modules/products/products.controller.ts b/src/modules/products/products.controller.ts index f5b3993a2..7bbcbe895 100644 --- a/src/modules/products/products.controller.ts +++ b/src/modules/products/products.controller.ts @@ -1,60 +1,122 @@ -import { Body, Controller, Delete, Get, HttpCode, Param, Patch, Post, UseGuards, Query } from '@nestjs/common'; -import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { + Body, + Controller, + Delete, + Get, + HttpCode, + Param, + Patch, + Post, + UseGuards, + Query, + HttpStatus, + Req, +} from '@nestjs/common'; +import { ApiBearerAuth, ApiBody, ApiOkResponse, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; import { OwnershipGuard } from '../../guards/authorization.guard'; import { CreateProductRequestDto } from './dto/create-product.dto'; import { ProductsService } from './products.service'; import { UpdateProductDTO } from './dto/update-product.dto'; +import { isUUID } from 'class-validator'; +import { CustomHttpException } from '../../helpers/custom-http-filter'; +import { INVALID_ORG_ID, INVALID_PRODUCT_ID } from '../../helpers/SystemMessages'; +import { AddCommentDto } from '../comments/dto/add-comment.dto'; +import { GetTotalProductsResponseDto } from './dto/get-total-products.dto'; +import { SuperAdminGuard } from '../../guards/super-admin.guard'; +import { skipAuth } from '../../helpers/skipAuth'; @ApiTags('Products') -@Controller('/organizations/:id/products') +@Controller('') export class ProductsController { constructor(private readonly productsService: ProductsService) {} + @skipAuth() + @Get('/products') + @ApiOperation({ summary: 'Fetch all products' }) + @ApiResponse({ status: 201, description: 'Products fetched successfully' }) + @ApiResponse({ status: 400, description: 'Bad request' }) + @ApiResponse({ status: 500, description: 'Internal server error' }) + async getAllProducts(@Query('page') page: number, @Query('page_size') pageSize: number) { + return await this.productsService.getAllProducts({ page, pageSize }); + } + @UseGuards(OwnershipGuard) - @Post('') @ApiBearerAuth() + @Get(':org_id/products') + @ApiOperation({ summary: 'Fetch all products' }) + @ApiResponse({ status: 201, description: 'Products fetched successfully' }) + @ApiResponse({ status: 400, description: 'Bad request' }) + @ApiResponse({ status: 500, description: 'Internal server error' }) + async getAllOrganisationProducts(@Param('org_id') organisationId: string) { + return await this.productsService.getAllOrganisationProducts(organisationId); + } + + @skipAuth() + @Get('/products/:product_id') + @ApiOperation({ summary: 'fetches a single product' }) + @ApiResponse({ status: 201, description: 'Product fetched successfully' }) + @ApiResponse({ status: 400, description: 'Bad request' }) + @ApiResponse({ status: 500, description: 'Internal server error' }) + async fetchSingleProduct(@Param('product_id') productId: string) { + return await this.productsService.getSingleProduct(productId); + } + + @ApiBearerAuth() + @UseGuards(SuperAdminGuard) + @Get('organisations/products/total') + @ApiOkResponse({ type: GetTotalProductsResponseDto, description: 'Total Products fetched successfully' }) + @ApiResponse({ status: 500, description: 'Internal server error' }) + async getTotalProducts() { + return await this.productsService.getTotalProducts(); + } + + @ApiBearerAuth() + @UseGuards(OwnershipGuard) + @Post('organisations/:orgId/products') @ApiOperation({ summary: 'Creates a new product' }) - @ApiParam({ name: 'id', description: 'organisation ID', example: '12345' }) + @ApiParam({ name: 'orgId', description: 'organisation ID', example: '12345' }) @ApiBody({ type: CreateProductRequestDto, description: 'Details of the product to be created' }) @ApiResponse({ status: 201, description: 'Product created successfully' }) @ApiResponse({ status: 400, description: 'Bad request' }) @ApiResponse({ status: 500, description: 'Internal server error' }) - async createProduct(@Param('id') id: string, @Body() createProductDto: CreateProductRequestDto) { - return this.productsService.createProduct(id, createProductDto); + async createProduct(@Param('orgId') orgId: string, @Body() createProductDto: CreateProductRequestDto) { + return this.productsService.createProduct(orgId, createProductDto); } - @Get('search') + @ApiBearerAuth() + @Get('organisations/:orgId/products/search') @ApiOperation({ summary: 'Search for products' }) - @ApiParam({ name: 'id', description: 'organisation ID', example: '12345' }) + @ApiParam({ name: 'orgId', description: 'organisation ID', example: '12345' }) @ApiResponse({ status: 200, description: 'Products found successfully' }) @ApiResponse({ status: 204, description: 'No products found' }) @ApiResponse({ status: 400, description: 'Bad request' }) @ApiResponse({ status: 500, description: 'Internal server error' }) async searchProducts( - @Param('id') id: string, + @Param('orgId') orgId: string, @Query('name') name?: string, @Query('category') category?: string, @Query('minPrice') minPrice?: number, @Query('maxPrice') maxPrice?: number ) { - return this.productsService.searchProducts(id, { name, category, minPrice, maxPrice }); + return this.productsService.searchProducts(orgId, { name, category, minPrice, maxPrice }); } + @ApiBearerAuth() @UseGuards(OwnershipGuard) - @Get(':id') + @Get('organisations/:orgId/products/:productId') @ApiOperation({ summary: 'Gets a product by id' }) - @ApiParam({ name: 'id', description: 'Organization ID', example: '12345' }) - @ApiBody({ type: CreateProductRequestDto, description: 'Details of the product to be created' }) + @ApiParam({ name: 'orgId', description: 'Organization ID', example: '12345' }) @ApiResponse({ status: 200, description: 'Product created successfully' }) @ApiResponse({ status: 400, description: 'Bad request' }) @ApiResponse({ status: 404, description: 'Product not found' }) @ApiResponse({ status: 500, description: 'Internal server error' }) - async getById(@Param('orgId') id: string, @Param('id') productId: string) { + async getById(@Param('orgId') id: string, @Param('productId') productId: string) { return this.productsService.getProductById(productId); } + @ApiBearerAuth() @UseGuards(OwnershipGuard) - @Patch('/:productId') + @Patch('organisations/:orgId/products/:productId') @HttpCode(200) @ApiOperation({ summary: 'Update product' }) @ApiParam({ name: 'productId', type: String, description: 'Product ID' }) @@ -63,22 +125,59 @@ export class ProductsController { @ApiResponse({ status: 404, description: 'Product not found' }) @ApiResponse({ status: 500, description: 'Internal server error' }) async updateProduct( - @Param('id') id: string, + @Param('orgId') orgId: string, @Param('productId') productId: string, @Body() updateProductDto: UpdateProductDTO ) { - return this.productsService.updateProduct(id, productId, updateProductDto); + return this.productsService.updateProduct(orgId, productId, updateProductDto); } + @ApiBearerAuth() @UseGuards(OwnershipGuard) - @Delete(':productId') + @Delete('organisations/:orgId/products/:productId') @ApiOperation({ summary: 'Delete a product' }) @ApiParam({ name: 'productId', description: 'Product ID' }) @ApiResponse({ status: 200, description: 'Product deleted successfully' }) @ApiResponse({ status: 400, description: 'Bad request' }) @ApiResponse({ status: 404, description: 'Product not found' }) @ApiResponse({ status: 500, description: 'Internal server error' }) - async deleteProduct(@Param('id') id: string, @Param('productId') productId: string) { - return this.productsService.deleteProduct(id, productId); + async deleteProduct(@Param('orgId') orgId: string, @Param('productId') productId: string) { + if (!isUUID(orgId)) { + throw new CustomHttpException(INVALID_ORG_ID, HttpStatus.BAD_REQUEST); + } + if (!isUUID(productId)) { + throw new CustomHttpException(INVALID_PRODUCT_ID, HttpStatus.BAD_REQUEST); + } + return this.productsService.deleteProduct(orgId, productId); + } + + @ApiBearerAuth() + @UseGuards(OwnershipGuard) + @Post('organisations/:productId/comments') + @ApiBearerAuth() + @ApiOperation({ summary: 'Creates a comment for a product' }) + @ApiParam({ name: 'id', description: 'organisation ID', example: '870ccb14-d6b0-4a50-b459-9895af803i89' }) + @ApiParam({ name: 'productId', description: 'product ID', example: '126ccb14-d6b0-4a50-b459-9895af803h6y' }) + @ApiBody({ type: AddCommentDto, description: 'Comment to be added' }) + @ApiResponse({ status: 201, description: 'Comment added successfully' }) + @ApiResponse({ status: 400, description: 'Bad request' }) + @ApiResponse({ status: 404, description: 'Not found' }) + @ApiResponse({ status: 500, description: 'Internal server error' }) + async addCommentToProduct(@Param('productId') productId: string, @Body() commentDto: AddCommentDto, @Req() req: any) { + const user = req.user; + return this.productsService.addCommentToProduct(productId, commentDto, user.sub); + } + + @ApiBearerAuth() + @UseGuards(OwnershipGuard) + @Get('organisations/:productId/stock') + @ApiOperation({ summary: 'Gets a product stock details by id' }) + @ApiParam({ name: 'id', description: 'Organization ID', example: '12345' }) + @ApiParam({ name: 'productId', description: 'Product ID' }) + @ApiResponse({ status: 200, description: 'Product stock retrieved successfully' }) + @ApiResponse({ status: 404, description: 'Product not found' }) + @ApiResponse({ status: 500, description: 'Internal server error' }) + async getProductStock(@Param('productId') productId: string) { + return this.productsService.getProductStock(productId); } } diff --git a/src/modules/products/products.module.ts b/src/modules/products/products.module.ts index b038c4fb2..441eca43c 100644 --- a/src/modules/products/products.module.ts +++ b/src/modules/products/products.module.ts @@ -1,14 +1,36 @@ import { Module } from '@nestjs/common'; -import { ProductsService } from './products.service'; -import { ProductsController } from './products.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { Product } from './entities/product.entity'; +import { Comment } from '../comments/entities/comments.entity'; +import { Cart } from '../dashboard/entities/cart.entity'; +import { OrderItem } from '../dashboard/entities/order-items.entity'; +import { Order } from '../dashboard/entities/order.entity'; import { Organisation } from '../organisations/entities/organisations.entity'; +import { User } from '../user/entities/user.entity'; +import { UserModule } from '../user/user.module'; import { ProductVariant } from './entities/product-variant.entity'; +import { OrganisationUserRole } from '../role/entities/organisation-user-role.entity'; +import { Role } from '../role/entities/role.entity'; +import { Product } from './entities/product.entity'; +import { ProductsController } from './products.controller'; +import { ProductsService } from './products.service'; @Module({ + imports: [ + TypeOrmModule.forFeature([ + Product, + Organisation, + ProductVariant, + User, + OrganisationUserRole, + Role, + Comment, + Order, + OrderItem, + Cart, + ]), + UserModule, + ], controllers: [ProductsController], providers: [ProductsService], - imports: [TypeOrmModule.forFeature([Product, Organisation, ProductVariant])], }) export class ProductsModule {} diff --git a/src/modules/products/products.service.ts b/src/modules/products/products.service.ts index 5f2575805..380791dc0 100644 --- a/src/modules/products/products.service.ts +++ b/src/modules/products/products.service.ts @@ -1,19 +1,24 @@ import { - BadRequestException, ForbiddenException, HttpStatus, Injectable, InternalServerErrorException, - NotFoundException, Logger, + NotFoundException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { endOfMonth, startOfMonth, subMonths } from 'date-fns'; import { Repository } from 'typeorm'; -import { Product, StockStatusType } from './entities/product.entity'; +import { CustomHttpException } from '../../helpers/custom-http-filter'; +import * as SYS_MSG from '../../helpers/SystemMessages'; +import { AddCommentDto } from '../comments/dto/add-comment.dto'; +import { Comment } from '../comments/entities/comments.entity'; import { Organisation } from '../organisations/entities/organisations.entity'; +import { User } from '../user/entities/user.entity'; import { CreateProductRequestDto } from './dto/create-product.dto'; import { UpdateProductDTO } from './dto/update-product.dto'; import { ProductVariant } from './entities/product-variant.entity'; +import { Product, ProductSizeType, StockStatusType } from './entities/product.entity'; interface SearchCriteria { name?: string; @@ -27,7 +32,9 @@ export class ProductsService { private readonly logger = new Logger(ProductsService.name); constructor( @InjectRepository(Product) private productRepository: Repository, - @InjectRepository(Organisation) private organisationRepository: Repository + @InjectRepository(Organisation) private organisationRepository: Repository, + @InjectRepository(Comment) private commentRepository: Repository, + @InjectRepository(User) private userRepository: Repository ) {} async createProduct(id: string, dto: CreateProductRequestDto) { @@ -38,10 +45,21 @@ export class ProductsService { message: 'Invalid organisation credentials', status_code: 422, }); - const newProduct: Product = this.productRepository.create(dto); + const payload = { + name: dto.name, + quantity: dto.quantity, + price: dto.price, + category: dto.category, + description: dto.description, + image: dto.image_url, + size: dto.size as ProductSizeType, + }; + + const newProduct: Product = this.productRepository.create(payload); newProduct.org = org; const statusCal = await this.calculateProductStatus(dto.quantity); newProduct.stock_status = statusCal; + newProduct.cost_price = 0.2 * dto.price - dto.price; const product = await this.productRepository.save(newProduct); if (!product || !newProduct) throw new InternalServerErrorException({ @@ -59,7 +77,6 @@ export class ProductsService { description: product.description, price: product.price, status: product.stock_status, - is_deleted: product.is_deleted, quantity: product.quantity, created_at: product.created_at, updated_at: product.updated_at, @@ -67,6 +84,49 @@ export class ProductsService { }; } + async getAllProducts({ page = 1, pageSize = 2 }: { page: number; pageSize: number }) { + const skip = (page - 1) * pageSize; + const allProucts = await this.productRepository.find({ skip, take: pageSize }); + const totalProducts = await this.productRepository.count(); + + return { + status_code: HttpStatus.OK, + message: 'Product retrieved successfully', + data: { + products: allProucts, + total: totalProducts, + page, + pageSize, + }, + }; + } + + async getAllOrganisationProducts(organisationId: string) { + const organisation = await this.organisationRepository.findOne({ + where: { id: organisationId }, + relations: ['products'], + }); + if (!organisation) { + throw new CustomHttpException('Invalid Organisation', HttpStatus.BAD_REQUEST); + } + + const allProucts = organisation.products; + + return { + status_code: HttpStatus.OK, + message: 'Products retrieved successfully', + data: allProucts, + }; + } + + async getSingleProduct(productId: string) { + const product = await this.productRepository.findOne({ where: { id: productId } }); + if (!product) { + throw new CustomHttpException(SYS_MSG.RESOURCE_NOT_FOUND, HttpStatus.NOT_FOUND); + } + return { status_code: HttpStatus.OK, message: 'Product fetched successfully', data: product }; + } + async searchProducts(orgId: string, criteria: SearchCriteria) { const org = await this.organisationRepository.findOne({ where: { id: orgId } }); if (!org) @@ -127,6 +187,7 @@ export class ProductsService { status_code: HttpStatus.NOT_FOUND, }); } + if (product.org.id !== org.id) { throw new ForbiddenException({ status: 'fail', @@ -137,6 +198,7 @@ export class ProductsService { try { await this.productRepository.update(productId, { ...updateProductDto, + cost_price: 0.2 * updateProductDto.price - updateProductDto.price, stock_status: await this.calculateProductStatus(updateProductDto.quantity), }); @@ -173,31 +235,110 @@ export class ProductsService { async deleteProduct(orgId: string, productId: string) { const org = await this.organisationRepository.findOne({ where: { id: orgId } }); if (!org) { - throw new InternalServerErrorException({ - status: 'Unprocessable entity exception', - message: 'Invalid organisation credentials', - status_code: 422, - }); + throw new CustomHttpException(SYS_MSG.ORG_NOT_FOUND, HttpStatus.NOT_FOUND); } - try { - const product = await this.productRepository.findOne({ where: { id: productId }, relations: ['org'] }); - if (product.org.id !== org.id) { - throw new ForbiddenException({ - status: 'fail', - message: 'Not allowed to perform this action', - }); - } - product.is_deleted = true; - await this.productRepository.save(product); - } catch (error) { - this.logger.log(error); - throw new InternalServerErrorException(`Internal error occurred: ${error.message}`); + const product = await this.productRepository.findOne({ where: { id: productId }, relations: ['org'] }); + if (!product) { + throw new CustomHttpException(SYS_MSG.PRODUCT_NOT_FOUND, HttpStatus.NOT_FOUND); } - + await this.productRepository.softDelete(product.id); return { message: 'Product successfully deleted', data: {}, }; } + + async addCommentToProduct(productId: string, commentDto: AddCommentDto, userId: string) { + const { comment } = commentDto; + const product = await this.productRepository.findOne({ where: { id: productId } }); + const user = await this.userRepository.findOne({ where: { id: userId } }); + + if (!product) { + throw new CustomHttpException(SYS_MSG.PRODUCT_NOT_FOUND, HttpStatus.NOT_FOUND); + } + + const productComment = this.commentRepository.create({ comment, product, user }); + + const saveComment = await this.commentRepository.save(productComment); + + const responsePayload = { + id: saveComment.id, + product_id: product.id, + comment: saveComment.comment, + user_id: userId, + created_at: saveComment.created_at, + }; + + return { + message: SYS_MSG.COMMENT_CREATED, + data: responsePayload, + }; + } + + async getProductStock(productId: string) { + const product = await this.productRepository.findOne({ where: { id: productId } }); + if (!product) { + throw new NotFoundException(`Product not found`); + } + return { + message: 'Product stock retrieved successfully', + data: { + product_id: product.id, + current_stock: product.quantity, + last_updated: product.updated_at, + }, + }; + } + + private getDateRange(date: Date) { + const monthStarts = startOfMonth(new Date(date)); + const monthEnds = endOfMonth(new Date(date)); + + return { monthStarts, monthEnds }; + } + + private async getTotalProductsForDateRange(startOfMonth: Date, endOfMonth: Date) { + const result = await this.productRepository + .createQueryBuilder('product') + .select('SUM(product.quantity)', 'total') + .where('product.created_at BETWEEN :startOfMonth AND :endOfMonth', { startOfMonth, endOfMonth }) + .getRawOne(); + + return result && result.total ? parseInt(result.total, 10) : 0; + } + + async getTotalProducts() { + const todaysDate = new Date(); + const lastMonth = subMonths(todaysDate, 1); + + const monthStarts = this.getDateRange(todaysDate).monthStarts; + const monthEnds = this.getDateRange(todaysDate).monthEnds; + const lastMonthStarts = this.getDateRange(lastMonth).monthStarts; + const lastMonthEnds = this.getDateRange(lastMonth).monthEnds; + + const totalProductsThisMonth = await this.getTotalProductsForDateRange(monthStarts, monthEnds); + + const totalProductsLastMonth = await this.getTotalProductsForDateRange(lastMonthStarts, lastMonthEnds); + + let percentageChange; + + if (totalProductsLastMonth === totalProductsThisMonth) { + percentageChange = + totalProductsLastMonth === 0 ? 'No products to compare from last month' : 'No change from last month'; + } else if (totalProductsLastMonth === 0 && totalProductsThisMonth > 0) { + percentageChange = `+100.00% from last month`; + } else { + const change = ((totalProductsThisMonth - totalProductsLastMonth) / totalProductsLastMonth) * 100; + percentageChange = `${change > 0 ? '+' : ''}${change.toFixed(2)}% from last month`; + } + + return { + message: SYS_MSG.TOTAL_PRODUCTS_FETCHED_SUCCESSFULLY, + data: { + total_products: totalProductsThisMonth, + percentage_change: percentageChange, + }, + }; + } } diff --git a/src/modules/products/products.superadmin.controller.ts b/src/modules/products/products.superadmin.controller.ts new file mode 100644 index 000000000..16e205b50 --- /dev/null +++ b/src/modules/products/products.superadmin.controller.ts @@ -0,0 +1,26 @@ +import { Body, Controller, Delete, Get, HttpCode, Param, Patch, Post, UseGuards, Query } from '@nestjs/common'; +import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { OwnershipGuard } from '../../guards/authorization.guard'; +import { CreateProductRequestDto } from './dto/create-product.dto'; +import { ProductsService } from './products.service'; +import { UpdateProductDTO } from './dto/update-product.dto'; +import { SuperAdminGuard } from '../../guards/super-admin.guard'; + +@ApiTags('add-Product-superAdmin') +@Controller('admin/products/:id') +export class ProductsController { + constructor(private readonly productsService: ProductsService) {} + + @UseGuards(SuperAdminGuard) + @Post('') + @ApiBearerAuth() + @ApiOperation({ summary: 'Creates a new product by super admin' }) + @ApiParam({ name: 'id', description: 'organisation ID', example: '12345' }) + @ApiBody({ type: CreateProductRequestDto, description: 'Details of the product to be created' }) + @ApiResponse({ status: 201, description: 'Product created successfully' }) + @ApiResponse({ status: 400, description: 'Bad request' }) + @ApiResponse({ status: 500, description: 'Internal server error' }) + async createProductBySuperAdmin(@Param('id') id: string, @Body() createProductDto: CreateProductRequestDto) { + return this.productsService.createProduct(id, createProductDto); + } +} diff --git a/src/modules/products/tests/mocks/comment.mock.ts b/src/modules/products/tests/mocks/comment.mock.ts new file mode 100644 index 000000000..53006b1b6 --- /dev/null +++ b/src/modules/products/tests/mocks/comment.mock.ts @@ -0,0 +1,14 @@ +import { mockUser } from '../../../user/tests/mocks/user.mock'; +import { Comment } from '../../../comments/entities/comments.entity'; +import { productMock } from './product.mock'; + +export const mockComment: Comment = { + id: 'Comment1', + comment: 'First comment', + model_id: 'Product1', + model_type: 'Product', + user: mockUser, + product: productMock, + created_at: new Date(), + updated_at: new Date(), +}; diff --git a/src/modules/products/tests/mocks/deleted-poruct.mock.ts b/src/modules/products/tests/mocks/deleted-product.mock.ts similarity index 79% rename from src/modules/products/tests/mocks/deleted-poruct.mock.ts rename to src/modules/products/tests/mocks/deleted-product.mock.ts index 09acc4934..5781f036f 100644 --- a/src/modules/products/tests/mocks/deleted-poruct.mock.ts +++ b/src/modules/products/tests/mocks/deleted-product.mock.ts @@ -1,5 +1,5 @@ import { randomUUID } from 'crypto'; -import { orgMock } from '../../../../modules/organisations/tests/mocks/organisation.mock'; +import { orgMock } from '../../../organisations/tests/mocks/organisation.mock'; import { Product, StockStatusType } from '../../entities/product.entity'; enum ProductSizeType { @@ -12,12 +12,14 @@ export const deletedProductMock: Product = { id: randomUUID(), name: 'TV', description: '', - is_deleted: true, stock_status: StockStatusType.LOW_STOCK, image: '', price: 12, category: 'Fashion', quantity: 7, + cost_price: 10, + orderItems: [], + cart: [], size: ProductSizeType.SMALL, org: orgMock, created_at: new Date(), diff --git a/src/modules/products/tests/mocks/product.mock.ts b/src/modules/products/tests/mocks/product.mock.ts index 91f99ab1d..c6b26ac6c 100644 --- a/src/modules/products/tests/mocks/product.mock.ts +++ b/src/modules/products/tests/mocks/product.mock.ts @@ -12,7 +12,6 @@ export const productMock: Product = { id: randomUUID(), name: 'TV', description: '', - is_deleted: false, stock_status: StockStatusType.LOW_STOCK, image: '', price: 12, @@ -22,4 +21,7 @@ export const productMock: Product = { org: orgMock, created_at: new Date(), updated_at: new Date(), + cost_price: 10, + cart: [], + orderItems: [], }; diff --git a/src/modules/products/tests/products.service.spec.ts b/src/modules/products/tests/products.service.spec.ts index 4b44d67db..00a64ac86 100644 --- a/src/modules/products/tests/products.service.spec.ts +++ b/src/modules/products/tests/products.service.spec.ts @@ -1,21 +1,29 @@ +import { InternalServerErrorException, NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { ProductsService } from '../products.service'; -import { Product, StockStatusType } from '../entities/product.entity'; +import { DeleteResult, Repository, UpdateResult } from 'typeorm'; +import { CustomHttpException } from '../../../helpers/custom-http-filter'; +import { Comment } from '../../../modules/comments/entities/comments.entity'; import { Organisation } from '../../../modules/organisations/entities/organisations.entity'; -import { ProductVariant } from '../entities/product-variant.entity'; -import { NotFoundException, InternalServerErrorException } from '@nestjs/common'; import { orgMock } from '../../../modules/organisations/tests/mocks/organisation.mock'; +import { User } from '../../../modules/user/entities/user.entity'; +import { mockUser } from '../../../modules/user/tests/mocks/user.mock'; +import { AddCommentDto } from '../../comments/dto/add-comment.dto'; +import { UpdateProductDTO } from '../dto/update-product.dto'; +import { ProductVariant } from '../entities/product-variant.entity'; +import { Product } from '../entities/product.entity'; +import { ProductsService } from '../products.service'; +import { mockComment } from './mocks/comment.mock'; +import { deletedProductMock } from './mocks/deleted-product.mock'; import { createProductRequestDtoMock } from './mocks/product-request-dto.mock'; import { productMock } from './mocks/product.mock'; -import { UpdateProductDTO } from '../dto/update-product.dto'; -import { deletedProductMock } from './mocks/deleted-poruct.mock'; describe('ProductsService', () => { let service: ProductsService; let productRepository: Repository; let organisationRepository: Repository; + let userRepository: Repository; + let commentRepository: Repository; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -29,6 +37,7 @@ describe('ProductsService', () => { save: jest.fn(), create: jest.fn(), update: jest.fn(), + softDelete: jest.fn(), }, }, { @@ -41,12 +50,26 @@ describe('ProductsService', () => { provide: getRepositoryToken(ProductVariant), useClass: Repository, }, + { + provide: getRepositoryToken(Comment), + useValue: { + createQueryBuilder: jest.fn(), + create: jest.fn(), + save: jest.fn(), + }, + }, + { + provide: getRepositoryToken(User), + useClass: Repository, + }, ], }).compile(); service = module.get(ProductsService); productRepository = module.get>(getRepositoryToken(Product)); organisationRepository = module.get>(getRepositoryToken(Organisation)); + userRepository = module.get>(getRepositoryToken(User)); + commentRepository = module.get>(getRepositoryToken(Comment)); }); it('should create a new product', async () => { @@ -165,14 +188,153 @@ describe('ProductsService', () => { describe('deleteProduct', () => { it('should delete the product successfully', async () => { + const deleteResult: UpdateResult = { raw: [], affected: 1, generatedMaps: [] }; jest.spyOn(organisationRepository, 'findOne').mockResolvedValue(orgMock); jest.spyOn(productRepository, 'findOne').mockResolvedValue(productMock); - jest.spyOn(productRepository, 'save').mockResolvedValue(deletedProductMock); + jest.spyOn(productRepository, 'softDelete').mockResolvedValue(deleteResult); const result = await service.deleteProduct(orgMock.id, productMock.id); expect(result.message).toEqual('Product successfully deleted'); - expect(deletedProductMock.is_deleted).toBe(true); + }); + }); + + describe('Add a comment under a product', () => { + it('should add a comment successfully', async () => { + const addCommentDto: AddCommentDto = { + comment: 'New Comment', + }; + + jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser); + jest.spyOn(productRepository, 'findOne').mockResolvedValue(productMock); + jest.spyOn(commentRepository, 'create').mockReturnValue(mockComment); + jest.spyOn(commentRepository, 'save').mockResolvedValue(mockComment); + + const result = await service.addCommentToProduct(productMock.id, addCommentDto, mockUser.id); + + expect(result.message).toEqual('Comment added successfully'); + expect(result.data).toBeDefined(); + }); + + it('should throw an error', async () => { + const addCommentDto: AddCommentDto = { + comment: 'New Comment', + }; + + jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser); + jest.spyOn(productRepository, 'findOne').mockResolvedValue(null); + jest.spyOn(commentRepository, 'create').mockReturnValue(mockComment); + jest.spyOn(commentRepository, 'save').mockResolvedValue(mockComment); + + await expect(service.addCommentToProduct(productMock.id, addCommentDto, mockUser.id)).rejects.toThrow( + CustomHttpException + ); + }); + }); + + describe('getProductStock', () => { + it('should return product stock details if the product is found', async () => { + const productId = '123'; + const productMock = { + id: productId, + quantity: 20, + updated_at: new Date(), + }; + + jest.spyOn(productRepository, 'findOne').mockResolvedValue(productMock as any); + + const result = await service.getProductStock(productId); + + expect(result).toEqual({ + message: 'Product stock retrieved successfully', + data: { + product_id: productMock.id, + current_stock: productMock.quantity, + last_updated: productMock.updated_at, + }, + }); + }); + + it('should throw NotFoundException if the product is not found', async () => { + const productId = 'nonexistent'; + + jest.spyOn(productRepository, 'findOne').mockResolvedValue(null); + + await expect(service.getProductStock(productId)).rejects.toThrow(new NotFoundException('Product not found')); + }); + }); + + describe('Get total products', () => { + it('should return the total number of products and the equivalent percentage change', async () => { + const mockMonthData = { total: '20' }; + const mockLastMonthData = { total: '10' }; + + const queryBuilderMock = { + select: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + getRawOne: jest.fn().mockResolvedValueOnce(mockMonthData).mockResolvedValueOnce(mockLastMonthData), + }; + + jest.spyOn(productRepository, 'createQueryBuilder').mockReturnValue(queryBuilderMock as any); + + const result = await service.getTotalProducts(); + + expect(result).toEqual({ + message: 'Total Products fetched successfully', + data: { + total_products: 20, + percentage_change: '+100.00% from last month', + }, + }); + }); + + it('should return 100% change when there were no products last month', async () => { + const mockMonthData = { total: '20' }; + const mockLastMonthData = { total: '0' }; + + const queryBuilderMock = { + select: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + getRawOne: jest.fn().mockResolvedValueOnce(mockMonthData).mockResolvedValueOnce(mockLastMonthData), + }; + + jest.spyOn(productRepository, 'createQueryBuilder').mockReturnValue(queryBuilderMock as any); + + const result = await service.getTotalProducts(); + + expect(result).toEqual({ + message: 'Total Products fetched successfully', + data: { + total_products: 20, + percentage_change: '+100.00% from last month', + }, + }); + }); + + it('should return negative percentage change if fewer products this month', async () => { + const mockMonthData = { total: '5' }; + const mockLastMonthData = { total: '10' }; + + const queryBuilderMock = { + select: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + getRawOne: jest.fn().mockResolvedValueOnce(mockMonthData).mockResolvedValueOnce(mockLastMonthData), + }; + + jest.spyOn(productRepository, 'createQueryBuilder').mockReturnValue(queryBuilderMock as any); + + const result = await service.getTotalProducts(); + + expect(result).toEqual({ + message: 'Total Products fetched successfully', + data: { + total_products: 5, + percentage_change: '-50.00% from last month', + }, + }); }); }); }); diff --git a/src/modules/profile/dto/file.validator.ts b/src/modules/profile/dto/file.validator.ts new file mode 100644 index 000000000..e5ad7a5e3 --- /dev/null +++ b/src/modules/profile/dto/file.validator.ts @@ -0,0 +1,49 @@ +import { PipeTransform, Injectable, ArgumentMetadata, HttpStatus } from '@nestjs/common'; +import { CustomHttpException } from '../../../helpers/custom-http-filter'; +import * as fileType from 'file-type-mime'; +import { FILE_EXCEEDS_SIZE, INVALID_FILE_TYPE } from '../../../helpers/SystemMessages'; + + +export interface CustomUploadTypeValidatorOptions { + fileType: string[]; + } + +@Injectable() +export class FileValidator implements PipeTransform { + constructor(private readonly options: { maxSize: number; mimeTypes: string[] }) { + + } + + async transform(value: Express.Multer.File, metadata: ArgumentMetadata) { + if (!value) { + throw new CustomHttpException('No file provided', HttpStatus.BAD_REQUEST); + } + + this.validateFileSize(value.size) + await this.validateFileType(value.buffer) + + return value; + } + + private validateFileSize(size: number) { + if (size > this.options.maxSize) { + throw new CustomHttpException( + FILE_EXCEEDS_SIZE(this.options.maxSize / (1024 * 1024)), + HttpStatus.BAD_REQUEST + ); + } + } + + + private async validateFileType(buffer: Buffer) { + const response = await fileType.parse(buffer); + if (!response || !this.options.mimeTypes.includes(response.mime)) { + throw new CustomHttpException( + INVALID_FILE_TYPE(this.options.mimeTypes.join(', ')), + HttpStatus.BAD_REQUEST + ); + } + } + + +} diff --git a/src/modules/profile/dto/upload-profile-pic.dto.ts b/src/modules/profile/dto/upload-profile-pic.dto.ts new file mode 100644 index 000000000..5e46214bc --- /dev/null +++ b/src/modules/profile/dto/upload-profile-pic.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { HasMimeType, MaxFileSize } from 'nestjs-form-data'; + +export class UploadProfilePicDto { + @ApiProperty({ + type: 'string', + format: 'binary', + description: 'Profile picture file', + maxLength: 2 * 1024 * 1024, + }) + @HasMimeType(['image/jpeg', 'image/png']) + @MaxFileSize(2 * 1024 * 1024) + avatar: Express.Multer.File; +} diff --git a/src/modules/profile/entities/profile.entity.ts b/src/modules/profile/entities/profile.entity.ts index 7712c8c32..b77fead41 100644 --- a/src/modules/profile/entities/profile.entity.ts +++ b/src/modules/profile/entities/profile.entity.ts @@ -1,4 +1,4 @@ -import { Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm'; +import { Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn, DeleteDateColumn } from 'typeorm'; import { User } from '../../user/entities/user.entity'; import { AbstractBaseEntity } from '../../../entities/base.entity'; @@ -36,4 +36,8 @@ export class Profile extends AbstractBaseEntity { @Column({ nullable: true }) profile_pic_url: string; + + @DeleteDateColumn() + deletedAt?: Date; + } diff --git a/src/modules/profile/mocks/mockUser.ts b/src/modules/profile/mocks/mockUser.ts new file mode 100644 index 000000000..1e1b291c6 --- /dev/null +++ b/src/modules/profile/mocks/mockUser.ts @@ -0,0 +1,32 @@ +import { orgMock } from '../../../modules/organisations/tests/mocks/organisation.mock'; +import { Profile } from '../../../modules/profile/entities/profile.entity'; +import { User, UserType } from '../../user/entities/user.entity'; + +const profile = new Profile(); +export const mockUserWithProfile: User = { + cart: null, + email: 'tester@example.com', + status: null, + first_name: 'John', + last_name: 'Doe', + is_active: true, + phone: '+1234567891', + id: 'some-uuid-value-here', + attempts_left: 2, + created_at: new Date(), + updated_at: new Date(), + backup_codes: [], + owned_organisations: [], + jobs: [], + hashPassword: () => null, + password: 'password123', + time_left: 5, + secret: 'secret', + is_2fa_enabled: true, + testimonials: [], + profile: null, + notification_settings: [], + notifications: [], + blogs: [], + organisations: null, +}; diff --git a/src/modules/profile/mocks/profileMock.ts b/src/modules/profile/mocks/profileMock.ts new file mode 100644 index 000000000..14eaf17bb --- /dev/null +++ b/src/modules/profile/mocks/profileMock.ts @@ -0,0 +1,19 @@ +import { Profile } from '../entities/profile.entity'; + +export const mockProfile: Profile = { + id: 'profile-id', + created_at: new Date(), + updated_at: new Date(), + username: 'testuser', + jobTitle: 'Software Engineer', + pronouns: 'he/him', + department: 'Engineering', + email: 'testuser@example.com', + bio: 'A passionate software engineer.', + social_links: ['https://twitter.com/testuser', 'https://github.com/testuser'], + language: 'English', + region: 'US', + timezones: 'PST', + profile_pic_url: 'http://example.com/uploads/test.jpg', + deletedAt: null, +}; diff --git a/src/modules/profile/profile.controller.ts b/src/modules/profile/profile.controller.ts index f3ed7089a..73c51c88b 100644 --- a/src/modules/profile/profile.controller.ts +++ b/src/modules/profile/profile.controller.ts @@ -1,11 +1,36 @@ -import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common'; +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + UseInterceptors, + ParseUUIDPipe, + UploadedFile, + Req, + ValidationPipe, + UsePipes, +} from '@nestjs/common'; import { ProfileService } from './profile.service'; -import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiBody, ApiConsumes, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { ResponseInterceptor } from '../../shared/inteceptors/response.interceptor'; +import { UploadProfilePicDto } from './dto/upload-profile-pic.dto'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { FileValidator } from './dto/file.validator'; import { UpdateProfileDto } from './dto/update-profile.dto'; +import { + BASE_URL, + MAX_PROFILE_PICTURE_SIZE, + VALID_UPLOADS_MIME_TYPES, +} from '../../helpers/app-constants'; + @ApiBearerAuth() @ApiTags('Profile') @Controller('profile') +@UseInterceptors(ResponseInterceptor) export class ProfileController { constructor(private readonly profileService: ProfileService) {} @@ -26,8 +51,51 @@ export class ProfileController { description: 'The updated record', }) @Patch(':userId') - updateProfile(@Param('userId') userId: string, @Body() body: UpdateProfileDto) { + updateProfile(@Param('userId', ParseUUIDPipe) userId: string, @Body() body: UpdateProfileDto) { const updatedProfile = this.profileService.updateProfile(userId, body); return updatedProfile; } + + @ApiOperation({ summary: 'Delete User Profile' }) + @ApiResponse({ + status: 200, + description: 'The deleted record', + }) + @Delete(':userId') + async deleteUserProfile(@Param('userId', ParseUUIDPipe) userId: string) { + return await this.profileService.deleteUserProfile(userId); + } + + @ApiOperation({ summary: 'Upload Profile Picture' }) + @ApiResponse({ + status: 201, + description: 'Profile picture uploaded successfully', + }) + @Post('upload-image') + @UseInterceptors(FileInterceptor('avatar')) + @ApiConsumes('multipart/form-data') + @ApiBody({ + type: UploadProfilePicDto, + description: 'Profile picture file', +}) + + @UsePipes(new ValidationPipe({ transform: true })) + async uploadProfilePicture( + @Req() req: any, + @UploadedFile( + new FileValidator({ + maxSize: MAX_PROFILE_PICTURE_SIZE, + mimeTypes: VALID_UPLOADS_MIME_TYPES, + }) + ) + file: Express.Multer.File + ): Promise<{ + status: string; + message: string + }> { + const userId = req.user.id; + const uploadProfilePicDto = new UploadProfilePicDto() + uploadProfilePicDto.avatar = file + return await this.profileService.uploadProfilePicture(userId, uploadProfilePicDto, BASE_URL) + } } diff --git a/src/modules/profile/profile.service.ts b/src/modules/profile/profile.service.ts index 16a59753a..ed21e670d 100644 --- a/src/modules/profile/profile.service.ts +++ b/src/modules/profile/profile.service.ts @@ -1,17 +1,38 @@ -import { profile } from 'console'; -import { BadRequestException, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'; +import { + BadRequestException, + HttpStatus, + Injectable, + InternalServerErrorException, + Logger, + NotFoundException, +} from '@nestjs/common'; import { Profile } from './entities/profile.entity'; import { Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; import { User } from '../user/entities/user.entity'; import { UpdateProfileDto } from './dto/update-profile.dto'; +import * as sharp from 'sharp'; +import * as fs from 'fs'; +import * as path from 'path'; +import { CustomHttpException } from '../../helpers/custom-http-filter'; +import * as SYS_MSG from '../../helpers/SystemMessages'; +import { UploadProfilePicDto } from './dto/upload-profile-pic.dto'; +import { PROFILE_PHOTO_UPLOADS } from '../../helpers/app-constants'; +import { pipeline, Readable } from 'stream'; @Injectable() export class ProfileService { + private uploadsDir: string; + constructor( @InjectRepository(Profile) private profileRepository: Repository, @InjectRepository(User) private userRepository: Repository - ) {} + ) { + this.uploadsDir = PROFILE_PHOTO_UPLOADS; + this.createUploadsDirectory().catch(error => { + console.error('Failed to create uploads directory:', error); + }); + } async findOneProfile(userId: string) { try { @@ -30,9 +51,11 @@ export class ProfileService { throw new NotFoundException('Profile not found'); } + const profileData = {...profile, avatar_url:profile.profile_pic_url} + const responseData = { message: 'Successfully fetched profile', - data: profile, + data:profileData, }; return responseData; @@ -73,4 +96,99 @@ export class ProfileService { throw new InternalServerErrorException(`Internal server error: ${error.message}`); } } + async deleteUserProfile(userId: string) { + const user = await this.userRepository.findOne({ where: { id: userId }, relations: ['profile'] }); + + if (!user || !user.profile) { + throw new NotFoundException('User profile not found'); + } + + const userProfile = await this.profileRepository.findOne({ + where: { id: user.profile.id }, + }); + + if (!userProfile) { + throw new NotFoundException('Profile not found'); + } + + await this.profileRepository.softDelete(userProfile.id); + + const responseData = { + message: 'Profile successfully deleted', + }; + + return responseData; + } + + async uploadProfilePicture( + userId: string, + uploadProfilePicDto: UploadProfilePicDto, + baseUrl: string + ): Promise<{ status: string; message: string; data: { avatar_url: string } }> { + if (!uploadProfilePicDto.avatar) { + throw new CustomHttpException(SYS_MSG.NO_FILE_FOUND, HttpStatus.BAD_REQUEST); + } + + const user = await this.userRepository.findOne({ where: { id: userId }, relations: ['profile'] }); + if (!user) { + throw new CustomHttpException(SYS_MSG.USER_NOT_FOUND, HttpStatus.NOT_FOUND); + } + + const profile = user.profile; + if (!profile) { + throw new CustomHttpException(SYS_MSG.PROFILE_NOT_FOUND, HttpStatus.NOT_FOUND); + } + + if (profile.profile_pic_url) { + const previousFilePath = path.join(this.uploadsDir, path.basename(profile.profile_pic_url)); + + try { + await fs.promises.access(previousFilePath); + await fs.promises.unlink(previousFilePath); + } catch (error) { + if (error.code === 'ENOENT') { + console.error(SYS_MSG.PROFILE_PIC_NOT_FOUND, previousFilePath); + } else { + throw new CustomHttpException(SYS_MSG.PROFILE_PIC_ERROR, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + } + + const fileExtension = path.extname(uploadProfilePicDto.avatar.originalname); + const fileName = `${userId}${fileExtension}`; + const filePath = path.join(this.uploadsDir, fileName); + + const fileStream = Readable.from(uploadProfilePicDto.avatar.buffer); + const writeStream = fs.createWriteStream(filePath); + + return new Promise((resolve, reject) => { + pipeline(fileStream, writeStream, async err => { + if (err) { + Logger.error(SYS_MSG.FILE_SAVE_ERROR, err.stack); + reject(new CustomHttpException(SYS_MSG.FILE_SAVE_ERROR, HttpStatus.INTERNAL_SERVER_ERROR)); + } else { + await sharp(uploadProfilePicDto.avatar.buffer).resize({ width: 200, height: 200 }).toFile(filePath); + + profile.profile_pic_url = `${baseUrl}/uploads/${fileName}`; + + await this.profileRepository.update(profile.id, profile); + const updatedProfile = await this.profileRepository.findOne({ where: { id: profile.id } }); + resolve({ + status: "success", + message: SYS_MSG.PICTURE_UPDATED, + data: { avatar_url: updatedProfile.profile_pic_url }, + }); + } + }); + }); + + } + + private async createUploadsDirectory() { + try { + await fs.promises.mkdir(this.uploadsDir, { recursive: true }); + } catch (error) { + console.error(SYS_MSG.ERROR_DIRECTORY, error); + } + } } diff --git a/src/modules/profile/tests/profile.service.spec.ts b/src/modules/profile/tests/profile.service.spec.ts index c33ee534d..7d0dfa0b3 100644 --- a/src/modules/profile/tests/profile.service.spec.ts +++ b/src/modules/profile/tests/profile.service.spec.ts @@ -1,12 +1,18 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ProfileService } from '../profile.service'; -import { Repository } from 'typeorm'; +import { Repository, UpdateResult } from 'typeorm'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Profile } from '../entities/profile.entity'; -import { NotFoundException, InternalServerErrorException } from '@nestjs/common'; +import { NotFoundException, InternalServerErrorException, HttpStatus } from '@nestjs/common'; import { User } from '../../user/entities/user.entity'; import { UpdateProfileDto } from '../dto/update-profile.dto'; - +import * as fs from 'fs'; +import * as sharp from 'sharp'; +import { CustomHttpException } from '../../../helpers/custom-http-filter'; +import { PICTURE_UPDATED } from '../../../helpers/SystemMessages'; +import { mockUser } from '../../../modules/invite/mocks/mockUser'; +import { mockUserWithProfile } from '../mocks/mockUser'; +jest.mock('sharp'); describe('ProfileService', () => { let service: ProfileService; let userRepository: Repository; @@ -18,11 +24,18 @@ describe('ProfileService', () => { ProfileService, { provide: getRepositoryToken(User), - useClass: Repository, + useValue: { + findOne: jest.fn(), + softDelete: jest.fn(), + }, }, { provide: getRepositoryToken(Profile), - useClass: Repository, + useValue: { + update: jest.fn(), + findOne: jest.fn(), + softDelete: jest.fn(), + }, }, ], }).compile(); @@ -123,4 +136,153 @@ describe('ProfileService', () => { await expect(service.updateProfile(userId, updateProfileDto)).rejects.toThrow(InternalServerErrorException); }); }); + + describe('deleteProfile', () => { + it('should throw NotFoundException if user is not found', async () => { + jest.spyOn(userRepository, 'findOne').mockResolvedValue(null); + + await expect(service.deleteUserProfile('nonexistentUserId')).rejects.toThrow(NotFoundException); + expect(userRepository.findOne).toHaveBeenCalledWith({ + where: { id: 'nonexistentUserId' }, + relations: ['profile'], + }); + }); + + it('should throw NotFoundException if user profile is not found', async () => { + const user = { id: 'existingUserId', profile: null } as any; + jest.spyOn(userRepository, 'findOne').mockResolvedValue(user); + + await expect(service.deleteUserProfile('existingUserId')).rejects.toThrow(NotFoundException); + expect(userRepository.findOne).toHaveBeenCalledWith({ where: { id: 'existingUserId' }, relations: ['profile'] }); + }); + + it('should throw NotFoundException if profile is not found in profileRepository', async () => { + const user = { id: 'existingUserId', profile: { id: 'profileId' } } as any; + jest.spyOn(userRepository, 'findOne').mockResolvedValue(user); + jest.spyOn(profileRepository, 'findOne').mockResolvedValue(null); + + await expect(service.deleteUserProfile('existingUserId')).rejects.toThrow(NotFoundException); + expect(userRepository.findOne).toHaveBeenCalledWith({ where: { id: 'existingUserId' }, relations: ['profile'] }); + expect(profileRepository.findOne).toHaveBeenCalledWith({ where: { id: 'profileId' } }); + }); + + it('should delete the profile successfully', async () => { + const user = { id: 'existingUserId', profile: { id: 'profileId' } } as any; + const profile = { id: 'profileId' } as any; + jest.spyOn(userRepository, 'findOne').mockResolvedValue(user); + jest.spyOn(profileRepository, 'findOne').mockResolvedValue(profile); + jest.spyOn(profileRepository, 'softDelete').mockResolvedValue(undefined); + + const result = await service.deleteUserProfile('existingUserId'); + + expect(result).toEqual({ message: 'Profile successfully deleted' }); + expect(userRepository.findOne).toHaveBeenCalledWith({ where: { id: 'existingUserId' }, relations: ['profile'] }); + expect(profileRepository.findOne).toHaveBeenCalledWith({ where: { id: 'profileId' } }); + expect(profileRepository.softDelete).toHaveBeenCalledWith('profileId'); + }); + }); + + describe('uploadProfilePicture', () => { + const userId = 'testUserId'; + const baseUrl = 'http://localhost:3000'; + const mockFile = { + buffer: Buffer.from('test'), + originalname: 'test.jpg', + }; + const mockUploadProfilePicDto = { avatar: mockFile as any }; + + it('should throw an exception if no file is provided', async () => { + await expect(service.uploadProfilePicture(userId, { avatar: null }, baseUrl)).rejects.toThrow( + CustomHttpException + ); + }); + + it('should throw an exception if user is not found', async () => { + jest.spyOn(userRepository, 'findOne').mockResolvedValue(null); + await expect(service.uploadProfilePicture(userId, mockUploadProfilePicDto, baseUrl)).rejects.toThrow( + CustomHttpException + ); + }); + + it('should throw an exception if profile is not found', async () => { + jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUserWithProfile); + (sharp as jest.MockedFunction).mockReturnValue({ + resize: jest.fn().mockReturnThis(), + toFile: jest.fn().mockResolvedValue(undefined), + } as any); + + await expect(service.uploadProfilePicture(userId, mockUploadProfilePicDto, baseUrl)).rejects.toThrow( + CustomHttpException + ); + }); + + it('should delete previous profile picture if it exists', async () => { + jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser); + + jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser); + jest.spyOn(profileRepository, 'update').mockResolvedValue(null); + jest.spyOn(profileRepository, 'findOne').mockResolvedValue(mockUser.profile); + + (sharp as jest.MockedFunction).mockReturnValue({ + resize: jest.fn().mockReturnThis(), + toFile: jest.fn().mockResolvedValue(undefined), + } as any); + + const mockUnlink = jest.spyOn(fs.promises, 'unlink').mockResolvedValue(undefined); + const mockAccess = jest.spyOn(fs.promises, 'access').mockResolvedValue(undefined); + + await service.uploadProfilePicture(userId, mockUploadProfilePicDto, baseUrl); + + expect(mockAccess).toHaveBeenCalled(); + expect(mockUnlink).toHaveBeenCalled(); + }); + + it('should handle non-existent previous profile picture', async () => { + const mockResult: UpdateResult = { + generatedMaps: [], + raw: [], + affected: 1, + }; + jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser); + jest.spyOn(profileRepository, 'update').mockResolvedValue(mockResult); + jest.spyOn(profileRepository, 'findOne').mockResolvedValue(mockUser.profile); + + (fs.promises.access as jest.Mock).mockRejectedValue({ code: 'ENOENT' }); + + (sharp as jest.MockedFunction).mockReturnValue({ + resize: jest.fn().mockReturnThis(), + toFile: jest.fn().mockResolvedValue(undefined), + } as any); + + await expect(service.uploadProfilePicture(userId, mockUploadProfilePicDto, baseUrl)).resolves.not.toThrow(); + }); + + it('should save new profile picture and update profile', async () => { + jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser); + + (sharp as jest.MockedFunction).mockReturnValue({ + resize: jest.fn().mockReturnThis(), + toFile: jest.fn().mockResolvedValue(undefined), + } as any); + + const mockResult: UpdateResult = { + generatedMaps: [], + raw: [], + affected: 1, + }; + + jest.spyOn(profileRepository, 'update').mockResolvedValue(mockResult); + jest.spyOn(profileRepository, 'findOne').mockResolvedValue(mockUser.profile); + + const result = await service.uploadProfilePicture(userId, mockUploadProfilePicDto, baseUrl); + + expect(result).toEqual({ + status: 'success', + message: PICTURE_UPDATED, + data: { avatar_url: `${baseUrl}/uploads/${userId}.jpg` }, + }); + expect(sharp).toHaveBeenCalled(); + expect(profileRepository.update).toHaveBeenCalled(); + }); + }); }); diff --git a/src/modules/organisation-role/dto/create-organisation-role.dto.ts b/src/modules/role/dto/create-organisation-role.dto.ts similarity index 100% rename from src/modules/organisation-role/dto/create-organisation-role.dto.ts rename to src/modules/role/dto/create-organisation-role.dto.ts diff --git a/src/modules/role/dto/create-role-with-permission.dto.ts b/src/modules/role/dto/create-role-with-permission.dto.ts new file mode 100644 index 000000000..fac4a7f14 --- /dev/null +++ b/src/modules/role/dto/create-role-with-permission.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { CreateOrganisationRoleDto } from './create-organisation-role.dto'; + +export class CreateRoleWithPermissionDto { + @ApiProperty({ description: 'The name of the role', maxLength: 50 }) + // @IsString() + // @IsNotEmpty() + // @MaxLength(50) + // name: string; + @ApiProperty({ description: 'The description of the role', maxLength: 200 }) + rolePayload: CreateOrganisationRoleDto; + permissions_ids: string[]; +} diff --git a/src/modules/role/dto/update-organisation-role.dto.ts b/src/modules/role/dto/update-organisation-role.dto.ts new file mode 100644 index 000000000..f38765a1e --- /dev/null +++ b/src/modules/role/dto/update-organisation-role.dto.ts @@ -0,0 +1,25 @@ +import { ApiProperty, PartialType } from '@nestjs/swagger'; +import { CreateOrganisationRoleDto } from './create-organisation-role.dto'; +import { IsString } from 'class-validator'; + +export class UpdateOrganisationRoleDto extends PartialType(CreateOrganisationRoleDto) {} + +export type AttachPermissionsDto = { + roleId: string; + permissions: string[]; +}; + +export class AttachPermissionsApiBody { + @ApiProperty({ + description: 'The role to be updated', + example: 'some-id', + }) + @IsString() + roleId: string; + + @ApiProperty({ + description: 'Array of permissions to be attached', + example: ['id', 's-id'], + }) + permissions: string[]; +} diff --git a/src/modules/role/entities/organisation-user-role.entity.ts b/src/modules/role/entities/organisation-user-role.entity.ts new file mode 100644 index 000000000..ac12f5e50 --- /dev/null +++ b/src/modules/role/entities/organisation-user-role.entity.ts @@ -0,0 +1,26 @@ +import { AbstractBaseEntity } from '../../../entities/base.entity'; +import { Column, Entity, ManyToOne, OneToMany } from 'typeorm'; +import { Role } from './role.entity'; +import { User } from '../../user/entities/user.entity'; +import { Organisation } from '../../organisations/entities/organisations.entity'; + +@Entity('organization_user_role') +export class OrganisationUserRole extends AbstractBaseEntity { + @Column() + userId: string; + + @Column() + roleId: string; + + @Column({ nullable: true }) + organisationId: string; + + @ManyToOne(() => Role) + role: Role; + + @ManyToOne(() => User) + user: User; + + @ManyToOne(() => Organisation) + organisation: Organisation; +} diff --git a/src/modules/role/entities/role.entity.ts b/src/modules/role/entities/role.entity.ts new file mode 100644 index 000000000..b182e70ad --- /dev/null +++ b/src/modules/role/entities/role.entity.ts @@ -0,0 +1,26 @@ +import { Entity, Column, OneToMany, ManyToMany, JoinTable } from 'typeorm'; +import { AbstractBaseEntity } from '../../../entities/base.entity'; +import { Permissions } from '../../permissions/entities/permissions.entity'; + +@Entity({ name: 'roles' }) +export class Role extends AbstractBaseEntity { + @Column() + name: string; + + @Column({ type: 'text', nullable: true }) + description: string; + + @ManyToMany(() => Permissions, permission => permission.roles, { cascade: true }) + @JoinTable({ + name: 'role_permissions', // This is the join table + joinColumn: { + name: 'role_id', + referencedColumnName: 'id', + }, + inverseJoinColumn: { + name: 'permission_id', + referencedColumnName: 'id', + }, + }) + permissions: Permissions[]; +} diff --git a/src/modules/role/role.controller.ts b/src/modules/role/role.controller.ts new file mode 100644 index 000000000..6d1f5d9ae --- /dev/null +++ b/src/modules/role/role.controller.ts @@ -0,0 +1,82 @@ +import { Body, Controller, Get, HttpCode, HttpStatus, Param, Patch, Post, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { CreateOrganisationRoleDto } from './dto/create-organisation-role.dto'; +import { RoleService } from './role.service'; +import { + AttachPermissionsApiBody, + AttachPermissionsDto, + UpdateOrganisationRoleDto, +} from './dto/update-organisation-role.dto'; +import { SuperAdminGuard } from '../../guards/super-admin.guard'; + +@ApiTags('organisation Settings') +@UseGuards(SuperAdminGuard) +@ApiBearerAuth() +@Controller('roles') +export class RoleController { + constructor(private readonly roleService: RoleService) {} + @Post('') + @HttpCode(HttpStatus.CREATED) + @ApiOperation({ summary: 'Create a new role in an organisation' }) + @ApiResponse({ status: 201, description: 'The role has been successfully created.' }) + @ApiResponse({ status: 400, description: 'Bad Request.' }) + @ApiResponse({ status: 401, description: 'Unauthorized.' }) + @ApiResponse({ status: 403, description: 'Forbidden.' }) + @ApiResponse({ status: 409, description: 'Conflict - Role with this name already exists.' }) + async createRole(@Body() createRoleDto: CreateOrganisationRoleDto) { + return await this.roleService.createRole(createRoleDto); + } + + @UseGuards(SuperAdminGuard) + @ApiBearerAuth() + @Get('/:roleId') + @ApiOperation({ summary: 'Fetch a single role ' }) + @ApiParam({ name: 'id', required: true, description: 'ID of the role' }) + @ApiResponse({ status: 200, description: 'The role has been successfully fetched.' }) + @ApiResponse({ status: 400, description: 'Bad Request - Invalid role ID format.' }) + @ApiResponse({ status: 401, description: 'Unauthorized.' }) + @ApiResponse({ status: 403, description: 'Forbidden.' }) + @ApiResponse({ status: 404, description: 'Not Found - Role does not exist.' }) + async findOne(@Param('roleId') roleId: string) { + return await this.roleService.findSingleRole(roleId); + } + + @UseGuards(SuperAdminGuard) + @ApiBearerAuth() + @Patch('/:roleId') + @ApiOperation({ summary: 'Update a role ' }) + @ApiResponse({ + status: 200, + description: 'The role has been successfully updated', + }) + @ApiResponse({ status: 400, description: 'Invalid role ID format or input data' }) + @ApiResponse({ status: 401, description: 'Unauthorized.' }) + @ApiResponse({ status: 403, description: 'Forbidden.' }) + @ApiResponse({ status: 404, description: 'Role not found' }) + @ApiResponse({ status: 404, description: 'Organisation not found' }) + async updateRole(updateRoleDto: UpdateOrganisationRoleDto, @Param('roleId') roleId: string) { + const data = await this.roleService.updateRole({ id: roleId, payload: updateRoleDto }); + + return { + status_code: 200, + data, + }; + } + + @UseGuards(SuperAdminGuard) + @ApiBearerAuth() + @Post('permissions') + @ApiOperation({ summary: 'Attach permissions to a role' }) + @ApiResponse({ + status: 200, + description: 'The role has been successfully updated', + }) + @ApiBody({ type: AttachPermissionsApiBody }) + @ApiResponse({ status: 400, description: 'Invalid role ID format or input data' }) + @ApiResponse({ status: 401, description: 'Unauthorized.' }) + @ApiResponse({ status: 403, description: 'Forbidden.' }) + @ApiResponse({ status: 404, description: 'Role not found' }) + async attachPermissions(@Body() attachRoletoPermissionsDto: AttachPermissionsDto) { + return await this.roleService.attachRoletoPermissions(attachRoletoPermissionsDto); + } +} diff --git a/src/modules/role/role.module.ts b/src/modules/role/role.module.ts new file mode 100644 index 000000000..d48eee65f --- /dev/null +++ b/src/modules/role/role.module.ts @@ -0,0 +1,19 @@ +import { Module } from '@nestjs/common'; +import { RoleService } from './role.service'; +import { RoleController } from './role.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { OrganisationUserRole } from './entities/organisation-user-role.entity'; +import { Organisation } from '../organisations/entities/organisations.entity'; +import { DefaultPermissions } from '../permissions/entities/default-permissions.entity'; +import { Permissions } from '../permissions/entities/permissions.entity'; +import { Role } from './entities/role.entity'; +import { User } from '../user/entities/user.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([OrganisationUserRole, Permissions, Organisation, DefaultPermissions, Role, User]), + ], + controllers: [RoleController], + providers: [RoleService], +}) +export class RoleModule {} diff --git a/src/modules/role/role.service.ts b/src/modules/role/role.service.ts new file mode 100644 index 000000000..0c27358ea --- /dev/null +++ b/src/modules/role/role.service.ts @@ -0,0 +1,178 @@ +import { + ConflictException, + HttpStatus, + Injectable, + InternalServerErrorException, + NotFoundException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { DefaultPermissions } from '../permissions/entities/default-permissions.entity'; +import { Permissions } from '../permissions/entities/permissions.entity'; +import { Organisation } from '../organisations/entities/organisations.entity'; +import { CreateOrganisationRoleDto } from './dto/create-organisation-role.dto'; +import { OrganisationUserRole } from './entities/organisation-user-role.entity'; +import { AttachPermissionsDto, UpdateOrganisationRoleDto } from './dto/update-organisation-role.dto'; +import { Role } from './entities/role.entity'; +import { CustomHttpException } from '../../helpers/custom-http-filter'; +import { CreateRoleWithPermissionDto } from './dto/create-role-with-permission.dto'; +import { RESOURCE_NOT_FOUND, ROLE_CREATED_SUCCESSFULLY, ROLE_FETCHED_SUCCESSFULLY } from '../../helpers/SystemMessages'; + +@Injectable() +export class RoleService { + constructor( + @InjectRepository(Role) + private rolesRepository: Repository, + + @InjectRepository(OrganisationUserRole) + private organisationUserRole: Repository, + @InjectRepository(Organisation) + private organisationRepository: Repository, + @InjectRepository(Permissions) + private permissionRepository: Repository, + @InjectRepository(DefaultPermissions) + private defaultPermissionsRepository: Repository + ) {} + + async createRole(createRoleOption: CreateOrganisationRoleDto) { + const existingRole = await this.rolesRepository.findOne({ where: { name: createRoleOption.name } }); + + if (existingRole) { + throw new CustomHttpException('A role with this name already exists in the organisation', HttpStatus.CONFLICT); + } + const newRole = new Role(); + Object.assign(newRole, createRoleOption); + + const role = await this.rolesRepository.save(newRole); + + return role; + } + + async attachRoletoPermissions(payload: AttachPermissionsDto) { + const roleExists = await this.rolesRepository.findOne({ where: { id: payload.roleId } }); + if (!roleExists) { + throw new CustomHttpException('Invalid Role', HttpStatus.BAD_REQUEST); + } + + return await this.updateRolePermissions({ roleId: payload.roleId, permissions: payload.permissions }); + } + + // async updateRoleWithPermissions({role, permissions}:{role: Role, permissions: string[]}) { + + // // const role = await this.rolesRepository.find(); + // const roleWithPermissions = await this.updateRolePermissions({ + // roleId: role.id, + // permissions: permissions_ids, + // }); + + // return { + // status_code: HttpStatus.CREATED, + // message: ROLE_CREATED_SUCCESSFULLY, + // data: { + // id: roleWithPermissions.id, + // name: roleWithPermissions.name, + // description: roleWithPermissions.description || '', + // permissions: roleWithPermissions.permissions.map(permission => permission.title), + // }, + // }; + // } + + async createRoleWithPermissions(createRoleDto: CreateRoleWithPermissionDto) { + const role = await this.createRole(createRoleDto.rolePayload); + const roleWithPermissions = await this.updateRolePermissions({ + roleId: role.id, + permissions: createRoleDto.permissions_ids, + }); + + return { + status_code: HttpStatus.CREATED, + message: ROLE_CREATED_SUCCESSFULLY, + data: { + id: roleWithPermissions.id, + name: roleWithPermissions.name, + description: roleWithPermissions.description || '', + permissions: roleWithPermissions.permissions.map(permission => permission.title), + }, + }; + } + + async getAllRolesInOrganisation(organisationId: string) { + const organisation = await this.organisationRepository.findOne({ + where: { id: organisationId }, + }); + if (!organisation) { + throw new NotFoundException('Organisation not found'); + } + + const query = (await this.organisationUserRole.find({ where: { organisationId: organisation.id } })).map( + organisationRole => organisationRole.roleId + ); + return query; + } + + public async getRoleById(id: string): Promise { + return await this.rolesRepository.findOne({ + where: { id }, + relations: ['permissions'], + }); + } + + async findSingleRole(id: string) { + const role = await this.getRoleById(id); + if (!role) { + throw new CustomHttpException(RESOURCE_NOT_FOUND('Role'), HttpStatus.NOT_FOUND); + } + return { + status_code: HttpStatus.OK, + message: ROLE_FETCHED_SUCCESSFULLY, + data: { + id: role.id, + name: role.name, + description: role.description, + permissions: role.permissions.map(permission => ({ + id: permission.id, + category: permission.title, + })), + }, + }; + } + + async updateRole(updateRoleOption: { id: string; payload: UpdateOrganisationRoleDto }) { + const role = await this.rolesRepository.findOne({ + where: { + id: updateRoleOption.id, + }, + }); + + if (!role) { + throw new CustomHttpException(RESOURCE_NOT_FOUND('Role'), HttpStatus.NOT_FOUND); + } + Object.assign(role, updateRoleOption.payload); + await this.rolesRepository.save(role); + return role; + } + + async updateRolePermissions({ roleId, permissions }: { roleId: string; permissions?: string[] }) { + const role = await this.rolesRepository.findOne({ + where: { id: roleId }, + relations: ['permissions'], + }); + + if (!role) { + throw new CustomHttpException(RESOURCE_NOT_FOUND('Role'), HttpStatus.NOT_FOUND); + } + + const newPermissions: Permissions[] = []; + for (const permission of permissions) { + const permissionInstance = await this.permissionRepository.findOne({ where: { id: permission } }); + if (permissionInstance) { + newPermissions.push(permissionInstance); + } + } + + role.permissions = newPermissions; + + await this.rolesRepository.save(role); + return role; + } +} diff --git a/src/modules/role/tests/role.service.spec.ts b/src/modules/role/tests/role.service.spec.ts new file mode 100644 index 000000000..3bfb2e51f --- /dev/null +++ b/src/modules/role/tests/role.service.spec.ts @@ -0,0 +1,231 @@ +import { ConflictException, HttpStatus, NotFoundException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { DefaultPermissions } from '../../permissions/entities/default-permissions.entity'; +import { Permissions } from '../../permissions/entities/permissions.entity'; +import { Organisation } from '../../organisations/entities/organisations.entity'; +import { Role } from '../entities/role.entity'; +import { RoleService } from '../role.service'; +import { UpdateOrganisationRoleDto } from '../dto/update-organisation-role.dto'; +import { OrganisationUserRole } from '../entities/organisation-user-role.entity'; +import { EXISTING_ROLE, ROLE_CREATED_SUCCESSFULLY, ROLE_FETCHED_SUCCESSFULLY } from '../../../helpers/SystemMessages'; +import { CustomHttpException } from '../../../helpers/custom-http-filter'; +import { CreateRoleWithPermissionDto } from '../dto/create-role-with-permission.dto'; +import { CreateOrganisationRoleDto } from '../dto/create-organisation-role.dto'; + +describe('RoleService', () => { + let service: RoleService; + let rolesRepository: Repository; + let organisationRepository: Repository; + let permissionRepository: Repository; + let defaultPermissionRepository: Repository; + let organisationUserRole: Repository; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + RoleService, + { + provide: getRepositoryToken(Role), + useClass: Repository, + }, + { + provide: getRepositoryToken(Organisation), + useClass: Repository, + }, + { + provide: getRepositoryToken(Permissions), + useClass: Repository, + }, + { + provide: getRepositoryToken(DefaultPermissions), + useClass: Repository, + }, + { + provide: getRepositoryToken(OrganisationUserRole), + useClass: Repository, + }, + ], + }).compile(); + + service = module.get(RoleService); + rolesRepository = module.get>(getRepositoryToken(Role)); + organisationRepository = module.get>(getRepositoryToken(Organisation)); + permissionRepository = module.get>(getRepositoryToken(Permissions)); + }); + + // describe('createRole', () => { + // it('should create a role successfully', async () => { + // const createRoleDto = { name: 'TestRole', description: 'Test Description' }; + // const organisationId = 'org123'; + // const mockOrganisation = { id: organisationId }; + // const mockDefaultPermissions = [{ id: 'perm1', category: 'category1', permission_list: true }]; + // const mockPermissions = { id: 'perm1', title: 'can_update' }; + // const mockSavedRole = { id: 'role123', ...createRoleDto }; + // const mockSavedRoleResult = { + // data: { ...mockSavedRole, permissions: [mockPermissions.title] }, + // status_code: 201, + // message: ROLE_CREATED_SUCCESSFULLY, + // }; + + // jest.spyOn(rolesRepository, 'findOne').mockResolvedValue(null); + // jest.spyOn(rolesRepository, 'save').mockResolvedValue(mockSavedRole as Role); + // jest.spyOn(rolesRepository, 'findOne').mockResolvedValue(mockSavedRole as Role); + // jest.spyOn(permissionRepository, 'findOne').mockResolvedValue(mockPermissions as Permissions); + // jest.spyOn(rolesRepository, 'save').mockResolvedValue(mockSavedRoleResult.data as unknown as Role); + + // const result = await service.createRoleWithPermissions({ + // permissions_ids: [mockPermissions.title], + // rolePayload: createRoleDto, + // }); + + // expect(result).toEqual(mockSavedRoleResult); + // expect(permissionRepository.save).toHaveBeenCalled(); + // expect(rolesRepository.save).toHaveBeenCalledWith( + // expect.objectContaining({ + // ...createRoleDto, + // }) + // ); + // }); + + // it('should throw ConflictException when role already exists', async () => { + // jest.spyOn(organisationRepository, 'findOne').mockResolvedValue({ id: 'org123' } as Organisation); + // jest.spyOn(rolesRepository, 'findOne').mockResolvedValue({ id: 'existing', name: 'mockRole' } as Role); + + // await expect(service.createRole({ name: 'mockRole' })).rejects.toThrow(CustomHttpException); + // }); + // }); + describe('createRole', () => { + it('should throw conflict exception if role already exists', async () => { + const createRoleOption: CreateOrganisationRoleDto = { name: 'admin' }; + + jest.spyOn(rolesRepository, 'findOne').mockResolvedValueOnce({ id: '1', name: 'admin' } as Role); + + await expect(service.createRole(createRoleOption)).rejects.toThrow( + new CustomHttpException(EXISTING_ROLE, HttpStatus.CONFLICT) + ); + }); + + it('should create and return a new role', async () => { + const createRoleOption: CreateOrganisationRoleDto = { name: 'admin' }; + + jest.spyOn(rolesRepository, 'findOne').mockResolvedValueOnce(null); + jest.spyOn(rolesRepository, 'save').mockResolvedValueOnce({ id: '1', name: 'admin' } as Role); + + const result = await service.createRole(createRoleOption); + + expect(result).toEqual({ id: '1', name: 'admin' }); + }); + }); + + describe('createRoleWithPermissions', () => { + it('should create role and update permissions', async () => { + const createRoleDto: CreateRoleWithPermissionDto = { + rolePayload: { name: 'admin' }, + permissions_ids: ['perm1', 'perm2'], + }; + + const createdRole = { id: '1', name: 'admin' } as Role; + + jest.spyOn(service, 'createRole').mockResolvedValueOnce(createdRole); + jest.spyOn(service, 'updateRolePermissions').mockResolvedValueOnce({ + ...createdRole, + permissions: [ + { id: 'perm1', title: 'Permission 1' }, + { id: 'perm2', title: 'Permission 2' }, + ] as Permissions[], + }); + + const result = await service.createRoleWithPermissions(createRoleDto); + + expect(result).toEqual({ + status_code: HttpStatus.CREATED, + message: ROLE_CREATED_SUCCESSFULLY, + data: { + id: '1', + name: 'admin', + description: '', + permissions: ['Permission 1', 'Permission 2'], + }, + }); + }); + }); + + describe('updateRolePermissions', () => { + it('should throw not found exception if role does not exist', async () => { + jest.spyOn(rolesRepository, 'findOne').mockResolvedValueOnce(null); + + await expect(service.updateRolePermissions({ roleId: '1', permissions: [] })).rejects.toThrow( + CustomHttpException + ); + }); + + it('should update and return role with new permissions', async () => { + const role = { id: '1', name: 'admin', permissions: [] } as Role; + const permissions = [{ id: 'perm1' } as Permissions]; + + jest.spyOn(rolesRepository, 'findOne').mockResolvedValueOnce(role); + jest.spyOn(permissionRepository, 'findOne').mockResolvedValueOnce(permissions[0]); + jest.spyOn(rolesRepository, 'save').mockResolvedValueOnce({ ...role, permissions }); + + const result = await service.updateRolePermissions({ roleId: '1', permissions: ['perm1'] }); + + expect(result).toEqual({ ...role, permissions }); + }); + }); + + describe('findSingleRole', () => { + it('should throw not found exception if role does not exist', async () => { + jest.spyOn(service, 'getRoleById').mockResolvedValueOnce(null); + + await expect(service.findSingleRole('1')).rejects.toThrow(CustomHttpException); + }); + + it('should return role data if role exists', async () => { + const role = { + id: '1', + name: 'admin', + description: 'Administrator role', + permissions: [{ id: 'perm1', title: 'Permission 1' }], + } as Role; + + jest.spyOn(service, 'getRoleById').mockResolvedValueOnce(role); + + const result = await service.findSingleRole('1'); + + expect(result).toEqual({ + status_code: HttpStatus.OK, + message: ROLE_FETCHED_SUCCESSFULLY, + data: { + id: '1', + name: 'admin', + description: 'Administrator role', + permissions: [{ id: 'perm1', category: 'Permission 1' }], + }, + }); + }); + }); + + describe('updateRole', () => { + it('should throw not found exception if role does not exist', async () => { + const updateRoleOption = { id: '1', payload: { name: 'new name' } as UpdateOrganisationRoleDto }; + + jest.spyOn(rolesRepository, 'findOne').mockResolvedValueOnce(null); + + await expect(service.updateRole(updateRoleOption)).rejects.toThrow(CustomHttpException); + }); + + it('should update and return the role', async () => { + const updateRoleOption = { id: '1', payload: { name: 'new name' } as UpdateOrganisationRoleDto }; + const role = { id: '1', name: 'admin' } as Role; + + jest.spyOn(rolesRepository, 'findOne').mockResolvedValueOnce(role); + jest.spyOn(rolesRepository, 'save').mockResolvedValueOnce({ ...role, ...updateRoleOption.payload }); + + const result = await service.updateRole(updateRoleOption); + + expect(result).toEqual({ ...role, ...updateRoleOption.payload }); + }); + }); +}); diff --git a/src/modules/subscriptions/dto/get-all-subscription-error.dto.ts b/src/modules/subscriptions/dto/get-all-subscription-error.dto.ts new file mode 100644 index 000000000..9a9255e0d --- /dev/null +++ b/src/modules/subscriptions/dto/get-all-subscription-error.dto.ts @@ -0,0 +1,6 @@ +export class GetAllSubscriptionsError { + status: boolean; + status_code: number; + error: string; + message: string; +} diff --git a/src/modules/subscriptions/dto/get-all-subscription-response.dto.ts b/src/modules/subscriptions/dto/get-all-subscription-response.dto.ts new file mode 100644 index 000000000..edb6e3713 --- /dev/null +++ b/src/modules/subscriptions/dto/get-all-subscription-response.dto.ts @@ -0,0 +1,8 @@ +export class GetAllSubscriptionsResponseDto { + message: string; + data: SubscriptionCount; +} + +interface SubscriptionCount { + subscription_count: number; +} diff --git a/src/modules/subscriptions/entities/subscription.entity.ts b/src/modules/subscriptions/entities/subscription.entity.ts new file mode 100644 index 000000000..c440a768f --- /dev/null +++ b/src/modules/subscriptions/entities/subscription.entity.ts @@ -0,0 +1 @@ +export class Subscription {} diff --git a/src/modules/subscriptions/subscriptions.controller.ts b/src/modules/subscriptions/subscriptions.controller.ts new file mode 100644 index 000000000..b8cdb9f06 --- /dev/null +++ b/src/modules/subscriptions/subscriptions.controller.ts @@ -0,0 +1,25 @@ +import { Controller, Get } from '@nestjs/common'; +import { ApiBearerAuth, ApiInternalServerErrorResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { NotificationSettingsErrorDto } from '../notification-settings/dto/notification-settings-error.dto'; +import { GetAllSubscriptionsResponseDto } from './dto/get-all-subscription-response.dto'; +import { SubscriptionsService } from './subscriptions.service'; + +@ApiBearerAuth() +@Controller('subscriptions') +@ApiTags('Dashboard') +export class SubscriptionsController { + constructor(private readonly subscriptionsService: SubscriptionsService) {} + + @Get() + @ApiOkResponse({ + description: 'Fetch all active subscription count', + type: GetAllSubscriptionsResponseDto, + }) + @ApiInternalServerErrorResponse({ + description: 'Internal Server Error', + type: NotificationSettingsErrorDto, + }) + getAllSubscriptions(): Promise { + return this.subscriptionsService.getAllSubscriptions(); + } +} diff --git a/src/modules/subscriptions/subscriptions.module.ts b/src/modules/subscriptions/subscriptions.module.ts new file mode 100644 index 000000000..30a25a08e --- /dev/null +++ b/src/modules/subscriptions/subscriptions.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { NewsletterSubscription } from '../newsletter-subscription/entities/newsletter-subscription.entity'; +import { NewsletterSubscriptionService } from '../newsletter-subscription/newsletter-subscription.service'; +import { Organisation } from '../organisations/entities/organisations.entity'; +import { User } from '../user/entities/user.entity'; +import { SubscriptionsController } from './subscriptions.controller'; +import { SubscriptionsService } from './subscriptions.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([NewsletterSubscription, Organisation, User])], + controllers: [SubscriptionsController], + providers: [SubscriptionsService, NewsletterSubscriptionService], +}) +export class SubscriptionsModule {} diff --git a/src/modules/subscriptions/subscriptions.service.spec.ts b/src/modules/subscriptions/subscriptions.service.spec.ts new file mode 100644 index 000000000..9ff3923c8 --- /dev/null +++ b/src/modules/subscriptions/subscriptions.service.spec.ts @@ -0,0 +1,44 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { NewsletterSubscription } from '../newsletter-subscription/entities/newsletter-subscription.entity'; +import { NewsletterSubscriptionService } from '../newsletter-subscription/newsletter-subscription.service'; +import { SubscriptionsService } from './subscriptions.service'; + +describe('SubscriptionsService', () => { + let service: SubscriptionsService; + let repository: Repository; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + SubscriptionsService, + NewsletterSubscriptionService, + { + provide: getRepositoryToken(NewsletterSubscription), + useClass: Repository, + }, + ], + }).compile(); + + service = module.get(SubscriptionsService); + repository = module.get>(getRepositoryToken(NewsletterSubscription)); + }); + + it('should return subscription count', async () => { + const subscriptionCount = 5; + const findAndCountMock = jest.spyOn(repository, 'findAndCount').mockResolvedValue([[], subscriptionCount]); + + const result = await service.getAllSubscriptions(); + + expect(findAndCountMock).toHaveBeenCalledWith({ + where: { deletedAt: null }, + }); + expect(result).toEqual({ + message: 'Subscription count fetched successfully', + data: { + subscription_count: subscriptionCount, + }, + }); + }); +}); diff --git a/src/modules/subscriptions/subscriptions.service.ts b/src/modules/subscriptions/subscriptions.service.ts new file mode 100644 index 000000000..75097348f --- /dev/null +++ b/src/modules/subscriptions/subscriptions.service.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { NewsletterSubscription } from '../newsletter-subscription/entities/newsletter-subscription.entity'; +import { NewsletterSubscriptionService } from '../newsletter-subscription/newsletter-subscription.service'; +import { GetAllSubscriptionsResponseDto } from './dto/get-all-subscription-response.dto'; + +@Injectable() +export class SubscriptionsService { + constructor( + private readonly newsletterSubscriptionService: NewsletterSubscriptionService, + + @InjectRepository(NewsletterSubscription) + private readonly newsletterSubscriptionRepository: Repository + ) {} + + async getAllSubscriptions(): Promise { + const [, subscription_count] = await this.newsletterSubscriptionRepository.findAndCount({ + where: { deletedAt: null }, + }); + + return { + message: 'Subscription count fetched successfully', + data: { + subscription_count, + }, + }; + } +} diff --git a/src/modules/teams/teams.controller.ts b/src/modules/teams/teams.controller.ts index 661d5c950..8efc0cded 100644 --- a/src/modules/teams/teams.controller.ts +++ b/src/modules/teams/teams.controller.ts @@ -3,7 +3,7 @@ import { TeamsService } from './teams.service'; import { CreateTeamDto } from './dto/create-team.dto'; import { UpdateTeamDto } from './dto/update-team.dto'; import { SuperAdminGuard } from '../../guards/super-admin.guard'; -import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBearerAuth } from '@nestjs/swagger'; import { TeamMemberResponseDto } from './dto/team.response.dto'; @ApiTags('Teams') @@ -11,6 +11,7 @@ import { TeamMemberResponseDto } from './dto/team.response.dto'; export class TeamsController { constructor(private readonly teamsService: TeamsService) {} + @ApiBearerAuth() @UseGuards(SuperAdminGuard) @Post() @ApiOperation({ summary: 'Create a new team member' }) @@ -72,6 +73,7 @@ export class TeamsController { }; } + @ApiBearerAuth() @UseGuards(SuperAdminGuard) @Patch(':id') @ApiOperation({ summary: 'Update a team member' }) @@ -85,6 +87,7 @@ export class TeamsController { return this.teamsService.updateTeamMember(id, updateTeamDto); } + @ApiBearerAuth() @UseGuards(SuperAdminGuard) @Delete(':id') @ApiOperation({ summary: 'Delete a team member' }) diff --git a/src/modules/teams/teams.module.ts b/src/modules/teams/teams.module.ts index 3ff7acd62..2b502f487 100644 --- a/src/modules/teams/teams.module.ts +++ b/src/modules/teams/teams.module.ts @@ -4,9 +4,12 @@ import { TeamsController } from './teams.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Team } from './entities/team.entity'; import { User } from '../user/entities/user.entity'; +import { Organisation } from '../organisations/entities/organisations.entity'; +import { OrganisationUserRole } from '../role/entities/organisation-user-role.entity'; +import { Role } from '../role/entities/role.entity'; @Module({ - imports: [TypeOrmModule.forFeature([Team, User])], + imports: [TypeOrmModule.forFeature([Team, User, Organisation, OrganisationUserRole, Role])], controllers: [TeamsController], providers: [TeamsService], }) diff --git a/src/modules/testimonials/dto/create-testimonial-response.dto.ts b/src/modules/testimonials/dto/create-testimonial-response.dto.ts index 3ae3308aa..d805dda03 100644 --- a/src/modules/testimonials/dto/create-testimonial-response.dto.ts +++ b/src/modules/testimonials/dto/create-testimonial-response.dto.ts @@ -24,6 +24,7 @@ export class CreateTestimonialResponseDto { type: TestimonialData, description: 'Testimonial data', example: { + id: '1', user_id: '1', name: 'John Doe', content: 'I am very happy with the service provided by the company', diff --git a/src/modules/testimonials/dto/get-testimonials.dto.ts b/src/modules/testimonials/dto/get-testimonials.dto.ts new file mode 100644 index 000000000..8692eac2a --- /dev/null +++ b/src/modules/testimonials/dto/get-testimonials.dto.ts @@ -0,0 +1,67 @@ +import { TestimonialData } from '../interfaces/testimonials.interface'; +import { IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +class TestimonialDataDto { + @ApiProperty({ + description: "The user's id", + example: '1', + }) + @IsString() + user_id: string; + + @ApiProperty({ + description: "The list of the user's testimonials", + example: { + id: 'testimonial-id', + name: 'Client Name', + content: 'Testimonial Content', + created_at: new Date(), + }, + }) + testimonials: TestimonialData[]; +} + +export class GetTestimonialsResponseDto { + @ApiProperty({ + description: 'Response message', + example: 'User testimonials retrieved successfully', + }) + @IsString() + message: string; + + @ApiProperty({ + description: 'The data', + }) + data: TestimonialDataDto; +} + +export class GetTestimonials400ErrorResponseDto { + @ApiProperty({ + description: 'Response message', + example: 'User has no testimonials', + }) + @IsString() + message: string; + + @ApiProperty({ + description: 'Response status code', + example: '400', + }) + status: number; +} + +export class GetTestimonials404ErrorResponseDto { + @ApiProperty({ + description: 'Response message', + example: 'User not found!', + }) + @IsString() + message: string; + + @ApiProperty({ + description: 'Response status code', + example: '404', + }) + status: number; +} diff --git a/src/modules/testimonials/dto/update-testimonial.dto.ts b/src/modules/testimonials/dto/update-testimonial.dto.ts new file mode 100644 index 000000000..7e19ec19e --- /dev/null +++ b/src/modules/testimonials/dto/update-testimonial.dto.ts @@ -0,0 +1,14 @@ +import { IsString, IsOptional } from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; + +export class UpdateTestimonialDto { + @ApiPropertyOptional({ description: 'Updated content of the testimonial' }) + @IsString() + @IsOptional() + content?: string; + + @ApiPropertyOptional({ description: 'Updated name associated with the testimonial' }) + @IsString() + @IsOptional() + name?: string; +} diff --git a/src/modules/testimonials/dto/update-testimonial.response.dto.ts b/src/modules/testimonials/dto/update-testimonial.response.dto.ts new file mode 100644 index 000000000..485584491 --- /dev/null +++ b/src/modules/testimonials/dto/update-testimonial.response.dto.ts @@ -0,0 +1,6 @@ +export class UpdateTestimonialResponseDto { + status: string; + message: string; + data: any; + } + \ No newline at end of file diff --git a/src/modules/testimonials/interfaces/testimonial-response.interface.ts b/src/modules/testimonials/interfaces/testimonial-response.interface.ts new file mode 100644 index 000000000..f94d1c120 --- /dev/null +++ b/src/modules/testimonials/interfaces/testimonial-response.interface.ts @@ -0,0 +1,6 @@ +export interface TestimonialResponse { + id: string; + author: string; + comments: any[]; + created_at: Date; +} diff --git a/src/modules/testimonials/mappers/testimonial-response.mapper.ts b/src/modules/testimonials/mappers/testimonial-response.mapper.ts new file mode 100644 index 000000000..d3886b369 --- /dev/null +++ b/src/modules/testimonials/mappers/testimonial-response.mapper.ts @@ -0,0 +1,15 @@ +import { Testimonial } from '../entities/testimonials.entity'; + +export class TestimonialResponseMapper { + static mapToEntity(testimonial: Testimonial) { + if (!testimonial) throw new Error('Testimonial is required'); + + return { + id: testimonial.id, + author: testimonial.name, + content: testimonial.content, + comments: [], + created_at: testimonial.created_at, + }; + } +} diff --git a/src/modules/testimonials/mappers/testimonial.mapper.ts b/src/modules/testimonials/mappers/testimonial.mapper.ts new file mode 100644 index 000000000..bbd04845b --- /dev/null +++ b/src/modules/testimonials/mappers/testimonial.mapper.ts @@ -0,0 +1,14 @@ +import { Testimonial } from '../entities/testimonials.entity'; + +export class TestimonialMapper { + static mapToEntity(testimonial: Testimonial) { + if (!testimonial) throw new Error('Testimonial is required'); + + return { + id: testimonial.id, + name: testimonial.name, + content: testimonial.content, + created_at: testimonial.created_at, + }; + } +} diff --git a/src/modules/testimonials/testimonials.controller.ts b/src/modules/testimonials/testimonials.controller.ts index ee3b600b2..1367fc70a 100644 --- a/src/modules/testimonials/testimonials.controller.ts +++ b/src/modules/testimonials/testimonials.controller.ts @@ -1,10 +1,31 @@ -import { Body, Controller, Post, Req } from '@nestjs/common'; +import { + Body, + Controller, + Post, + Req, + Get, + Param, + DefaultValuePipe, + Query, + ParseIntPipe, + ParseUUIDPipe, + HttpStatus, + Delete, + Patch, +} from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { UserPayload } from '../user/interfaces/user-payload.interface'; import UserService from '../user/user.service'; import { CreateTestimonialResponseDto } from './dto/create-testimonial-response.dto'; import { CreateTestimonialDto } from './dto/create-testimonial.dto'; import { TestimonialsService } from './testimonials.service'; +import { + GetTestimonialsResponseDto, + GetTestimonials400ErrorResponseDto, + GetTestimonials404ErrorResponseDto, +} from './dto/get-testimonials.dto'; +import { UpdateTestimonialDto } from './dto/update-testimonial.dto'; +import { UpdateTestimonialResponseDto } from './dto/update-testimonial.response.dto'; @ApiBearerAuth() @ApiTags('Testimonials') @@ -12,8 +33,7 @@ import { TestimonialsService } from './testimonials.service'; export class TestimonialsController { constructor( private readonly testimonialsService: TestimonialsService, - - private userService: UserService + private readonly userService: UserService ) {} @Post() @@ -24,13 +44,14 @@ export class TestimonialsController { @ApiResponse({ status: 500, description: 'Internal Server Error' }) async create( @Body() createTestimonialDto: CreateTestimonialDto, - @Req() req: { user: UserPayload } + @Req() req: { user: UserPayload; language: string } ): Promise { - const userId = req?.user.id; + const language = req.language; + const userId = req.user.id; const user = await this.userService.getUserRecord({ identifier: userId, identifierType: 'id' }); - const data = await this.testimonialsService.createTestimonial(createTestimonialDto, user); + const data = await this.testimonialsService.createTestimonial(createTestimonialDto, user, language); return { status: 'success', @@ -38,4 +59,95 @@ export class TestimonialsController { data, }; } + + @Get('user/:user_id') + @ApiOperation({ summary: "Get All User's Testimonials" }) + @ApiResponse({ + status: 200, + description: 'User testimonials retrieved successfully', + type: GetTestimonialsResponseDto, + }) + @ApiResponse({ + status: 404, + description: 'User not found', + type: GetTestimonials404ErrorResponseDto, + }) + @ApiResponse({ + status: 400, + description: 'User has no testimonials', + type: GetTestimonials400ErrorResponseDto, + }) + async getAllTestimonials( + @Param('user_id') userId: string, + @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number, + @Query('page_size', new DefaultValuePipe(3), ParseIntPipe) page_size: number, + @Req() req: { language: string } + ) { + const language = req.language; + return this.testimonialsService.getAllTestimonials(userId, page, page_size, language); + } + + @Get(':testimonial_id') + @ApiOperation({ summary: 'Get Testimonial By ID' }) + @ApiResponse({ + status: 200, + description: 'Testimonial fetched successfully', + }) + @ApiResponse({ + status: 404, + description: 'Testimonial not found', + type: GetTestimonials404ErrorResponseDto, + }) + @ApiResponse({ + status: 401, + description: 'Unauthorized', + }) + async getTestimonialById( + @Param('testimonial_id', ParseUUIDPipe) testimonialId: string, + @Req() req: { language: string } + ) { + const language = req.language; + + const testimonial = await this.testimonialsService.getTestimonialById(testimonialId, language); + + return { + status_code: HttpStatus.OK, + message: 'Testimonial fetched successfully', + data: testimonial, + }; + } + + @Delete(':id') + @ApiOperation({ summary: 'Delete a Testimonial' }) + @ApiResponse({ status: 200, description: 'Testimonial deleted successfully' }) + @ApiResponse({ status: 400, description: 'Bad Request' }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + @ApiResponse({ status: 404, description: 'Testimonial not found' }) + @ApiResponse({ status: 500, description: 'Internal Server Error' }) + async deleteTestimonial(@Param('id') id: string) { + return this.testimonialsService.deleteTestimonial(id); + } + + @Patch(':id') + @ApiOperation({ summary: 'Update Testimonial' }) + @ApiResponse({ status: 200, description: 'Testimonial updated successfully' }) + @ApiResponse({ status: 404, description: 'Testimonial not found' }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + @ApiResponse({ status: 500, description: 'Internal Server Error' }) + async update( + @Param('id') id: string, + @Body() updateTestimonialDto: UpdateTestimonialDto, + @Req() req: { user: UserPayload; language: string } + ): Promise { + const language = req.language; + const userId = req.user.id; + + const data = await this.testimonialsService.updateTestimonial(id, updateTestimonialDto, userId, language); + + return { + status: 'success', + message: 'Testimonial updated successfully', + data, + }; + } } diff --git a/src/modules/testimonials/testimonials.module.ts b/src/modules/testimonials/testimonials.module.ts index 202e34bb9..312fde31b 100644 --- a/src/modules/testimonials/testimonials.module.ts +++ b/src/modules/testimonials/testimonials.module.ts @@ -7,10 +7,11 @@ import { Testimonial } from './entities/testimonials.entity'; import { TestimonialsController } from './testimonials.controller'; import { TestimonialsService } from './testimonials.service'; import { Profile } from '../profile/entities/profile.entity'; +import { TextService } from '../translation/translation.service'; @Module({ imports: [TypeOrmModule.forFeature([Testimonial, User, Profile])], controllers: [TestimonialsController], - providers: [TestimonialsService, Repository, UserService], + providers: [TestimonialsService, Repository, UserService, TextService], }) export class TestimonialsModule {} diff --git a/src/modules/testimonials/testimonials.service.ts b/src/modules/testimonials/testimonials.service.ts index 78accfdea..5793b1fe0 100644 --- a/src/modules/testimonials/testimonials.service.ts +++ b/src/modules/testimonials/testimonials.service.ts @@ -9,34 +9,53 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { CreateTestimonialDto } from './dto/create-testimonial.dto'; import { Testimonial } from './entities/testimonials.entity'; +import * as SYS_MSG from '../../helpers/SystemMessages'; +import { CustomHttpException } from '../../helpers/custom-http-filter'; +import UserService from '../user/user.service'; +import { TestimonialMapper } from './mappers/testimonial.mapper'; +import { TestimonialResponseMapper } from './mappers/testimonial-response.mapper'; +import { TestimonialResponse } from './interfaces/testimonial-response.interface'; +import { UpdateTestimonialDto } from './dto/update-testimonial.dto'; +import { TextService } from '../translation/translation.service'; @Injectable() export class TestimonialsService { constructor( @InjectRepository(Testimonial) - private readonly testimonialRepository: Repository + private readonly testimonialRepository: Repository, + private userService: UserService, + private readonly textService: TextService ) {} - async createTestimonial(createTestimonialDto: CreateTestimonialDto, user) { + + private async translateContent(content: string, lang: string) { + return this.textService.translateText(content, lang); + } + + async createTestimonial(createTestimonialDto: CreateTestimonialDto, user, language?: string) { try { const { content, name } = createTestimonialDto; if (!user) { throw new NotFoundException({ status: 'error', - error: 'Not Found', + error: 'User not found', status_code: HttpStatus.NOT_FOUND, }); } - await this.testimonialRepository.save({ + const translatedContent = await this.translateContent(content, language); + + const newTestimonial = await this.testimonialRepository.save({ user, name, - content, + content: translatedContent, }); return { + id: newTestimonial.id, user_id: user.id, - ...createTestimonialDto, + name: name, + content: translatedContent, created_at: new Date(), }; } catch (error) { @@ -49,4 +68,97 @@ export class TestimonialsService { } } } + + async getAllTestimonials(userId: string, page: number, pageSize: number, lang?: string) { + const user = await this.userService.getUserRecord({ + identifier: userId, + identifierType: 'id', + }); + + if (!user) throw new CustomHttpException(SYS_MSG.USER_NOT_FOUND, HttpStatus.NOT_FOUND); + + let testimonials = await this.testimonialRepository.find({ + relations: ['user'], + where: { + user: { + id: userId, + }, + }, + }); + + if (!testimonials.length) throw new CustomHttpException(SYS_MSG.NO_USER_TESTIMONIALS, HttpStatus.BAD_REQUEST); + + testimonials = testimonials.slice((page - 1) * pageSize, page * pageSize); + + const data = await Promise.all( + testimonials.map(async testimonial => { + testimonial.content = await this.translateContent(testimonial.content, lang); + return TestimonialMapper.mapToEntity(testimonial); + }) + ); + + return { + message: SYS_MSG.USER_TESTIMONIALS_FETCHED, + data: { + user_id: userId, + testimonials: data, + }, + pagination: { + page: page, + page_size: pageSize, + total_pages: Math.ceil(testimonials.length / pageSize), + }, + }; + } + + async getTestimonialById(testimonialId: string, lang: string): Promise { + const testimonial = await this.testimonialRepository.findOne({ + where: { id: testimonialId }, + relations: ['user'], + }); + + if (!testimonial) { + throw new CustomHttpException('Testimonial not found', HttpStatus.NOT_FOUND); + } + + testimonial.content = await this.translateContent(testimonial.content, lang); + + return TestimonialResponseMapper.mapToEntity(testimonial); + } + + async updateTestimonial(id: string, updateTestimonialDto: UpdateTestimonialDto, userId: string, lang?: string) { + const testimonial = await this.testimonialRepository.findOne({ where: { id, user: { id: userId } } }); + + if (!testimonial) { + throw new CustomHttpException('Testimonial not found', HttpStatus.NOT_FOUND); + } + + Object.assign(testimonial, updateTestimonialDto); + + if (updateTestimonialDto.content) { + testimonial.content = await this.translateContent(updateTestimonialDto.content, lang); + } + + await this.testimonialRepository.save(testimonial); + + return { + id: testimonial.id, + user_id: userId, + content: testimonial.content, + name: testimonial.name, + updated_at: new Date(), + }; + } + + async deleteTestimonial(id: string) { + const testimonial = await this.testimonialRepository.findOne({ where: { id } }); + if (!testimonial) { + throw new CustomHttpException('Testimonial not found', HttpStatus.NOT_FOUND); + } + await this.testimonialRepository.remove(testimonial); + return { + message: 'Testimonial deleted successfully', + status_code: HttpStatus.OK, + }; + } } diff --git a/src/modules/testimonials/tests/mocks/testimonials.mock.ts b/src/modules/testimonials/tests/mocks/testimonials.mock.ts new file mode 100644 index 000000000..966abb6bc --- /dev/null +++ b/src/modules/testimonials/tests/mocks/testimonials.mock.ts @@ -0,0 +1,45 @@ +import { Testimonial } from '../../entities/testimonials.entity'; +import { mockUser } from '../../../organisations/tests/mocks/user.mock'; + +export const testimonialsMock: Testimonial[] = [ + { + id: '1', + user: mockUser, + name: 'Mary Jane', + content: 'Excellent Work!', + created_at: new Date(), + updated_at: new Date(), + }, + { + id: '2', + user: mockUser, + name: 'Clara James', + content: 'Excellent Work! Highly Recommend', + created_at: new Date(), + updated_at: new Date(), + }, + { + id: '3', + user: mockUser, + name: 'Jamie Waen', + content: 'Organized and quality work!', + created_at: new Date(), + updated_at: new Date(), + }, + { + id: '4', + user: mockUser, + name: 'Joanne', + content: 'Excellent Work!', + created_at: new Date(), + updated_at: new Date(), + }, + { + id: '5', + user: mockUser, + name: 'Mary Jane', + content: 'Highly Recommend!', + created_at: new Date(), + updated_at: new Date(), + }, +]; diff --git a/src/modules/testimonials/tests/testimonials.service.spec.ts b/src/modules/testimonials/tests/testimonials.service.spec.ts index 98f56dde7..9751aebfe 100644 --- a/src/modules/testimonials/tests/testimonials.service.spec.ts +++ b/src/modules/testimonials/tests/testimonials.service.spec.ts @@ -1,4 +1,4 @@ -import { InternalServerErrorException, NotFoundException } from '@nestjs/common'; +import { InternalServerErrorException, NotFoundException, HttpStatus } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; @@ -8,6 +8,17 @@ import UserService from '../../user/user.service'; import { CreateTestimonialDto } from '../dto/create-testimonial.dto'; import { Testimonial } from '../entities/testimonials.entity'; import { TestimonialsService } from '../testimonials.service'; +import * as SYS_MSG from '../../../helpers/SystemMessages'; +import { CustomHttpException } from '../../../helpers/custom-http-filter'; +import { mockUser } from '../../organisations/tests/mocks/user.mock'; +import { testimonialsMock } from './mocks/testimonials.mock'; +import { TextService } from '../../translation/translation.service'; + +class MockTextService { + translateText(text: string, targetLang: string) { + return Promise.resolve(text); + } +} describe('TestimonialsService', () => { let service: TestimonialsService; @@ -19,6 +30,10 @@ describe('TestimonialsService', () => { providers: [ TestimonialsService, UserService, + { + provide: TextService, + useClass: MockTextService, + }, { provide: getRepositoryToken(Testimonial), useClass: Repository, @@ -50,13 +65,19 @@ describe('TestimonialsService', () => { content: 'Great service!', }; const user = { id: 'user_id' } as User; + const testimonial = { + id: 'test_id', + ...createTestimonialDto, + created_at: new Date(), + } as Testimonial; jest.spyOn(userService, 'getUserRecord').mockResolvedValue(user); - jest.spyOn(testimonialRepository, 'save').mockResolvedValue(undefined); + jest.spyOn(testimonialRepository, 'save').mockResolvedValue(testimonial); const result = await service.createTestimonial(createTestimonialDto, user); expect(result).toEqual({ + id: 'test_id', user_id: 'user_id', ...createTestimonialDto, created_at: expect.any(Date), @@ -99,4 +120,68 @@ describe('TestimonialsService', () => { ); }); }); + + describe("retrieve all user's testimonials", () => { + it('should validate the user id', async () => { + jest.spyOn(userService, 'getUserRecord').mockResolvedValue(null); + + await expect(service.getAllTestimonials('user_id', 1, 10)).rejects.toThrow( + new CustomHttpException(SYS_MSG.USER_NOT_FOUND, HttpStatus.NOT_FOUND) + ); + }); + + it('should throw an error if the user has no testimonials', async () => { + jest.spyOn(userService, 'getUserRecord').mockResolvedValue(mockUser); + jest.spyOn(testimonialRepository, 'find').mockResolvedValue([]); + + await expect(service.getAllTestimonials('user_id', 1, 10)).rejects.toThrow( + new CustomHttpException(SYS_MSG.NO_USER_TESTIMONIALS, HttpStatus.BAD_REQUEST) + ); + }); + + it('should return all testimonials for a user', async () => { + jest.spyOn(userService, 'getUserRecord').mockResolvedValue(mockUser); + jest.spyOn(testimonialRepository, 'find').mockResolvedValue(testimonialsMock); + + const res = await service.getAllTestimonials(mockUser.id, 1, 5); + + expect(res.message).toEqual(SYS_MSG.USER_TESTIMONIALS_FETCHED); + expect(res.data.testimonials.length).toEqual(testimonialsMock.length); + expect(res.data.user_id).toEqual(mockUser.id); + }); + }); + + describe('deleteTestimonial', () => { + it('should successfully delete a testimonial', async () => { + const testimonialId = 'test_id'; + const mockTestimonial = new Testimonial(); + mockTestimonial.id = testimonialId; + + jest.spyOn(testimonialRepository, 'findOne').mockResolvedValue(mockTestimonial); + jest.spyOn(testimonialRepository, 'remove').mockResolvedValue(undefined); + + const result = await service.deleteTestimonial(testimonialId); + + expect(testimonialRepository.findOne).toHaveBeenCalledWith({ where: { id: testimonialId } }); + expect(testimonialRepository.remove).toHaveBeenCalledWith(mockTestimonial); + expect(result).toEqual({ + message: 'Testimonial deleted successfully', + status_code: HttpStatus.OK, + }); + }); + + it('should throw CustomHttpException when testimonial is not found', async () => { + const id = 'non_existent_id'; + + jest.spyOn(testimonialRepository, 'findOne').mockResolvedValue(null); + jest.spyOn(testimonialRepository, 'remove').mockImplementation(jest.fn()); + + await expect(service.deleteTestimonial(id)).rejects.toThrow( + new CustomHttpException('Testimonial not found', HttpStatus.NOT_FOUND) + ); + + expect(testimonialRepository.findOne).toHaveBeenCalledWith({ where: { id } }); + expect(testimonialRepository.remove).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/modules/testimonials/tests/update.service.spec.ts b/src/modules/testimonials/tests/update.service.spec.ts new file mode 100644 index 000000000..282209637 --- /dev/null +++ b/src/modules/testimonials/tests/update.service.spec.ts @@ -0,0 +1,93 @@ +import { InternalServerErrorException, NotFoundException, HttpStatus } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Profile } from '../../profile/entities/profile.entity'; +import { User } from '../../user/entities/user.entity'; +import UserService from '../../user/user.service'; +import { UpdateTestimonialDto } from '../dto/update-testimonial.dto'; +import { Testimonial } from '../entities/testimonials.entity'; +import { TestimonialsService } from '../testimonials.service'; +import { TextService } from '../../translation/translation.service'; + +describe('TestimonialsService', () => { + let service: TestimonialsService; + let userService: UserService; + let testimonialRepository: Repository; + class MockTextService { + translateText(text: string, targetLang: string) { + return Promise.resolve(text); + } + } + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + TestimonialsService, + UserService, + { + provide: TextService, + useClass: MockTextService, + }, + { + provide: getRepositoryToken(Testimonial), + useClass: Repository, + }, + { + provide: getRepositoryToken(User), + useClass: Repository, + }, + { + provide: getRepositoryToken(Profile), + useClass: Repository, + }, + ], + }).compile(); + + service = module.get(TestimonialsService); + userService = module.get(UserService); + testimonialRepository = module.get>(getRepositoryToken(Testimonial)); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('updateTestimonial', () => { + it('should successfully update a testimonial', async () => { + const id = 'testimonial_id'; + const updateTestimonialDto: UpdateTestimonialDto = { + name: 'Updated Name', + content: 'Updated content!', + }; + const userId = 'user_id'; + const testimonial = { id, user: { id: userId }, ...updateTestimonialDto } as Testimonial; + + jest.spyOn(testimonialRepository, 'findOne').mockResolvedValue(testimonial); + jest.spyOn(testimonialRepository, 'save').mockResolvedValue(testimonial); + + const result = await service.updateTestimonial(id, updateTestimonialDto, userId); + + expect(result).toEqual({ + id, + user_id: userId, + ...updateTestimonialDto, + updated_at: expect.any(Date), + }); + }); + + it('should throw a NotFoundException if testimonial is not found', async () => { + const id = 'testimonial_id'; + const updateTestimonialDto: UpdateTestimonialDto = { + name: 'Updated Name', + content: 'Updated content!', + }; + const userId = 'user_id'; + + jest.spyOn(testimonialRepository, 'findOne').mockResolvedValue(null); + + await expect(service.updateTestimonial(id, updateTestimonialDto, userId)).rejects.toThrow( + new NotFoundException('Testimonial not found') + ); + }); + }); +}); diff --git a/src/modules/translation/translation.service.ts b/src/modules/translation/translation.service.ts new file mode 100644 index 000000000..6788de5f4 --- /dev/null +++ b/src/modules/translation/translation.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; +import { GenerativeModel, GoogleGenerativeAI } from '@google/generative-ai'; + +@Injectable() +export class TextService { + private model: GenerativeModel; + + constructor() { + const genAI = new GoogleGenerativeAI(process.env.OPENAI_API_KEY); + this.model = genAI.getGenerativeModel({ model: 'gemini-1.5-flash' }); + } + + async translateText(text: string, targetLanguage: string): Promise { + const prompt = `Please provide a direct translation of the following text to ${targetLanguage}, without any additional context or explanations:\n\n"${text}"`; + + const result = await this.model.generateContent(prompt); + const response = await result.response; + let translatedText = await response.text(); + translatedText = translatedText.replace(/^"|"$/g, '').trim(); + + return translatedText; + } +} diff --git a/src/modules/user/dto/get-user-stats-response.dto.ts b/src/modules/user/dto/get-user-stats-response.dto.ts new file mode 100644 index 000000000..6a7544f51 --- /dev/null +++ b/src/modules/user/dto/get-user-stats-response.dto.ts @@ -0,0 +1,25 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class GetUserStatsResponseDto { + @ApiProperty({ example: 'success' }) + status: string; + + @ApiProperty({ example: 200 }) + status_code: number; + + @ApiProperty({ example: 'User statistics retrieved successfully' }) + message: string; + + @ApiProperty({ + example: { + total_users: 100, + active_users: 80, + deleted_users: 20, + }, + }) + data: { + total_users: number; + active_users: number; + deleted_users: number; + }; +} diff --git a/src/modules/user/dto/reactivate-account.dto.ts b/src/modules/user/dto/reactivate-account.dto.ts new file mode 100644 index 000000000..8efa431fa --- /dev/null +++ b/src/modules/user/dto/reactivate-account.dto.ts @@ -0,0 +1,22 @@ +import { IsBoolean, IsOptional, IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class ReactivateAccountDto { + @ApiProperty({ + example: true, + description: 'Email to reactivate the account', + nullable: false, + }) + @IsString() + email: string; + + @ApiProperty({ + example: 'Now needed', + description: 'Optional reason for reactivating the account', + nullable: true, + required: false, + }) + @IsString() + @IsOptional() + reason?: string; +} diff --git a/src/modules/user/dto/update-user-status-response.dto.ts b/src/modules/user/dto/update-user-status-response.dto.ts new file mode 100644 index 000000000..fc3504e9e --- /dev/null +++ b/src/modules/user/dto/update-user-status-response.dto.ts @@ -0,0 +1,25 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { number, object } from 'joi'; +import { User } from '../entities/user.entity'; + +export class UpdateUserStatusResponseDto { + @ApiProperty({ type: String, example: 'success' }) + status: string; + + @ApiProperty({ type: number, example: 200 }) + status_code: number; + + @ApiProperty({ + type: object, + example: { + id: '4a3731d6-8dfd-42b1-b572-96c7805f7586', + created_at: '2024-08-05T19:16:57.264Z', + updated_at: '2024-08-05T19:43:25.073Z', + first_name: 'John', + last_name: 'Smith', + email: 'john.smith@example.com', + status: 'Hello there! This is what my updated status looks like!', + }, + }) + data: object; +} diff --git a/src/modules/user/dto/update-user-status.dto.ts b/src/modules/user/dto/update-user-status.dto.ts new file mode 100644 index 000000000..7447236d7 --- /dev/null +++ b/src/modules/user/dto/update-user-status.dto.ts @@ -0,0 +1,6 @@ +import { IsString } from 'class-validator'; + +export class UpdateUserStatusDto { + @IsString() + status: string; +} diff --git a/src/modules/user/dto/user-data-export.dto.ts b/src/modules/user/dto/user-data-export.dto.ts new file mode 100644 index 000000000..27a1e8879 --- /dev/null +++ b/src/modules/user/dto/user-data-export.dto.ts @@ -0,0 +1,11 @@ +import { IsEnum } from 'class-validator'; + +export enum FileFormat { + JSON = 'json', + XLSX = 'xlsx', +} + +export class UserDataExportDto { + @IsEnum(FileFormat) + format: FileFormat; +} diff --git a/src/modules/user/entities/user.entity.ts b/src/modules/user/entities/user.entity.ts index 507076ab8..ef0e40209 100644 --- a/src/modules/user/entities/user.entity.ts +++ b/src/modules/user/entities/user.entity.ts @@ -1,11 +1,25 @@ import * as bcrypt from 'bcryptjs'; -import { BeforeInsert, BeforeUpdate, Column, Entity, JoinColumn, OneToMany, OneToOne } from 'typeorm'; +import { + BeforeInsert, + BeforeUpdate, + Column, + DeleteDateColumn, + Entity, + JoinColumn, + JoinTable, + ManyToMany, + OneToMany, + OneToOne, +} from 'typeorm'; import { AbstractBaseEntity } from '../../../entities/base.entity'; import { Job } from '../../../modules/jobs/entities/job.entity'; import { NotificationSettings } from '../../../modules/notification-settings/entities/notification-setting.entity'; import { Notification } from '../../../modules/notifications/entities/notifications.entity'; import { Testimonial } from '../../../modules/testimonials/entities/testimonials.entity'; -import { OrganisationMember } from '../../organisations/entities/org-members.entity'; +import { Blog } from '../../blogs/entities/blog.entity'; +import { Comment } from '../../comments/entities/comments.entity'; +import { Cart } from '../../dashboard/entities/cart.entity'; +import { Order } from '../../dashboard/entities/order.entity'; import { Organisation } from '../../organisations/entities/organisations.entity'; import { Profile } from '../../profile/entities/profile.entity'; @@ -26,6 +40,9 @@ export class User extends AbstractBaseEntity { @Column({ unique: true, nullable: false }) email: string; + @Column({ unique: false, nullable: true }) + status: string; + @Column({ nullable: false }) password: string; @@ -50,18 +67,15 @@ export class User extends AbstractBaseEntity { @Column({ default: false }) is_2fa_enabled: boolean; - @Column({ - type: 'enum', - enum: UserType, - default: UserType.USER, - }) - user_type: UserType; + @DeleteDateColumn({ nullable: true }) + deletedAt?: Date; @OneToMany(() => Organisation, organisation => organisation.owner) owned_organisations: Organisation[]; - @OneToMany(() => Organisation, organisation => organisation.creator) - created_organisations: Organisation[]; + @ManyToMany(() => Organisation, organisation => organisation.members) + @JoinTable() + organisations: Organisation[]; @OneToMany(() => Job, job => job.user) jobs: Job[]; @@ -73,8 +87,8 @@ export class User extends AbstractBaseEntity { @OneToMany(() => Testimonial, testimonial => testimonial.user) testimonials: Testimonial[]; - @OneToMany(() => OrganisationMember, organisationMember => organisationMember.organisation_id) - organisationMembers: OrganisationMember[]; + @OneToMany(() => Blog, blog => blog.author) + blogs?: Blog[]; @BeforeInsert() @BeforeUpdate() @@ -87,4 +101,13 @@ export class User extends AbstractBaseEntity { @OneToOne(() => NotificationSettings, notification_settings => notification_settings.user) notification_settings: NotificationSettings[]; + + @OneToMany(() => Comment, comment => comment.user) + comments?: Comment[]; + + @OneToMany(() => Order, order => order.user) + orders?: Order[]; + + @OneToMany(() => Cart, cart => cart.user) + cart: Cart[]; } diff --git a/src/modules/user/interfaces/UserInterface.ts b/src/modules/user/interfaces/UserInterface.ts index 22132cb1c..b1984d5ec 100644 --- a/src/modules/user/interfaces/UserInterface.ts +++ b/src/modules/user/interfaces/UserInterface.ts @@ -1,4 +1,4 @@ -import { Profile } from 'src/modules/profile/entities/profile.entity'; +import { Profile } from '../../../modules/profile/entities/profile.entity'; import { UserType } from '../entities/user.entity'; interface UserInterface { @@ -18,8 +18,6 @@ interface UserInterface { is_2fa_enabled: boolean; - user_type: UserType; - is_active: boolean; attempts_left: number; diff --git a/src/modules/user/interfaces/user-payload.interface.ts b/src/modules/user/interfaces/user-payload.interface.ts index 502cb7162..f6cb45ca9 100644 --- a/src/modules/user/interfaces/user-payload.interface.ts +++ b/src/modules/user/interfaces/user-payload.interface.ts @@ -3,5 +3,5 @@ import { UserType } from '../entities/user.entity'; export interface UserPayload { id: string; email: string; - user_type: UserType; + // user_type: UserType; } diff --git a/src/modules/user/options/UpdateUserRecordOption.ts b/src/modules/user/options/UpdateUserRecordOption.ts index fe137b4ed..c4302d601 100644 --- a/src/modules/user/options/UpdateUserRecordOption.ts +++ b/src/modules/user/options/UpdateUserRecordOption.ts @@ -1,4 +1,4 @@ -import { UpdateRecordGeneric } from 'src/helpers/UpdateRecordGeneric'; +import { UpdateRecordGeneric } from '../../../helpers/UpdateRecordGeneric'; import UserIdentifierOptionsType from './UserIdentifierOptions'; import UserInterface from '../interfaces/UserInterface'; diff --git a/src/modules/user/tests/mocks/user.mock.ts b/src/modules/user/tests/mocks/user.mock.ts new file mode 100644 index 000000000..9f9dfd58e --- /dev/null +++ b/src/modules/user/tests/mocks/user.mock.ts @@ -0,0 +1,28 @@ +import { User } from '../../entities/user.entity'; + +export const mockUser: User = { + id: 'user1', + first_name: 'John', + last_name: 'Doe', + email: 'john@example.com', + password: 'hashedpassword', + phone: '1234567890', + is_active: true, + attempts_left: 3, + time_left: null, + secret: 'secret', + is_2fa_enabled: false, + owned_organisations: [], + profile: null, + status: 'active', + testimonials: [], + backup_codes: [], + jobs: [], + created_at: new Date(), + updated_at: new Date(), + notification_settings: [], + notifications: [], + hashPassword: () => null, + cart: [], + organisations: null, +}; diff --git a/src/modules/user/tests/reactivate-user.service.spec.ts b/src/modules/user/tests/reactivate-user.service.spec.ts new file mode 100644 index 000000000..b8f8daee2 --- /dev/null +++ b/src/modules/user/tests/reactivate-user.service.spec.ts @@ -0,0 +1,108 @@ +import { ReactivateAccountDto } from '../dto/reactivate-account.dto'; +import { BadRequestException, ForbiddenException, HttpException, NotFoundException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { User } from '../entities/user.entity'; +import UserService from '../user.service'; +import { Profile } from '../../profile/entities/profile.entity'; +import { CustomHttpException } from '../../../helpers/custom-http-filter'; + +describe('UserService', () => { + let service: UserService; + let userRepository: Repository; + let profileRepository: Repository; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + UserService, + { + provide: getRepositoryToken(User), + useClass: Repository, + }, + { + provide: getRepositoryToken(Profile), + useClass: Repository, + }, + ], + }).compile(); + + service = module.get(UserService); + userRepository = module.get>(getRepositoryToken(User)); + profileRepository = module.get>(getRepositoryToken(Profile)); + + userRepository.save = jest.fn(); + }); + + describe('reactivateUser', () => { + const mockEmail = 'test@example.com'; + const mockUser = { + id: 1, + first_name: 'John', + last_name: 'Doe', + is_active: false, + attempts_left: 0, + time_left: 0, + email: mockEmail, + } as any; + + const reactivateAccountDto: ReactivateAccountDto = { + email: mockEmail, + }; + + it('should reactivate a user successfully', async () => { + jest.spyOn(service, 'getUserRecord').mockResolvedValue(mockUser); + jest.spyOn(userRepository, 'save').mockResolvedValue(mockUser); + + const result = await service.reactivateUser(mockEmail, reactivateAccountDto); + + expect(result).toEqual({ + status: 'success', + message: 'User Reactivated Successfully', + user: { + id: mockUser.id, + name: `${mockUser.first_name} ${mockUser.last_name}`, + }, + }); + expect(service.getUserRecord).toHaveBeenCalledWith({ + identifier: mockEmail, + identifierType: 'email', + }); + expect(userRepository.save).toHaveBeenCalledWith({ + ...mockUser, + is_active: true, + attempts_left: 5, + time_left: 30 * 60 * 1000, + }); + }); + + it('should throw a CustomHTTPException if user is not found', async () => { + jest.spyOn(service, 'getUserRecord').mockResolvedValue(null); + + await expect(service.reactivateUser(mockEmail, reactivateAccountDto)).rejects.toThrow(CustomHttpException); + expect(service.getUserRecord).toHaveBeenCalledWith({ + identifier: mockEmail, + identifierType: 'email', + }); + expect(userRepository.save).not.toHaveBeenCalled(); + }); + + it('should handle unexpected errors', async () => { + jest.spyOn(service, 'getUserRecord').mockResolvedValue(mockUser); + jest.spyOn(userRepository, 'save').mockRejectedValue(new Error('Unexpected error')); + + await expect(service.reactivateUser(mockEmail, reactivateAccountDto)).rejects.toThrow(Error); + expect(service.getUserRecord).toHaveBeenCalledWith({ + identifier: mockEmail, + identifierType: 'email', + }); + expect(userRepository.save).toHaveBeenCalledWith({ + ...mockUser, + is_active: true, + attempts_left: 5, + time_left: 30 * 60 * 1000, + }); + }); + }); +}); diff --git a/src/modules/user/tests/user.service.spec.ts b/src/modules/user/tests/user.service.spec.ts index 979103a1c..89d39ccd0 100644 --- a/src/modules/user/tests/user.service.spec.ts +++ b/src/modules/user/tests/user.service.spec.ts @@ -11,6 +11,11 @@ import { UserPayload } from '../interfaces/user-payload.interface'; import CreateNewUserOptions from '../options/CreateNewUserOptions'; import UserIdentifierOptionsType from '../options/UserIdentifierOptions'; import UserService from '../user.service'; +import { mockUser } from './mocks/user.mock'; +import exp from 'constants'; +import { PassThrough } from 'stream'; +import { Response } from 'express'; +import { FileFormat } from '../dto/user-data-export.dto'; describe('UserService', () => { let service: UserService; @@ -21,6 +26,14 @@ describe('UserService', () => { save: jest.fn(), findOne: jest.fn(), findAndCount: jest.fn(), + count: jest.fn(), + softDelete: jest.fn(), + }; + + const mockResponse: Partial = { + setHeader: jest.fn().mockReturnThis(), + pipe: jest.fn(), + end: jest.fn(), }; beforeEach(async () => { @@ -81,7 +94,7 @@ describe('UserService', () => { expect(result).toEqual(userResponseDto); expect(repository.findOne).toHaveBeenCalledWith({ where: { email }, - relations: ['profile', 'organisationMembers', 'created_organisations', 'owned_organisations'], + relations: ['profile', 'owned_organisations'], }); }); @@ -100,7 +113,7 @@ describe('UserService', () => { expect(result).toEqual(userResponseDto); expect(repository.findOne).toHaveBeenCalledWith({ where: { id }, - relations: ['profile', 'organisationMembers', 'created_organisations', 'owned_organisations'], + relations: ['profile', 'owned_organisations'], }); }); @@ -127,49 +140,56 @@ describe('UserService', () => { first_name: 'John', last_name: 'Doe', phone_number: '0987654321', - user_type: UserType.USER, }; + const updatedUserStatusResponse = { + id: '4a3731d6-8dfd-42b1-b572-96c7805f7586', + created_at: '2024-08-05T19:16:57.264Z', + updated_at: '2024-08-05T19:43:25.073Z', + first_name: 'John', + last_name: 'Smith', + email: 'john.smith@example.com', + status: 'Hello there! This is what my updated status looks like!', + }; + const updatedUser = { ...existingUser, ...updateOptions }; const superAdminPayload: UserPayload = { id: 'super-admin-id', email: 'superadmin@example.com', - user_type: UserType.SUPER_ADMIN, }; const regularUserPayload: UserPayload = { id: userId, email: 'user@example.com', - user_type: UserType.USER, }; const anotherUserPayload: UserPayload = { id: 'another-user-id', email: 'anotheruser@example.com', - user_type: UserType.USER, }; - it('should allow super admin to update any user', async () => { - mockUserRepository.findOne.mockResolvedValueOnce(existingUser); - mockUserRepository.save.mockResolvedValueOnce(updatedUser); - - const result = await service.updateUser(userId, updateOptions, superAdminPayload); - - expect(result).toEqual({ - status: 'success', - message: 'User Updated Successfully', - user: { - id: userId, - name: 'Jane Doe', - phone_number: '1234567890', - }, - }); - expect(mockUserRepository.findOne).toHaveBeenCalledWith({ - where: { id: userId }, - relations: ['profile', 'organisationMembers', 'created_organisations', 'owned_organisations'], - }); - expect(mockUserRepository.save).toHaveBeenCalledWith(updatedUser); - }); + // it('should allow super admin to update any user', async () => { + // mockUserRepository.findOne.mockResolvedValueOnce(existingUser); + // mockUserRepository.save.mockResolvedValueOnce(updatedUser); + + // const result = await service.updateUser(userId, updateOptions, superAdminPayload); + + // expect(result).toEqual({ + // status: 'success', + // message: 'User Updated Successfully', + // user: { + // id: userId, + // name: 'Jane Doe', + // phone_number: '1234567890', + // }, + // }); + + // expect(mockUserRepository.findOne).toHaveBeenCalledWith({ + // where: { id: userId }, + // relations: ['profile', 'owned_organisations'], + // }); + // expect(mockUserRepository.save).toHaveBeenCalledWith(updatedUser); + // }); it('should allow user to update their own details', async () => { mockUserRepository.findOne.mockResolvedValueOnce(existingUser); @@ -188,7 +208,7 @@ describe('UserService', () => { }); expect(mockUserRepository.findOne).toHaveBeenCalledWith({ where: { id: userId }, - relations: ['profile', 'organisationMembers', 'created_organisations', 'owned_organisations'], + relations: ['profile', 'owned_organisations'], }); expect(mockUserRepository.save).toHaveBeenCalledWith(updatedUser); }); @@ -197,13 +217,21 @@ describe('UserService', () => { mockUserRepository.findOne.mockResolvedValueOnce(existingUser); await expect(service.updateUser(userId, updateOptions, anotherUserPayload)).rejects.toThrow(ForbiddenException); + expect(mockUserRepository.findOne).toHaveBeenCalledWith({ where: { id: userId }, - relations: ['profile', 'organisationMembers', 'created_organisations', 'owned_organisations'], + relations: ['profile', 'owned_organisations'], }); expect(mockUserRepository.save).not.toHaveBeenCalled(); }); + it('should update the user status successfully (super admin)', async () => { + mockUserRepository.findOne.mockResolvedValueOnce(existingUser); + mockUserRepository.save.mockResolvedValueOnce(updatedUserStatusResponse); + const result = await service.updateUserStatus(userId, 'Hello there! This is what my new status looks like!'); + expect(result.data).toEqual(updatedUserStatusResponse); + }); + it('should throw NotFoundException for invalid userId', async () => { const invalidUserId = 'invalid-id'; mockUserRepository.findOne.mockResolvedValueOnce(null); @@ -213,7 +241,7 @@ describe('UserService', () => { ); expect(mockUserRepository.findOne).toHaveBeenCalledWith({ where: { id: invalidUserId }, - relations: ['profile', 'organisationMembers', 'created_organisations', 'owned_organisations'], + relations: ['profile', 'owned_organisations'], }); }); @@ -230,12 +258,12 @@ describe('UserService', () => { mockUserRepository.findOne.mockResolvedValueOnce(existingUser); mockUserRepository.save.mockRejectedValueOnce(new Error('Invalid field')); - await expect(service.updateUser(userId, invalidUpdateOptions, superAdminPayload)).rejects.toThrow( + await expect(service.updateUser(userId, invalidUpdateOptions, regularUserPayload)).rejects.toThrow( BadRequestException ); expect(mockUserRepository.findOne).toHaveBeenCalledWith({ where: { id: userId }, - relations: ['profile', 'organisationMembers', 'created_organisations', 'owned_organisations'], + relations: ['profile', 'owned_organisations'], }); expect(mockUserRepository.save).toHaveBeenCalled(); }); @@ -267,7 +295,7 @@ describe('UserService', () => { expect(result.message).toBe('Account Deactivated Successfully'); expect(mockUserRepository.findOne).toHaveBeenCalledWith({ where: { id: userId }, - relations: ['profile', 'organisationMembers', 'created_organisations', 'owned_organisations'], + relations: ['profile', 'owned_organisations'], }); expect(mockUserRepository.save).toHaveBeenCalledWith({ ...userToUpdate, is_active: false }); }); @@ -288,7 +316,7 @@ describe('UserService', () => { }); expect(mockUserRepository.findOne).toHaveBeenCalledWith({ where: { id: userId }, - relations: ['profile', 'organisationMembers', 'created_organisations', 'owned_organisations'], + relations: ['profile', 'owned_organisations'], }); expect(mockUserRepository.save).not.toHaveBeenCalled(); }); @@ -313,7 +341,7 @@ describe('UserService', () => { expect(result.user).not.toHaveProperty('password'); expect(mockUserRepository.findOne).toHaveBeenCalledWith({ where: { id: userId }, - relations: ['profile', 'organisationMembers', 'created_organisations', 'owned_organisations'], + relations: ['profile', 'owned_organisations'], }); }); }); @@ -324,12 +352,10 @@ describe('UserService', () => { const superAdminPayload: UserPayload = { id: 'super-admin-id', email: 'superadmin@example.com', - user_type: UserType.SUPER_ADMIN, }; const regularUserPayload: UserPayload = { id: 'regular-user-id', email: 'user@example.com', - user_type: UserType.USER, }; it('should return users when called by super admin', async () => { @@ -386,11 +412,6 @@ describe('UserService', () => { }); }); - it('should throw ForbiddenException when called by non-super admin', async () => { - await expect(service.getUsersByAdmin(page, limit, regularUserPayload)).rejects.toThrow(ForbiddenException); - expect(mockUserRepository.findAndCount).not.toHaveBeenCalled(); - }); - it('should handle pagination correctly', async () => { const users = Array(15) .fill(null) @@ -443,5 +464,202 @@ describe('UserService', () => { }, }); }); + describe('getUserStats', () => { + it('should return user statistics for active status', async () => { + const totalUsers = 100; + const activeUsers = 70; + const deletedUsers = 30; + + mockUserRepository.count + .mockResolvedValueOnce(totalUsers) + .mockResolvedValueOnce(activeUsers) + .mockResolvedValueOnce(deletedUsers); + + const result = await service.getUserStats('active'); + + expect(result).toEqual({ + status: 'success', + status_code: 200, + message: 'Request completed successfully', + data: { + total_users: totalUsers, + active_users: activeUsers, + deleted_users: deletedUsers, + }, + }); + expect(mockUserRepository.count).toHaveBeenCalledTimes(3); + }); + + it('should return user statistics for deleted status', async () => { + const totalUsers = 100; + const activeUsers = 40; + const deletedUsers = 60; + + mockUserRepository.count + .mockResolvedValueOnce(totalUsers) + .mockResolvedValueOnce(activeUsers) + .mockResolvedValueOnce(deletedUsers); + + const result = await service.getUserStats('deleted'); + + expect(result).toEqual({ + status: 'success', + status_code: 200, + message: 'Request completed successfully', + data: { + total_users: totalUsers, + active_users: activeUsers, + deleted_users: deletedUsers, + }, + }); + expect(mockUserRepository.count).toHaveBeenCalledTimes(3); + }); + + it('should throw BadRequestException for invalid status', async () => { + await expect(service.getUserStats('unknown')).rejects.toThrow(BadRequestException); + expect(mockUserRepository.count).not.toHaveBeenCalled(); + }); + + it('should return user statistics without status', async () => { + const totalUsers = 100; + const activeUsers = 70; + const deletedUsers = 30; + + mockUserRepository.count + .mockResolvedValueOnce(totalUsers) + .mockResolvedValueOnce(activeUsers) + .mockResolvedValueOnce(deletedUsers); + + const result = await service.getUserStats(); + + expect(result).toEqual({ + status: 'success', + status_code: 200, + message: 'Request completed successfully', + data: { + total_users: totalUsers, + active_users: activeUsers, + deleted_users: deletedUsers, + }, + }); + expect(mockUserRepository.count).toHaveBeenCalledTimes(3); + }); + }); + }); + + describe('userDataExport', () => { + it('should return JSON data when the requested format is JSON', async () => { + const mockStream = new PassThrough(); + const result: Buffer[] = []; // Use Buffer[] to handle binary data chunks + + const mockJsonData = { user: { id: 'mockUserId', first_name: 'John', last_name: 'Doe' } }; + + mockUserRepository.findOne.mockResolvedValue(mockJsonData.user); + + const streamableFile = await service.exportUserDataAsJsonOrExcelFile( + 'json' as FileFormat.JSON, + mockJsonData.user.id, + mockResponse as Response + ); + + streamableFile.getStream().pipe(mockStream); + + mockStream.on('data', chunk => { + result.push(chunk); + }); + + await new Promise((resolve, reject) => { + mockStream.on('end', () => { + try { + const parsedResult = JSON.parse(Buffer.concat(result).toString()); + expect(parsedResult).toEqual(mockJsonData); + resolve(); + } catch (error) { + reject(error); + } + }); + + mockStream.on('error', err => reject(err)); + }); + + expect(mockResponse.setHeader).toHaveBeenNthCalledWith( + 1, + 'Content-Disposition', + `attachment; filename="${mockJsonData.user.id}-data.json"` + ); + + expect(mockResponse.setHeader).toHaveBeenNthCalledWith(2, 'Content-Type', 'application/json'); + }); + }); + describe('softDeleteUser', () => { + it('should soft delete a user', async () => { + const userId = '1'; + const authenticatedUserId = '1'; + const userToDelete = { + id: '1', + first_name: 'John', + last_name: 'Doe', + email: 'test@example.com', + password: 'hashedpassword', + is_active: true, + attempts_left: 3, + time_left: 60, + }; + + mockUserRepository.findOne.mockResolvedValueOnce(userToDelete); + mockUserRepository.softDelete.mockResolvedValueOnce({ affected: 1 }); + mockUserRepository.softDelete.mockResolvedValueOnce({ affected: 1 }); + + const result = await service.softDeleteUser(userId, authenticatedUserId); + + expect(result.status).toBe('success'); + expect(result.message).toBe('Deletion in progress'); + expect(mockUserRepository.findOne).toHaveBeenCalledWith({ + where: { id: userId }, + }); + expect(mockUserRepository.softDelete).toHaveBeenCalledWith(userId); + }); + + it('should throw an error if user is not found', async () => { + const userId = '1'; + const authenticatedUserId = '1'; + + mockUserRepository.findOne.mockResolvedValueOnce(null); + + await expect(service.softDeleteUser(userId, authenticatedUserId)).rejects.toHaveProperty( + 'response', + 'User not found' + ); + expect(mockUserRepository.findOne).toHaveBeenCalledWith({ + where: { id: userId }, + }); + expect(mockUserRepository.softDelete).not.toHaveBeenCalled(); + }); + + it('should throw an error if the user is not authorized to delete the user', async () => { + const userId = '1'; + const authenticatedUserId = '2'; + const userToDelete = { + id: '1', + first_name: 'John', + last_name: 'Doe', + email: 'test@example.com', + password: 'hashedpassword', + is_active: true, + attempts_left: 3, + time_left: 60, + }; + + mockUserRepository.findOne.mockResolvedValueOnce(userToDelete); + + await expect(service.softDeleteUser(userId, authenticatedUserId)).rejects.toHaveProperty( + 'response', + 'You are not authorized to delete this user' + ); + expect(mockUserRepository.findOne).toHaveBeenCalledWith({ + where: { id: userId }, + }); + expect(mockUserRepository.softDelete).not.toHaveBeenCalled(); + }); }); }); diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index ff4a5d252..4cb2bbff5 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -1,9 +1,44 @@ -import { Body, Controller, Get, Param, Patch, Query, Req, Request } from '@nestjs/common'; -import { ApiBearerAuth, ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { + Body, + Controller, + Get, + Param, + ParseUUIDPipe, + Patch, + Query, + Req, + Request, + UseGuards, + Res, + StreamableFile, + Header, + ParseEnumPipe, + Delete, +} from '@nestjs/common'; +import { + ApiBearerAuth, + ApiForbiddenResponse, + ApiInternalServerErrorResponse, + ApiOkResponse, + ApiOperation, + ApiQuery, + ApiResponse, + ApiTags, + ApiUnauthorizedResponse, +} from '@nestjs/swagger'; import { DeactivateAccountDto } from './dto/deactivate-account.dto'; import { UpdateUserDto } from './dto/update-user-dto'; import { UserPayload } from './interfaces/user-payload.interface'; import UserService from './user.service'; +import { SuperAdminGuard } from '../../guards/super-admin.guard'; +import { ReactivateAccountDto } from './dto/reactivate-account.dto'; +import { UpdateUserStatusDto } from './dto/update-user-status.dto'; +import { UpdateUserStatusResponseDto } from './dto/update-user-status-response.dto'; +import { GetUserStatsResponseDto } from './dto/get-user-stats-response.dto'; +import { skipAuth } from '../../helpers/skipAuth'; +import { Response } from 'express'; +import * as path from 'path'; +import { UserDataExportDto } from './dto/user-data-export.dto'; @ApiBearerAuth() @ApiTags('Users') @@ -11,7 +46,7 @@ import UserService from './user.service'; export class UserController { constructor(private readonly userService: UserService) {} - @Patch('/deactivate') + @Patch('deactivate') @ApiBearerAuth() @ApiOperation({ summary: 'Deactivate a user account' }) @ApiResponse({ status: 200, description: 'The account has been successfully deactivated.' }) @@ -25,6 +60,38 @@ export class UserController { return this.userService.deactivateUser(userId, deactivateAccountDto); } + @Patch('/reactivate') + @ApiBearerAuth() + @ApiOperation({ summary: 'Reactivate a user account' }) + @ApiResponse({ status: 200, description: 'The account has been successfully reactivated.' }) + @ApiResponse({ status: 400, description: 'Bad Request.' }) + @ApiResponse({ status: 401, description: 'Unauthorized.' }) + @ApiResponse({ status: 500, description: 'Internal Server Error.' }) + async reactivateAccount(@Body() reactivateAccountDto: ReactivateAccountDto) { + const { email } = reactivateAccountDto; + + return this.userService.reactivateUser(email, reactivateAccountDto); + } + + @Get('stats') + @ApiOperation({ summary: 'Get user statistics (Super Admin only)' }) + @ApiResponse({ + status: 200, + description: 'User statistics retrieved successfully', + type: GetUserStatsResponseDto, + }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + @ApiQuery({ + name: 'status', + required: false, + enum: ['active', 'inactive', 'deleted'], + description: 'Filter users by status', + }) + @UseGuards(SuperAdminGuard) + async getUserStats(@Query('status') status?: string): Promise { + return this.userService.getUserStats(status); + } + @ApiOperation({ summary: 'Update User' }) @ApiResponse({ status: 200, @@ -40,6 +107,45 @@ export class UserController { return this.userService.updateUser(userId, updatedUserDto, req.user); } + @ApiQuery({ + name: 'format', + description: 'The format in which the user data should be exported (e.g., JSON, XLSX)', + enum: ['json', 'xlsx'], + required: true, + }) + @ApiResponse({ + status: 200, + description: 'Returns the user data in the requested format.', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + user: { + type: 'object', + description: 'User data object', + }, + }, + }, + }, + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': { + schema: { + type: 'string', + format: 'binary', + }, + }, + }, + }) + @Get('export') + async exportUserData( + @Query() { format }: UserDataExportDto, + @Res({ passthrough: false }) res: Response, + @Req() { user } + ) { + const file = await this.userService.exportUserDataAsJsonOrExcelFile(format, user.id, res); + file.getStream().pipe(res); + } + @ApiBearerAuth() @ApiOperation({ summary: 'Get User Data' }) @ApiResponse({ status: 200, description: 'User data fetched successfully' }) @@ -50,6 +156,7 @@ export class UserController { return this.userService.getUserDataWithoutPasswordById(id); } + @UseGuards(SuperAdminGuard) @Get() @ApiBearerAuth() @ApiOperation({ summary: 'Get all users (Super Admin only)' }) @@ -65,4 +172,41 @@ export class UserController { ) { return this.userService.getUsersByAdmin(page, limit, req.user); } + + @Patch(':userId/status') + @ApiOperation({ summary: 'Update a user status (Super Admin only)' }) + @ApiOkResponse({ description: 'Status updated successfully', type: UpdateUserStatusResponseDto }) + @ApiUnauthorizedResponse({ + description: 'User is not authorized', + type: 'object', + example: { + message: 'User is currently unauthorized, kindly authenticate to continue', + status: 401, + }, + }) + @ApiForbiddenResponse({ + description: 'User is forbidden', + example: { + message: 'You dont have the permission to perform this action', + status: 403, + }, + }) + @ApiInternalServerErrorResponse({ description: 'Internal Server Error' }) + @UseGuards(SuperAdminGuard) + async updateUserStatus(@Param('userId', ParseUUIDPipe) userId: string, @Body() { status }: UpdateUserStatusDto) { + return this.userService.updateUserStatus(userId, status); + } + + @Delete(':userId') + @ApiBearerAuth() + @ApiOperation({ summary: 'Soft delete a user account' }) + @ApiResponse({ status: 204, description: 'Deletion in progress' }) + @ApiResponse({ status: 400, description: 'Bad Request' }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + @ApiResponse({ status: 500, description: 'Internal Server Error' }) + async softDeleteUser(@Param('userId', ParseUUIDPipe) userId: string, @Req() req) { + const authenticatedUserId = req['user'].id; + + return this.userService.softDeleteUser(userId, authenticatedUserId); + } } diff --git a/src/modules/user/user.module.ts b/src/modules/user/user.module.ts index e7db45bb4..5551b2977 100644 --- a/src/modules/user/user.module.ts +++ b/src/modules/user/user.module.ts @@ -5,11 +5,14 @@ import { Repository } from 'typeorm'; import UserService from './user.service'; import { UserController } from './user.controller'; import { Profile } from '../profile/entities/profile.entity'; +import { Organisation } from '../organisations/entities/organisations.entity'; +import { OrganisationUserRole } from '../role/entities/organisation-user-role.entity'; +import { Role } from '../role/entities/role.entity'; @Module({ controllers: [UserController], providers: [UserService, Repository], - imports: [TypeOrmModule.forFeature([User, Profile])], + imports: [TypeOrmModule.forFeature([User, Profile, Organisation, OrganisationUserRole, Role])], exports: [UserService], }) export class UserModule {} diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 1e01fbe77..cc51c6698 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -5,6 +5,7 @@ import { HttpStatus, Injectable, NotFoundException, + StreamableFile, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; @@ -18,6 +19,16 @@ import { UserPayload } from './interfaces/user-payload.interface'; import CreateNewUserOptions from './options/CreateNewUserOptions'; import UpdateUserRecordOption from './options/UpdateUserRecordOption'; import UserIdentifierOptionsType from './options/UserIdentifierOptions'; +import { ReactivateAccountDto } from './dto/reactivate-account.dto'; +import { pick } from '../../helpers/pick'; +import { GetUserStatsResponseDto } from './dto/get-user-stats-response.dto'; +import * as SYS_MSG from '../../helpers/SystemMessages'; +import { Readable, Writable } from 'stream'; +import * as xlsx from 'xlsx'; +import * as path from 'path'; +import { Response } from 'express'; +import { FileFormat, UserDataExportDto } from './dto/user-data-export.dto'; +import { CustomHttpException } from '../../helpers/custom-http-filter'; @Injectable() export default class UserService { @@ -33,11 +44,6 @@ export default class UserService { const newUser = new User(); Object.assign(newUser, createUserPayload); newUser.is_active = true; - if (createUserPayload.admin_secret == process.env.ADMIN_SECRET_KEY) { - newUser.user_type = UserType.SUPER_ADMIN; - } else { - newUser.user_type = UserType.USER; - } newUser.profile = profile; return await this.userRepository.save(newUser); } @@ -76,7 +82,7 @@ export default class UserService { private async getUserByEmail(email: string) { const user: UserResponseDTO = await this.userRepository.findOne({ where: { email: email }, - relations: ['profile', 'organisationMembers', 'created_organisations', 'owned_organisations'], + relations: ['profile', 'owned_organisations'], }); return user; } @@ -84,7 +90,7 @@ export default class UserService { private async getUserById(identifier: string) { const user: UserResponseDTO = await this.userRepository.findOne({ where: { id: identifier }, - relations: ['profile', 'organisationMembers', 'created_organisations', 'owned_organisations'], + relations: ['profile', 'owned_organisations'], }); return user; } @@ -125,9 +131,8 @@ export default class UserService { status_code: HttpStatus.NOT_FOUND, }); } - - // Check if the current user is a super admin or the user being updated - if (currentUser.user_type !== UserType.SUPER_ADMIN && currentUser.id !== userId) { + // TODO: CHECK IF USER IS AN ADMIN + if (currentUser.id !== userId) { throw new ForbiddenException({ error: 'Forbidden', message: 'You are not authorized to update this user', @@ -193,15 +198,36 @@ export default class UserService { return { is_active: user.is_active, message: 'Account Deactivated Successfully' }; } - async getUsersByAdmin(page: number = 1, limit: number = 10, currentUser: UserPayload): Promise { - if (currentUser.user_type !== UserType.SUPER_ADMIN) { - throw new ForbiddenException({ - error: 'Forbidden', - message: 'Only super admins can access this endpoint', - status_code: HttpStatus.FORBIDDEN, - }); + async reactivateUser(email: string, reactivateAccountDto: ReactivateAccountDto) { + const identifierOptions: UserIdentifierOptionsType = { + identifier: email, + identifierType: 'email', + }; + + const user = await this.getUserRecord(identifierOptions); + + if (!user) { + throw new CustomHttpException('User not found', HttpStatus.NOT_FOUND); } + user.is_active = true; + user.attempts_left = 5; + user.time_left = 30 * 60 * 1000; + + await this.userRepository.save(user); + + return { + status: 'success', + message: 'User Reactivated Successfully', + user: { + id: user.id, + name: `${user.first_name} ${user.last_name}`, + phone_number: user.phone_number, + }, + }; + } + + async getUsersByAdmin(page: number = 1, limit: number = 10, currentUser: UserPayload): Promise { const [users, total] = await this.userRepository.findAndCount({ select: ['id', 'first_name', 'last_name', 'email', 'phone', 'is_active', 'created_at'], skip: (page - 1) * limit, @@ -233,4 +259,156 @@ export default class UserService { }, }; } + + async softDeleteUser(userId: string, authenticatedUserId: string): Promise { + const user = await this.userRepository.findOne({ + where: { id: userId }, + }); + + if (!user) { + throw new CustomHttpException('User not found', HttpStatus.NOT_FOUND); + } + + if (user.id !== authenticatedUserId) { + throw new CustomHttpException('You are not authorized to delete this user', HttpStatus.UNAUTHORIZED); + } + + await this.userRepository.softDelete(userId); + + return { + status: 'success', + message: 'Deletion in progress', + }; + } + + // async getUserStatistics(currentUser: UserPayload): Promise { + // if (currentUser.user_type !== UserType.SUPER_ADMIN) { + // throw new ForbiddenException({ + // error: 'Forbidden', + // message: 'You are not authorized to access user statistics', + // }); + // } + async updateUserStatus(userId: string, status: string) { + const keepColumns = ['id', 'created_at', 'updated_at', 'first_name', 'last_name', 'email', 'status']; + const user = await this.userRepository.findOne({ + where: { id: userId }, + }); + if (!user) { + throw new NotFoundException({ + error: 'Not Found', + message: 'User not found', + status_code: HttpStatus.NOT_FOUND, + }); + } + const updatedUser = Object.assign(user, { status }); + const result = await this.userRepository.save(updatedUser); + + return { + status: 'success', + status_code: HttpStatus.OK, + data: pick(result, keepColumns), + }; + } + + async getUserStats(status?: string): Promise { + const filters = {}; + + if (status) { + if (status === 'active') { + filters['is_active'] = true; + } else if (status === 'deleted') { + filters['is_active'] = false; + } else { + throw new BadRequestException({ + error: 'Bad Request', + message: SYS_MSG.BAD_REQUEST, + status_code: HttpStatus.BAD_REQUEST, + }); + } + } + + const totalUsers = await this.userRepository.count(); + + const activeUsers = status + ? await this.userRepository.count({ where: { ...filters, is_active: true } }) + : await this.userRepository.count({ where: { is_active: true } }); + + const deletedUsers = status + ? await this.userRepository.count({ where: { ...filters, is_active: false } }) + : await this.userRepository.count({ where: { is_active: false } }); + + return { + status: 'success', + status_code: 200, + message: SYS_MSG.REQUEST_SUCCESSFUL, + data: { + total_users: totalUsers, + active_users: activeUsers, + deleted_users: deletedUsers, + }, + }; + } + + async exportUserDataAsJsonOrExcelFile(format: FileFormat, userId: string, res: Response) { + const stream = new Readable(); + const jsonData = { user: {} }; + const omitColumns: Array = ['password']; + const relations = [ + 'profile', + 'organisationMembers', + 'created_organisations', + 'owned_organisations', + 'blogs', + 'notifications', + 'testimonials', + ]; + const user = await this.userRepository.findOne({ + where: { id: userId }, + relations, + }); + + for (const i in user) { + if (omitColumns.includes(i as keyof User)) delete user[i]; + if (!relations.includes(i)) jsonData.user[i] = user[i]; + else jsonData[i] = user[i]; + } + + if (format === 'xlsx') { + res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + res.setHeader('Content-Disposition', `attachment; filename="${userId}-data.xlsx"`); + stream.push(this.generateExcelExportFile(jsonData)); + stream.push(null); + } else if (format === 'json') { + res.setHeader('Content-Disposition', `attachment; filename="${userId}-data.json"`); + res.setHeader('Content-Type', 'application/json'); + stream.push(JSON.stringify(jsonData)); + stream.push(null); + } + + const result = new StreamableFile(stream); + return result; + } + + private generateExcelExportFile(jsonData): Buffer { + const workbook = xlsx.utils.book_new(); + + function generateColumnsAndContents(data: object[], columnName: string) { + const worksheet = xlsx.utils.json_to_sheet(data); + xlsx.utils.book_append_sheet(workbook, worksheet, columnName); + } + + for (const i in jsonData) { + if (jsonData[i] === null) { + generateColumnsAndContents([{ no: null, data: null, found: null }], i); + } else if (!Array.isArray(jsonData[i])) { + generateColumnsAndContents([jsonData[i]], i); + } else if (Array.isArray(jsonData[i])) { + if (jsonData[i].length === 0) { + jsonData[i][0] = { no: null, data: null, found: null }; + } + jsonData[i].forEach(entry => generateColumnsAndContents([entry], i)); + } + } + return xlsx.write(workbook, { bookType: 'xlsx', type: 'buffer' }); + } } diff --git a/src/modules/waitlist/docs/waitlist-swagger.docs.ts b/src/modules/waitlist/docs/waitlist-swagger.docs.ts new file mode 100644 index 000000000..97fa4f6a9 --- /dev/null +++ b/src/modules/waitlist/docs/waitlist-swagger.docs.ts @@ -0,0 +1,38 @@ +import { applyDecorators, HttpStatus, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { GetWaitlistResponseDto } from '../dto/get-waitlist.dto'; +import { ErrorResponseDto } from '../dto/waitlist-error-response.dto'; +import { WaitlistResponseDto } from '../dto/create-waitlist-response.dto'; + +export function createWaitlistDocs() { + return applyDecorators( + ApiOperation({ summary: 'Create a new waitlist entry' }), + ApiResponse({ + status: HttpStatus.CREATED, + description: 'Successfully created a waitlist entry.', + type: WaitlistResponseDto, + }), + ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'Invalid input data.', + type: ErrorResponseDto, + }) + ); +} + +export function getAllWaitlistDocs() { + return applyDecorators( + ApiBearerAuth(), + ApiOperation({ summary: 'Get all waitlist entries' }), + ApiResponse({ + status: HttpStatus.OK, + description: 'Successfully retrieved all waitlist entries.', + type: GetWaitlistResponseDto, + }), + ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: 'Internal server error.', + type: ErrorResponseDto, + }) + ); +} diff --git a/src/modules/waitlist/dto/create-waitlist-response.dto.ts b/src/modules/waitlist/dto/create-waitlist-response.dto.ts new file mode 100644 index 000000000..639f27441 --- /dev/null +++ b/src/modules/waitlist/dto/create-waitlist-response.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +export class WaitlistResponseDto { + @ApiProperty({ + description: 'Success message indicating the user is signed up.', + example: 'You are all signed up!', + }) + @IsString() + message: string; +} diff --git a/src/modules/waitlist/dto/create-waitlist.dto.ts b/src/modules/waitlist/dto/create-waitlist.dto.ts new file mode 100644 index 000000000..abc6d2e40 --- /dev/null +++ b/src/modules/waitlist/dto/create-waitlist.dto.ts @@ -0,0 +1,11 @@ +import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; + +export class CreateWaitlistDto { + @IsNotEmpty({ message: 'Name is required' }) + @IsString({ message: 'Name must be a string' }) + full_name: string; + + @IsNotEmpty({ message: 'Email is required' }) + @IsEmail({}, { message: 'Invalid email format' }) + email: string; +} diff --git a/src/modules/waitlist/dto/get-waitlist.dto.ts b/src/modules/waitlist/dto/get-waitlist.dto.ts index bd25283bb..a06cc06c0 100644 --- a/src/modules/waitlist/dto/get-waitlist.dto.ts +++ b/src/modules/waitlist/dto/get-waitlist.dto.ts @@ -1,9 +1,18 @@ +import { ApiProperty } from '@nestjs/swagger'; import { Waitlist } from '../entities/waitlist.entity'; +import { HttpStatus } from '@nestjs/common'; export class GetWaitlistResponseDto { - status: number; - status_code: number; + @ApiProperty({ + description: 'Success message indicating the result of the operation.', + example: 'Waitlist found successfully', + }) message: string; + + @ApiProperty({ + description: 'Data containing the waitlist entries.', + type: [Waitlist], + }) data: { waitlist: Waitlist[]; }; diff --git a/src/modules/waitlist/dto/waitlist-error-response.dto.ts b/src/modules/waitlist/dto/waitlist-error-response.dto.ts new file mode 100644 index 000000000..dc25f9b9c --- /dev/null +++ b/src/modules/waitlist/dto/waitlist-error-response.dto.ts @@ -0,0 +1,23 @@ +import { HttpStatus } from '@nestjs/common'; +import { ApiProperty } from '@nestjs/swagger'; + +export class ErrorResponseDto { + @ApiProperty({ + description: 'HTTP status code of the error response.', + example: HttpStatus.BAD_REQUEST, + }) + status_code: number; + + @ApiProperty({ + description: 'Error message(s) describing the issue. Can be a single string or an array of strings.', + example: ['Name should not be empty', 'Email must be an email'], + oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], + }) + message: string | string[]; + + @ApiProperty({ + description: 'Error type.', + example: 'Bad Request', + }) + error: string; +} diff --git a/src/modules/waitlist/entities/waitlist.entity.ts b/src/modules/waitlist/entities/waitlist.entity.ts index d4a08d70a..af93c7a9a 100644 --- a/src/modules/waitlist/entities/waitlist.entity.ts +++ b/src/modules/waitlist/entities/waitlist.entity.ts @@ -9,9 +9,6 @@ export class Waitlist extends AbstractBaseEntity { @Column({ nullable: false, unique: true }) email: string; - @Column({ nullable: false, default: false }) + @Column({ default: false }) status: boolean; - - @Column({ nullable: true }) - url_slug: string; } diff --git a/src/modules/waitlist/tests/waitlist.service.spec.ts b/src/modules/waitlist/tests/waitlist.service.spec.ts index f14c5879a..f0a2647d5 100644 --- a/src/modules/waitlist/tests/waitlist.service.spec.ts +++ b/src/modules/waitlist/tests/waitlist.service.spec.ts @@ -1,30 +1,52 @@ import { Repository } from 'typeorm'; -import WaitlistService from '../waitlist.service'; import { Waitlist } from '../entities/waitlist.entity'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; +import { MailerService } from '@nestjs-modules/mailer'; +import { CreateWaitlistDto } from '../dto/create-waitlist.dto'; +import { WaitlistResponseDto } from '../dto/create-waitlist-response.dto'; +import WaitlistService from '../waitlist.service'; +import { HttpStatus } from '@nestjs/common'; describe('WaitlistService', () => { - let waitlistService: WaitlistService; let waitlistRepository: Repository; + let mailerService: MailerService; + let waitlistService: WaitlistService; - const mockUserRepository = { + const mockWaitlistRepository = { find: jest.fn(), + save: jest.fn(), + create: jest.fn(), + findOne: jest.fn(), }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [WaitlistService, { provide: getRepositoryToken(Waitlist), useValue: mockUserRepository }], + providers: [ + WaitlistService, + { provide: getRepositoryToken(Waitlist), useValue: mockWaitlistRepository }, + { + provide: MailerService, + useValue: { + sendMail: jest.fn(), + }, + }, + ], }).compile(); waitlistService = module.get(WaitlistService); waitlistRepository = module.get>(getRepositoryToken(Waitlist)); + mailerService = module.get(MailerService); }); afterEach(() => { jest.clearAllMocks(); }); + it('should be defined', () => { + expect(waitlistService).toBeDefined(); + }); + describe('getAllWaitlist', () => { it('should return all waitlist', async () => { await waitlistService.getAllWaitlist(); @@ -32,4 +54,45 @@ describe('WaitlistService', () => { expect(waitlistRepository.find).toHaveBeenCalled(); }); }); + + describe('createWaitlist', () => { + it('should create a waitlist entry and send a confirmation email', async () => { + const createWaitlistDto: CreateWaitlistDto = { + full_name: 'John Doe', + email: 'johndoe@gmail.com', + }; + + const findOneSpy = jest.spyOn(waitlistRepository, 'findOne').mockResolvedValue(null); + const saveSpy = jest.spyOn(waitlistRepository, 'save').mockResolvedValue(undefined); + const sendMailSpy = jest.spyOn(mailerService, 'sendMail').mockResolvedValue(undefined); + + const result: WaitlistResponseDto = await waitlistService.createWaitlist(createWaitlistDto); + + expect(findOneSpy).toHaveBeenCalledWith({ where: { email: createWaitlistDto.email } }); + expect(saveSpy).toHaveBeenCalled(); + expect(sendMailSpy).toHaveBeenCalledWith({ + to: createWaitlistDto.email, + subject: 'Waitlist Confirmation', + template: 'waitlist-confirmation', + context: { recipientName: createWaitlistDto.full_name }, + }); + expect(result).toEqual({ message: 'You are all signed up!' }); + }); + + it('should return 400 Bad Request for invalid data', async () => { + const createWaitlistDto: CreateWaitlistDto = { + full_name: '', + email: 'invalid-email', + }; + + try { + await waitlistService.createWaitlist(createWaitlistDto); + } catch (e) { + expect(e.response).toEqual({ + status_code: HttpStatus.BAD_REQUEST, + message: ['Name should not be empty', 'Email must be an email'], + }); + } + }); + }); }); diff --git a/src/modules/waitlist/waitlist.controller.ts b/src/modules/waitlist/waitlist.controller.ts index de921fe86..72dfd35db 100644 --- a/src/modules/waitlist/waitlist.controller.ts +++ b/src/modules/waitlist/waitlist.controller.ts @@ -1,19 +1,31 @@ -import { Controller, Get } from '@nestjs/common'; +import { Body, Controller, Get, HttpCode, HttpStatus, Post, UseGuards } from '@nestjs/common'; import WaitlistService from './waitlist.service'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { CreateWaitlistDto } from './dto/create-waitlist.dto'; +import { WaitlistResponseDto } from './dto/create-waitlist-response.dto'; import { GetWaitlistResponseDto } from './dto/get-waitlist.dto'; +import { ErrorResponseDto } from './dto/waitlist-error-response.dto'; +import { skipAuth } from '../../helpers/skipAuth'; +import { SuperAdminGuard } from '../../guards/super-admin.guard'; +import { createWaitlistDocs, getAllWaitlistDocs } from './docs/waitlist-swagger.docs'; -@ApiBearerAuth() @ApiTags('Waitlist') @Controller('waitlist') export class WaitlistController { constructor(private readonly waitlistService: WaitlistService) {} - @ApiOperation({ summary: 'Get all waitlist' }) - @ApiResponse({ status: 200, description: 'Wait list retrieved successfully' }) - @ApiResponse({ status: 500, description: 'Internal Server Error' }) + @Post() + @skipAuth() + @createWaitlistDocs() + @HttpCode(HttpStatus.CREATED) + async createWaitlist(@Body() createWaitlistDto: CreateWaitlistDto): Promise { + return await this.waitlistService.createWaitlist(createWaitlistDto); + } + @Get() - getAllWaitlist() { + @UseGuards(SuperAdminGuard) + @getAllWaitlistDocs() + async getAllWaitlist(): Promise { return this.waitlistService.getAllWaitlist(); } } diff --git a/src/modules/waitlist/waitlist.module.ts b/src/modules/waitlist/waitlist.module.ts index 25f5d4bda..dca9a3a23 100644 --- a/src/modules/waitlist/waitlist.module.ts +++ b/src/modules/waitlist/waitlist.module.ts @@ -3,10 +3,14 @@ import { WaitlistController } from './waitlist.controller'; import WaitlistService from './waitlist.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Waitlist } from './entities/waitlist.entity'; +import { User } from '../user/entities/user.entity'; +import { OrganisationUserRole } from '../role/entities/organisation-user-role.entity'; +import { Organisation } from '../organisations/entities/organisations.entity'; +import { Role } from '../role/entities/role.entity'; @Module({ controllers: [WaitlistController], providers: [WaitlistService], - imports: [TypeOrmModule.forFeature([Waitlist])], + imports: [TypeOrmModule.forFeature([Waitlist, User, OrganisationUserRole, Organisation, Role])], }) export class WaitlistModule {} diff --git a/src/modules/waitlist/waitlist.service.ts b/src/modules/waitlist/waitlist.service.ts index 70eded699..eb59182c3 100644 --- a/src/modules/waitlist/waitlist.service.ts +++ b/src/modules/waitlist/waitlist.service.ts @@ -2,20 +2,40 @@ import { HttpStatus, Injectable } from '@nestjs/common'; import { Repository } from 'typeorm'; import { Waitlist } from './entities/waitlist.entity'; import { InjectRepository } from '@nestjs/typeorm'; +import { MailerService } from '@nestjs-modules/mailer'; +import { CreateWaitlistDto } from './dto/create-waitlist.dto'; +import { WaitlistResponseDto } from './dto/create-waitlist-response.dto'; +import * as SYS_MSG from '../../helpers/SystemMessages'; +import { CustomHttpException } from '../../helpers/custom-http-filter'; @Injectable() export default class WaitlistService { - constructor(@InjectRepository(Waitlist) private readonly waitlistRepository: Repository) {} + constructor( + @InjectRepository(Waitlist) private readonly waitlistRepository: Repository, + private mailerService: MailerService + ) {} + + async createWaitlist(createWaitlistDto: CreateWaitlistDto): Promise { + const { full_name: name, email } = createWaitlistDto; + + const alreadyWaitlisted = await this.waitlistRepository.findOne({ where: { email } }); + if (alreadyWaitlisted) throw new CustomHttpException(SYS_MSG.USER_ALREADY_WAITLISTED, HttpStatus.CONFLICT); + + const waitlist = this.waitlistRepository.create({ name, email }); + await this.waitlistRepository.save(waitlist); + + await this.mailerService.sendMail({ + to: email, + subject: 'Waitlist Confirmation', + template: 'waitlist-confirmation', + context: { recipientName: name }, + }); + + return { message: 'You are all signed up!' }; + } async getAllWaitlist() { const waitlist = await this.waitlistRepository.find(); - return { - status_code: HttpStatus.OK, - status: HttpStatus.OK, - message: 'Added to waitlist', - data: { - waitlist, - }, - }; + return { message: 'Waitlist found successfully', data: { waitlist } }; } } diff --git a/src/run-tests/python.py b/src/run-tests/python.py index 11231854b..a883ab0f9 100644 --- a/src/run-tests/python.py +++ b/src/run-tests/python.py @@ -1,5 +1,177 @@ import requests import unittest + + +from faker import Faker + + +base_URL = "https://deployment.api-nestjs.boilerplate.hng.tech" + + +class APITest(unittest.TestCase): + def setUp(self): + # Initialize Faker + self.faker= Faker() + self.email = Faker.email + self.token = None + + + # REGISTER USER + def test_register_user(self): + endpoint = "/api/v1/auth/register" + url = base_URL + endpoint + # unique_email = self.fake.email() # Generate a unique email + print(self.email) + payload = { + "email": self.email, + "first_name": "Ria", + "last_name": "Test", + "password": "wretyuTRY#n1@kels" + } + response = requests.post(url, json=payload) + self.assertEqual(response.status_code, 201) + + + # LOGIN + def test_login(self): + endpoint = "/api/v1/auth/login" + url = base_URL + endpoint + payload = { + "password": "Tester@01", + "email": self.email + } + response = requests.post(url, json=payload) + self.token = response.json()["access_token"] + + self.assertEqual(response.status_code, 200) + + # GENERATE TOKEN + @staticmethod + def get_token(): + endpoint = "/api/v1/auth/login" + url = base_URL + endpoint + payload = { + "password": "Tester@01", + "email": APITest().email + } + response = requests.post(url, json=payload) + response_json = response.json() + + + # TEST ROOT + def test_root(self): + endpoint = "/api" + url = base_URL + endpoint + response = requests.get(url) + self.assertEqual(response.status_code, 200) + + # TEST VERSION + def test_version(self): + endpoint = "/api/v1" + url = base_URL + endpoint + response = requests.get(url) + self.assertEqual(response.status_code, 200) + + # TEST HEALTH + def test_health(self): + endpoint = "/health" + url = base_URL + endpoint + response = requests.get(url) + self.assertEqual(response.status_code, 200) + + # TEST PROBE + def test_probe(self): + endpoint = "/probe" + url = base_URL + endpoint + response = requests.get(url) + self.assertEqual(response.status_code, 200) + + # TEST SEED + def test_seed(self): + endpoint = "/api/v1/seed" + url = base_URL + endpoint + response = requests.post(url) + self.assertEqual(response.status_code, 201) + + + def test_squeeze(self): + endpoint = "/api/v1/squeeze" + url = base_URL + endpoint + data = { + "email": self.email, + "first_name": "string", + "last_name": "string", + "phone": "string", + "location": "string", + "job_title": "string", + "company": "string", + "interests": [ + "string" + ], + "referral_source": "string" + } + response = requests.post(url, json=data) + self.assertEqual(response.status_code, 201) + + + def test_get_timezones(self): + endpoint = "/api/v1/timezones" + access_token = self.token + headers = { + "Authorization": f"Bearer {access_token}" + } + url = base_URL + endpoint + response = requests.get(url, headers=headers) + self.assertEqual(response.status_code, 200) + + + def test_get_user(self): + user_id = "8f7ca676-52af-44d0-acc2-d43b68d90467" + endpoint = f"/api/v1/users/{user_id}" + access_token = self.token + headers = { + "Authorization": f"Bearer {access_token}" + } + url = base_URL + endpoint + response = requests.get(url, headers=headers) + + self.assertEqual(response.status_code, 200) + + + def test_update_user(self): + user_id = "8f7ca676-52af-44d0-acc2-d43b68d90467" + endpoint = f"/api/v1/users/{user_id}" + access_token = self.token + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json" + } + url = base_URL + endpoint + data = { + "last_name": "Doe" + } + response = requests.patch(url, json=data, headers=headers) + self.assertEqual(response.status_code, 200) + + + def test_create_testimonial(self): + endpoint = "/api/v1/testimonials" + access_token = self.token + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json" + } + url = base_URL + endpoint + data = { + "name": "John Doe", + "content": "I am very happy with the service provided by the company" + } + response = requests.post(url, json=data, headers=headers) + self.assertEqual(response.status_code, 201) + + +import requests +import unittest from dotenv import load_dotenv import os @@ -155,3 +327,5 @@ def test_login(self): if __name__ == "__main__": unittest.main(verbosity=2) + + unittest.main(verbosity=2) \ No newline at end of file diff --git a/src/run-tests/run-tests.controller.ts b/src/run-tests/run-tests.controller.ts index fb0957b62..101f37627 100644 --- a/src/run-tests/run-tests.controller.ts +++ b/src/run-tests/run-tests.controller.ts @@ -2,12 +2,19 @@ import { Controller, Get, Header, Res } from '@nestjs/common'; import { Response } from 'express'; import { skipAuth } from '../helpers/skipAuth'; import { RunTestsService } from './run-tests.service'; +import { ApiOperation } from '@nestjs/swagger'; @Controller('/run-tests') export class RunTestsController { constructor(private readonly runTestsService: RunTestsService) {} @skipAuth() @Get() + @ApiOperation({ + description: + 'This PR extends the functionality of the NestJS application\ + by integrating a Python script for running compatibility tests. The script is executed via\ + a new API endpoint, and the output is streamed to the user on the browser.', + }) @Header('Content-Type', 'text/plain') runTests(@Res() res: Response) { return this.runTestsService.runTests(res); diff --git a/src/shared/inteceptors/response.interceptor.ts b/src/shared/inteceptors/response.interceptor.ts index 4f2aa691e..52ee1f74b 100644 --- a/src/shared/inteceptors/response.interceptor.ts +++ b/src/shared/inteceptors/response.interceptor.ts @@ -1,11 +1,11 @@ import { - Logger, CallHandler, ExecutionContext, HttpException, HttpStatus, Injectable, InternalServerErrorException, + Logger, NestInterceptor, } from '@nestjs/common'; import { Observable, throwError } from 'rxjs'; @@ -28,7 +28,7 @@ export class ResponseInterceptor implements NestInterceptor { `Error processing request for ${req.method} ${req.url}, Message: ${exception['message']}, Stack: ${exception['stack']}` ); return new InternalServerErrorException({ - status: HttpStatus.INTERNAL_SERVER_ERROR, + status_code: HttpStatus.INTERNAL_SERVER_ERROR, message: 'Internal server error', }); } @@ -36,14 +36,14 @@ export class ResponseInterceptor implements NestInterceptor { responseHandler(res: any, context: ExecutionContext) { const ctx = context.switchToHttp(); const response = ctx.getResponse(); - const status = response.statusCode; + const status_code = response.statusCode; response.setHeader('Content-Type', 'application/json'); if (typeof res === 'object') { - const { message, staus_code, ...data } = res; + const { message, ...data } = res; return { - status, + status_code, message, ...data, }; diff --git a/src/uploads/testUserId.jpg b/src/uploads/testUserId.jpg new file mode 100644 index 000000000..30d74d258 --- /dev/null +++ b/src/uploads/testUserId.jpg @@ -0,0 +1 @@ +test \ No newline at end of file diff --git a/staging-ecosystem-config.json b/staging-ecosystem-config.json index d8fa81c86..48d1d822b 100644 --- a/staging-ecosystem-config.json +++ b/staging-ecosystem-config.json @@ -1,11 +1,11 @@ { "apps": [ { - "name": "team-alpha-staging", + "name": "nestjs2-staging", "script": "npm run start:prod", "log_file": "~/.pm2/logs/team-alpha-staging-out.log", "combine_logs": true, "log_date_format": "YYYY-MM-DD HH:mm:ss Z" } ] -} \ No newline at end of file +} diff --git a/start_server.sh b/start_server.sh new file mode 100755 index 000000000..ea52ab4a5 --- /dev/null +++ b/start_server.sh @@ -0,0 +1,5 @@ +npm run build; +npm run migration:generate; +npm run migration:run; +npm run start:prod +# pm2 restart dev-ecosystem-config.json || pm2 start dev-ecosystem-config.json; \ No newline at end of file diff --git a/uploads/testUserId.jpg b/uploads/testUserId.jpg new file mode 100644 index 000000000..30d74d258 --- /dev/null +++ b/uploads/testUserId.jpg @@ -0,0 +1 @@ +test \ No newline at end of file diff --git a/wiki_readme/CICD-Pipeline.md b/wiki_readme/CICD-Pipeline.md deleted file mode 100644 index 02c105528..000000000 --- a/wiki_readme/CICD-Pipeline.md +++ /dev/null @@ -1,313 +0,0 @@ -### CI/CD Pipeline - -The CI/CD pipeline for our NestJS project automates the build, test, and deployment processes, ensuring code quality and efficient delivery. We utilize GitHub Actions to orchestrate these operations across different environments—development, staging, and production. - -#### GitHub Actions Workflow - -Our CI/CD workflow is defined across three GitHub Actions configuration files: `dev.yml`, `staging.yml`, and `main.yml`. These workflows handle the continuous integration and deployment tasks specific to their respective branches. Below are the key components of each workflow: - -1. **Build**: Checks out the codebase, sets up Node.js, installs dependencies, and builds the application. -2. **Test**: Executes unit tests to validate code quality and functionality. -3. **Deploy**: Deploys the application to the provided remote environment, controlled by branch-specific triggers. - -#### Detailed Workflow Configuration - -##### Development Environment (`dev.yml`) - -```yaml -name: CI/CD-Dev - -on: - pull_request: - branches: - - dev - push: - branches: - - dev - -jobs: - test-and-build-dev: - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Set up Node.js - uses: actions/setup-node@v3 - with: - node-version: '18' - - - name: Install dependencies - run: npm install --include=dev - - - name: Build project - run: npm run build - - - name: Run tests - run: npm run test - - deploy-push: - runs-on: ubuntu-latest - if: github.event_name == 'push' - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Set up Node.js - uses: actions/setup-node@v3 - with: - node-version: '18' - - - name: Install dependencies - run: npm install --include=dev - - - name: Build project - run: npm run build - - - name: Run tests - run: npm run test - - - name: Deploying to virtual machine - uses: appleboy/ssh-action@v1.0.3 - with: - host: ${{ secrets.SERVER_HOST }} - username: ${{ secrets.SERVER_USER }} - # key: ${{ secrets.SERVER_PRIVATE_KEY }} - password: ${{ secrets.SERVER_PASSWORD }} - port: ${{ secrets.SERVER_PORT }} - script: | - echo "hello" - export PATH=$PATH:/home/teamalpha/.nvm/versions/node/v20.15.1/bin - bash ~/deployment.sh -``` - -This workflow is triggered on both pull requests and pushes to the `dev` branch. - -**1. Pull Request Triggered:** - -- **Checkout Code:** The repository's code is checked out. -- **Set up Node.js:** The specified Node.js version (18) is set up on the runner. -- **Install Dependencies:** Project dependencies, including development dependencies, are installed using `npm install --include=dev`. -- **Build Project:** The project is built using `npm run build`. -- **Run Tests:** Unit and integration tests are executed to ensure code quality using `npm run test`. - -**2. Push Triggered:** - -- **Checkout Code:** The repository's code is checked out. -- **Set up Node.js:** The specified Node.js version (18) is set up on the runner. -- **Install Dependencies:** Project dependencies, including development dependencies, are installed using `npm install --include=dev`. -- **Build Project:** The project is built using `npm run build`. -- **Run Tests:** Unit and integration tests are executed to ensure code quality using `npm run test`. -- **Deploying to Virtual Machine:** - - The workflow uses the `appleboy/ssh-action@v1.0.3` GitHub Action to establish an SSH connection to the development server. - - Authentication is handled securely using the server's host, username, and password, which are stored as encrypted secrets within the GitHub repository settings. - - Once connected, the workflow executes the `~/deployment.sh` script located on the server. This script handles the environment-specific deployment tasks, such as building and deploying the Docker image, updating environment variables, and restarting the application. - -##### Staging Environment (`staging.yml`) - -```yaml -name: CI/CD--Staging - -on: - pull_request: - branches: - - staging - push: - branches: - - staging - -jobs: - test-and-build-staging: - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Set up Node.js - uses: actions/setup-node@v3 - with: - node-version: '18' - - - name: Install dependencies - run: npm install --include=dev - - - name: Build project - run: npm run build - - - name: Run tests - run: npm run test - - deploy-staging: - runs-on: ubuntu-latest - # needs: test-and-build-main - if: github.event_name == 'push' - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Set up Node.js - uses: actions/setup-node@v3 - with: - node-version: '18' - - - name: Install dependencies - run: npm install --include=dev - - - name: Build project - run: npm run build - - - name: Run tests - run: npm run test - - - name: Deploying to virtual machine - uses: appleboy/ssh-action@v1.0.3 - with: - host: ${{ secrets.SERVER_HOST }} - username: ${{ secrets.SERVER_USER }} - # key: ${{ secrets.SERVER_PRIVATE_KEY }} - password: ${{ secrets.SERVER_PASSWORD }} - port: ${{ secrets.SERVER_PORT }} - script: | - echo "hello" - export PATH=$PATH:/home/teamalpha/.nvm/versions/node/v20.15.1/bin - bash ~/staging-deployment.sh -``` - -This workflow is very similar to the `dev.yml` workflow but is specifically designed for the `staging` branch and deploys to the staging environment. - -**1. Pull Request Triggered:** - -- **Checkout Code:** The repository's code is checked out. -- **Set up Node.js:** The specified Node.js version (18) is set up on the runner. -- **Install Dependencies:** Project dependencies, including development dependencies, are installed using `npm install --include=dev`. -- **Build Project:** The project is built using `npm run build`. -- **Run Tests:** Unit and integration tests are executed to ensure code quality using `npm run test`. - -**2. Push Triggered:** - -- **Checkout Code:** The repository's code is checked out. -- **Set up Node.js:** The specified Node.js version (18) is set up on the runner. -- **Install Dependencies:** Project dependencies, including development dependencies, are installed using `npm install --include=dev`. -- **Build Project:** The project is built using `npm run build`. -- **Run Tests:** Unit and integration tests are executed to ensure code quality using `npm run test`. -- **Deploying to Virtual Machine:** - - The workflow uses the `appleboy/ssh-action@v1.0.3` GitHub Action to establish an SSH connection to the staging server. - - Authentication is handled securely using the server's host, username, and password, which are stored as encrypted secrets within the GitHub repository settings. - - Once connected, the workflow executes the `~/staging-deployment.sh` script located on the server. This script handles the staging-specific deployment tasks, such as building and deploying the Docker image, updating environment variables, and restarting the application. - -##### Production Environment (`main.yml`) - -```yaml -name: CI/CD--Main - -on: - pull_request: - branches: - - main - push: - branches: - - main - -jobs: - test-and-build-main: - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Set up Node.js - uses: actions/setup-node@v3 - with: - node-version: '18' - - - name: Install dependencies - run: npm install --include=dev - - - name: Build project - run: npm run build - - - name: Run tests - run: npm run test - - deploy-main: - runs-on: ubuntu-latest - # needs: test-and-build-main - if: github.event_name == 'push' - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Set up Node.js - uses: actions/setup-node@v3 - with: - node-version: '18' - - - name: Install dependencies - run: npm install --include=dev - - - name: Build project - run: npm run build - - - name: Run tests - run: npm run test - - - name: Deploying to virtual machine - uses: appleboy/ssh-action@v1.0.3 - with: - host: ${{ secrets.SERVER_HOST }} - username: ${{ secrets.SERVER_USER }} - # key: ${{ secrets.SERVER_PRIVATE_KEY }} - password: ${{ secrets.SERVER_PASSWORD }} - port: ${{ secrets.SERVER_PORT }} - script: | - echo "hello" - export PATH=$PATH:/home/teamalpha/.nvm/versions/node/v20.15.1/bin - bash ~/main-deployment.sh -``` - -This workflow manages the deployment to the production environment and is triggered by pull requests and pushes to the `main` branch. - -**1. Pull Request Triggered:** - -- **Checkout Code:** The repository's code is checked out. -- **Set up Node.js:** The specified Node.js version (18) is set up on the runner. -- **Install Dependencies:** Project dependencies, including development dependencies, are installed using `npm install --include=dev`. -- **Build Project:** The project is built using `npm run build`. -- **Run Tests:** Unit and integration tests are executed to ensure code quality using `npm run test`. - -**2. Push Triggered:** - -- **Checkout Code:** The repository's code is checked out. -- **Set up Node.js:** The specified Node.js version (18) is set up on the runner. -- **Install Dependencies:** Project dependencies, including development dependencies, are installed using `npm install --include=dev`. -- **Build Project:** The project is built using `npm run build`. -- **Run Tests:** Unit and integration tests are executed to ensure code quality using `npm run test`. -- **Deploying to Virtual Machine:** - - The workflow uses the `appleboy/ssh-action@v1.0.3` GitHub Action to establish a secure SSH connection to the production server. - - Authentication is handled using server credentials (host, username, password) stored as encrypted secrets within the GitHub repository settings. - - Once connected, the workflow executes the `~/main-deployment.sh` script located on the production server. This script handles the production-specific deployment tasks, such as building and deploying the Docker image, updating environment variables, and restarting the application. - -#### Branching Strategy - -- **Main**: Represents the production-ready codebase. -- **Dev**: Serves as the main development branch. Changes here are merged into `main` for releases. -- **Staging**: Used for final testing before production. Deployed to a subdomain or a specific path mimicking production settings. - -#### Security and Secrets - -GitHub Secrets are used to securely handle deployment credentials and configurations, ensuring that sensitive information is not exposed in the workflow files. These secrets are set in the repository settings under "Settings > Secrets and variables." - -#### Deployment Scripts - -Each environment uses a custom deployment script (`deployment.sh`, `main-deployment.sh`, `staging-deployment.sh`) executed via SSH to the virtual machine. This script is responsible for additional setup tasks and bringing the application online in the respective environments. - -This setup not only ensures the application's stability and security but also facilitates a streamlined development-to-deployment flow. diff --git a/wiki_readme/Database-Setup.md b/wiki_readme/Database-Setup.md deleted file mode 100644 index 170856097..000000000 --- a/wiki_readme/Database-Setup.md +++ /dev/null @@ -1,73 +0,0 @@ -## Database Setup: PostgreSQL with Docker Compose - -This section of the documentation details the setup and configuration of the PostgreSQL databases for the development and staging environments using Docker Compose. - -**1. Docker Compose Configuration** - -The `docker-compose.yml` file defines the configuration for both PostgreSQL databases: - -```yaml -version: '3.3' -services: - postgresdb-prod: - image: postgres:13 - restart: always - env_file: - - .env - ports: - - '5432:5432' - volumes: - - db-data-prod:/var/lib/postgresql/data - postgresdb-staging: - image: postgres:13 - restart: always - env_file: - - ./.env.staging - ports: - - '5433:5432' - volumes: - - db-data-staging:/var/lib/postgresql/data - -volumes: - db-data-prod: - driver: local - db-data-staging: - driver: local -``` - -**2. Database Details** - -- **`postgresdb-prod`:** - - - **Image:** `postgres:13` (Official PostgreSQL 13 image) - - **Environment Variables:** Loaded from `.env` file. - - **Port Mapping:** Exposes PostgreSQL on port `5432` on the host machine. - - **Data Persistence:** Uses a named volume `db-data-prod` to persist database data. - -- **`postgresdb-staging`:** - - **Image:** `postgres:13` - - **Environment Variables:** Loaded from `.env.staging` file. - - **Port Mapping:** Exposes PostgreSQL on port `5433` on the host machine. - - **Data Persistence:** Uses a named volume `db-data-staging` to persist data. - -**3. Environment Variables** - -- **`.env` and `.env.staging`:** These files contain environment-specific configurations for the databases, such as: - - `POSTGRES_DB`: Database name - - `POSTGRES_USER`: Database username - - `POSTGRES_PASSWORD`: Database password - -**4. Data Persistence** - -Both database containers use Docker volumes (`db-data-prod` and `db-data-staging`) to ensure data persistence. This means that even if the containers are stopped and restarted, the database data will be preserved. - -**5. Accessing the Databases** - -- **`postgresdb-prod`:** Accessible from the host machine via `localhost:5432`. -- **`postgresdb-staging`:** Accessible from the host machine via `localhost:5433`. - -**6. Connecting from the Application** - -The NestJS application is configured to connect to the appropriate database based on the current environment using the environment variables loaded from the respective `.env` files. - -This section outlines the configuration and setup of the PostgreSQL databases using Docker Compose. It ensures data isolation between environments and facilitates easy management and scaling of the database infrastructure. diff --git a/wiki_readme/Home.md b/wiki_readme/Home.md deleted file mode 100644 index 86dd9bfc1..000000000 --- a/wiki_readme/Home.md +++ /dev/null @@ -1,167 +0,0 @@ -## NestJS Boilerplate Project Documentation - -Welcome to the comprehensive documentation for our NestJS boilerplate project. This guide will walk you through the setup and integration of advanced CI/CD pipelines, messaging queues, and deployment processes using GitHub Actions. Our goal is to automate build, test, and deployment tasks, improve service communication via messaging queues, and ensure reliable deployments. - -**Table of Contents** - -- [NestJS Boilerplate Project Documentation](#nestjs-boilerplate-project-documentation) -- [Introduction](#introduction) -- [CI/CD Setup](#cicd-setup) - - [Choosing CI/CD Tool](#choosing-cicd-tool) - - [Pipeline Setup](#pipeline-setup) - - [Branching Strategy](#branching-strategy) -- [Database Setup](#database-setup) - - [Configuration](#configuration) -- [Messaging Queue Integration](#messaging-queue-integration) - - [RabbitMQ Setup](#rabbitmq-setup) - - [Integration with NestJS](#integration-with-nestjs) -- [Deployment](#deployment) - - [Server Setup](#server-setup) - - [Deployment Process](#deployment-process) - - [Steps for Deployment](#steps-for-deployment) - - [Domain Name Configuration](#domain-name-configuration) -- [Documentation](#documentation) - - [CI/CD Pipelines](#cicd-pipelines) - - [Messaging Queue Integration](#messaging-queue-integration-1) - - [NGINX Configuration](#nginx-configuration) - - [Database Setup](#database-setup-1) -- [Getting Started](#getting-started) - -## Introduction - -This project aims to streamline the management, deployment, and communication of boilerplate projects. Using GitHub Actions for CI/CD, RabbitMQ for messaging queues, and Docker for database management, we ensure a robust and efficient development environment. - -## CI/CD Setup - -The CI/CD pipeline automates the build, test, and deployment processes of the application, ensuring code quality and efficient delivery. - -### Choosing CI/CD Tool - -For this project, we selected GitHub Actions due to its seamless integration with our GitHub repository and powerful automation capabilities. - -### Pipeline Setup - -We have configured the CI/CD pipelines to automate the build, test, and deployment processes for the NestJS boilerplate project. The pipeline runs on each pull request and push to the `dev`, `staging`, and `main` branches. - -CI/CD Workflow - -- **Build**: This job checks out the codebase, installs dependencies, and builds the application. -- **Test**: Executes unit and integration tests to ensure code quality and functionality. -- **Deploy**: Deploys the application to the designated environment (development, staging, or production). - -The workflow is triggered on every push to the repository and on pull requests. - -### Branching Strategy - -The project follows a Gitflow-like branching strategy: - -- **main**: Represents the production-ready codebase. -- **dev**: The main development branch. Merged into main for releases. -- **staging**: The main staging branch. - -## Database Setup - -Two separate PostgreSQL databases are set up using Docker containers: one for the development environment and one for production. This approach ensures data isolation and allows for independent database configurations. - -### Configuration - -The databases are configured in the `docker-compose.yml` file and connected to the application through environment variables. - -## Messaging Queue Integration - -We utilize RabbitMQ as the message broker for this project. - -### RabbitMQ Setup - -RabbitMQ is installed and runs on the remote server. - -### Integration with NestJS - -The NestJS application is configured to connect to the RabbitMQ server. We use the @nestjs/microservices package to implement message producers and consumers within the application. - -## Deployment - -The deployment process is automated through the CI/CD pipeline, aiming for 99% uptime. Projects are accessible via their respective domain names, and DNS settings are configured accordingly. - -### Server Setup - -The application is deployed to a remote server. Ensure the server meets the following requirements: - -- Node.js and npm installed -- Docker and Docker Compose installed -- Nginx installed and configured as a reverse proxy - -### Deployment Process - -The CI/CD pipeline builds and tests the application to ensure code quality and functionality. The deployment process involves several steps to set up and configure the environment, including database setup, RabbitMQ installation, NGINX proxy configuration, and using PM2 as the process manager to reload the NestJS application. - -#### Steps for Deployment - -1. **CI/CD Pipeline Execution** - - - The pipeline is triggered by a push or pull request to the repository. - - The pipeline checks out the code, installs dependencies, runs tests, and builds the project. - - Upon successful build and test, the pipeline deploys the application to the target environment (development, staging, or production). - -2. **Database Setup** - - - Two separate PostgreSQL databases are configured using Docker containers: one for development and one for production. - - Docker Compose is used to manage the database containers, ensuring they are isolated and correctly configured. - - Environment variables are set to connect the NestJS application to the appropriate database. - -3. **RabbitMQ Installation** - - - RabbitMQ is installed on the remote server to handle messaging queues for the application. - - The RabbitMQ service is configured to start on boot and is integrated into the NestJS application using environment variables. - -4. **NGINX Proxy Configuration** - - - NGINX is used as a reverse proxy to route incoming requests to the NestJS application. - - The NGINX configuration file is set up to forward requests to the appropriate port where the NestJS application is running. - - SSL certificates are configured in NGINX for secure communication if necessary. - - The NGINX service is restarted to apply the new configuration. - -5. **Using PM2 as the Process Manager** - - - PM2 is used to manage the NestJS application processes. - - The application is started using PM2, which ensures it runs in the background and restarts automatically on failure. - - The deployment script reloads the application using PM2 to apply any new changes. - -6. **Deployment Script Execution** - - A deployment script (`deployment.sh`, `main-deployment.sh`, `staging-deployment.sh`) is used to automate the deployment tasks. - - The script includes steps to pull the latest code, install dependencies, build the project, and reload the application using PM2. - - Environment variables are sourced to ensure all configurations are correctly applied. - -### Domain Name Configuration - -- We configured DNS records for [Main](https://api-nestjs.boilerplate.hng.tech), - [Staging](https://staging.api-nestjs.boilerplate.hng.tech), - [Dev](https://deployment.api-nestjs.boilerplate.hng.tech) to point to the server's IP address. -- Set up Nginx to act as a reverse proxy, directing traffic to the appropriate application based on the domain name. - -## Documentation - -All processes are documented comprehensively in this GitHub Wiki. Each section covers detailed steps, configurations, and commands used in the setup and deployment of the application. - -### [CI/CD Pipelines](cicd-pipeline) - -Automated processes for build, test, and deployment are detailed, including YAML configurations and environment setups. - -### [Messaging Queue Integration](rabbitmq-installation-and-setup) - -Documentation covers the installation, configuration, and integration of RabbitMQ into the NestJS project. - -### [NGINX Configuration](nginx-configuration) - -This outlines the NGINX configuration used for routing traffic to our NestJS applications deployed on different ports and subdomains. - -### [Database Setup](database-setup) - -Step-by-step guides on setting up and configuring Postgres databases in Docker containers for development and production. - -## Getting Started - -1. Clone the repository. -2. Install dependencies: `npm install` -3. Configure environment variables (database connection details, RabbitMQ credentials, etc.). -4. Start the application: `npm run start:dev`, `npm run start:staging`, `npm run start:prod` diff --git a/wiki_readme/NGINX-Configuration.md b/wiki_readme/NGINX-Configuration.md deleted file mode 100644 index e2f4b17bf..000000000 --- a/wiki_readme/NGINX-Configuration.md +++ /dev/null @@ -1,102 +0,0 @@ -## NGINX Configuration for NestJS Applications - -This outlines the NGINX configuration used for routing traffic to our NestJS applications deployed on different ports and subdomains. - -**Configuration Breakdown:** - -The provided NGINX configuration defines four server blocks: - -**1. Production Server (api-nestjs.boilerplate.hng.tech:443)** - -```nginx -server { - server_name api-nestjs.boilerplate.hng.tech; - - access_log /var/log/nginx/access.log; - error_log /var/log/nginx/error.log; - - location / { - proxy_pass http://localhost:3007; - } - - listen 443 ssl; - ssl_certificate /etc/letsencrypt/live/api-nestjs.boilerplate.hng.tech/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/api-nestjs.boilerplate.hng.tech/privkey.pem; - include /etc/letsencrypt/options-ssl-nginx.conf; - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; -} -``` - -- This block handles HTTPS traffic for the main application domain `api-nestjs.boilerplate.hng.tech`. -- It listens on port 443 (default HTTPS port). -- Traffic is forwarded to the NestJS application running on `http://localhost:3007` using `proxy_pass`. -- SSL is enabled using certificates obtained from Let's Encrypt. - -**2. Deployment Server (deployment.api-nestjs.boilerplate.hng.tech:443)** - -```nginx -server { - listen 443 ssl; - server_name deployment.api-nestjs.boilerplate.hng.tech; - - ssl_certificate /etc/letsencrypt/live/deployment.api-nestjs.boilerplate.hng.tech/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/deployment.api-nestjs.boilerplate.hng.tech/privkey.pem; - - access_log /var/log/nginx/access.log; - error_log /var/log/nginx/error.log; - - location / { - proxy_pass http://localhost:3008; - } -} -``` - -- This block handles HTTPS traffic for the subdomain `deployment.api-nestjs.boilerplate.hng.tech`, likely used for a deployment preview environment. -- Traffic is forwarded to the application instance running on `http://localhost:3008`. -- Similar to the production server, SSL is enabled with Let's Encrypt certificates. - -**3. Staging Server (staging.api-nestjs.boilerplate.hng.tech:443)** - -```nginx -server { - listen 443 ssl; - server_name staging.api-nestjs.boilerplate.hng.tech; - - ssl_certificate /etc/letsencrypt/live/staging.api-nestjs.boilerplate.hng.tech/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/staging.api-nestjs.boilerplate.hng.tech/privkey.pem; - - access_log /var/log/nginx/access.log; - error_log /var/log/nginx/error.log; - - location / { - proxy_pass http://localhost:3009; - } -} -``` - -- This block handles HTTPS traffic for the subdomain `staging.api-nestjs.boilerplate.hng.tech`, likely used for a staging environment. -- Traffic is forwarded to the application instance running on `http://localhost:3009`. -- SSL is enabled using Let's Encrypt certificates. - -**4. HTTP Redirection Server (port 80)** - -```nginx -server { - listen 80; - server_name api-nestjs.boilerplate.hng.tech staging.api-nestjs.boilerplate.hng.tech deployment.api-nestjs.boilerplate.hng.tech; - - location /.well-known/acme-challenge/ { - allow all; - root /var/www/certbot; - } - - location / { - return 301 https://$host$request_uri; - } -} -``` - -- This block listens on port 80 (default HTTP port) for all defined server names. -- It handles two types of requests: - - Requests for the Let's Encrypt ACME challenge used for certificate renewal are allowed and served from `/var/www/certbot`. - - All other requests are redirected to their HTTPS equivalent using a 301 redirect (`return 301 https://$host$request_uri;`). diff --git a/wiki_readme/RabbitMQ-Installation-and-Setup.md b/wiki_readme/RabbitMQ-Installation-and-Setup.md deleted file mode 100644 index 1cc5172d5..000000000 --- a/wiki_readme/RabbitMQ-Installation-and-Setup.md +++ /dev/null @@ -1,124 +0,0 @@ -## RabbitMQ Installation and Setup - -This document details the process of installing and setting up RabbitMQ on a remote server, enabling its use as a message broker for the NestJS application. - -**Server Requirements:** - -- Ubuntu or a compatible Linux distribution -- Root or sudo access -- Stable internet connection - -### Installation Steps: - -1. **Update System Packages:** - - ```bash - sudo apt-get update -y - ``` - -2. **Install Prerequisite Packages:** - - ```bash - sudo apt-get install curl gnupg apt-transport-https -y - ``` - - - **curl:** Used to download files from the internet. - - **gnupg:** Used for verifying package signatures. - - **apt-transport-https:** Enables HTTPS support for APT package manager. - -3. **Import Signing Keys:** - - ```bash - ## Team RabbitMQ's main signing key - curl -1sLf "https://keys.openpgp.org/vks/v1/by-fingerprint/0A9AF2115F4687BD29803A206B73A36E6026DFCA" | sudo gpg --dearmor | sudo tee /usr/share/keyrings/com.rabbitmq.team.gpg > /dev/null - - ## Community mirror of Cloudsmith: modern Erlang repository - curl -1sLf https://github.com/rabbitmq/signing-keys/releases/download/3.0/cloudsmith.rabbitmq-erlang.E495BB49CC4BBE5B.key | sudo gpg --dearmor | sudo tee /usr/share/keyrings/rabbitmq.E495BB49CC4BBE5B.gpg > /dev/null - - ## Community mirror of Cloudsmith: RabbitMQ repository - curl -1sLf https://github.com/rabbitmq/signing-keys/releases/download/3.0/cloudsmith.rabbitmq-server.9F4587F226208342.key | sudo gpg --dearmor | sudo tee /usr/share/keyrings/rabbitmq.9F4587F226208342.gpg > /dev/null - ``` - - - This step imports the necessary GPG keys for verifying the authenticity of the RabbitMQ and Erlang packages. - -4. **Add RabbitMQ Repositories:** - - ```bash - sudo tee /etc/apt/sources.list.d/rabbitmq.list <