diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ecfa1fc00..7e31559e5 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -57,19 +57,10 @@ updates: applies-to: version-updates patterns: - "*apollo-server*" - babel: - applies-to: version-updates - patterns: - - "@babel*" metascraper: applies-to: version-updates patterns: - "metascraper*" - typescript: - applies-to: version-updates - patterns: - - "ts*" - - "*types?" # webapp - package-ecosystem: docker diff --git a/.github/workflows/check-documentation.yml b/.github/workflows/check-documentation.yml index 27e761fa5..6b2cddeac 100644 --- a/.github/workflows/check-documentation.yml +++ b/.github/workflows/check-documentation.yml @@ -54,7 +54,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7 - name: Setup Node 20 - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.0.3 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.0.3 with: node-version: '20' diff --git a/.github/workflows/deploy-documentation.yml b/.github/workflows/deploy-documentation.yml index d1816bddd..13e0fb963 100644 --- a/.github/workflows/deploy-documentation.yml +++ b/.github/workflows/deploy-documentation.yml @@ -30,7 +30,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7 - name: Setup Node 20 - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.0.3 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.0.3 with: node-version: 20 diff --git a/.github/workflows/docker-push.yml b/.github/workflows/docker-push.yml index 9e95fabe5..cee1be7e3 100644 --- a/.github/workflows/docker-push.yml +++ b/.github/workflows/docker-push.yml @@ -68,7 +68,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@70b2cdc6480c1a8b86edf1777157f8f437de2166 + uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | @@ -81,7 +81,7 @@ jobs: type=sha - name: Build and push Docker images id: push - uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 with: context: ${{ matrix.app.context }} target: ${{ matrix.app.target }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b66413f22..ff2961e48 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -64,7 +64,7 @@ jobs: echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV - run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV #- name: Repository Dispatch - # uses: peter-evans/repository-dispatch@7d980a9b9f8ecf8955ea90507b3ed89122f53215 # v3.0.0 + # uses: peter-evans/repository-dispatch@0eae9e597ebc81bcc8c2220e34ddff4bc7c769b3 # v3.0.0 # with: # token: ${{ github.token }} # event-type: trigger-ocelot-build-success @@ -72,7 +72,7 @@ jobs: # client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}", "VERSION": "${VERSION}", "BUILD_DATE": "${BUILD_DATE}", "BUILD_COMMIT": "${BUILD_COMMIT}", "BUILD_VERSION": "${BUILD_VERSION}"}' - name: Repository Dispatch stage.ocelot.social - uses: peter-evans/repository-dispatch@7d980a9b9f8ecf8955ea90507b3ed89122f53215 # v3.0.0 + uses: peter-evans/repository-dispatch@0eae9e597ebc81bcc8c2220e34ddff4bc7c769b3 # v3.0.0 with: token: ${{ secrets.OCELOT_PUBLISH_EVENT_PAT }} # this token is required to access the other repository event-type: trigger-ocelot-build-success @@ -80,7 +80,7 @@ jobs: client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}", "GITHUB_RUN_NUMBER": "${{ env.GITHUB_RUN_NUMBER }}", "VERSION": "${VERSION}", "BUILD_DATE": "${BUILD_DATE}", "BUILD_COMMIT": "${BUILD_COMMIT}", "BUILD_VERSION": "${BUILD_VERSION}"}' - name: Repository Dispatch stage.yunite.me - uses: peter-evans/repository-dispatch@7d980a9b9f8ecf8955ea90507b3ed89122f53215 # v3.0.0 + uses: peter-evans/repository-dispatch@0eae9e597ebc81bcc8c2220e34ddff4bc7c769b3 # v3.0.0 with: token: ${{ secrets.OCELOT_PUBLISH_EVENT_PAT }} # this token is required to access the other repository event-type: trigger-ocelot-build-success diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml index 766717a97..a7eff0ef2 100644 --- a/.github/workflows/test-backend.yml +++ b/.github/workflows/test-backend.yml @@ -113,7 +113,7 @@ jobs: - name: backend | docker compose # doesn't work without the --build flag - this either means we should not load the cached images or cache the correct image - run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps neo4j backend --build + run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach backend --build - name: backend | Initialize Database run: docker compose exec -T backend yarn db:migrate init diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 8e3570d95..bbf321a47 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -3,8 +3,69 @@ name: ocelot.social end-to-end test CI on: push jobs: - docker_preparation: - name: Fullstack test preparation + prepare_backend_environment: + name: Fullstack | prepare backend environment + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7 + + - name: Copy backend env file + run: | + cp backend/.env.test_e2e backend/.env + cp webapp/.env.template webapp/.env + + - name: Build backend and dependencies + run: | + # Build and start all required images for backend + docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach backend --build + + # Save the build images + docker save "ghcr.io/ocelot-social-community/ocelot-social/backend:test" > /tmp/backend.tar + docker save "ghcr.io/ocelot-social-community/ocelot-social/neo4j-community:test" > /tmp/neo4j.tar + docker save "quay.io/minio/minio:latest" > /tmp/minio.tar + docker save "quay.io/minio/mc:latest" > /tmp/minio-mc.tar + docker save "maildev/maildev:latest" > /tmp/mailserver.tar + + # Stop the containers + docker compose -f docker-compose.yml -f docker-compose.test.yml down + + - name: Cache docker images + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2 + with: + path: | + /tmp/backend.tar + /tmp/neo4j.tar + /tmp/minio.tar + /tmp/minio-mc.tar + /tmp/mailserver.tar + key: ${{ github.run_id }}-e2e-backend-environment-cache + + prepare_webapp_image: + name: Fullstack | prepare webapp image + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7 + + - name: Copy backend env file + run: | + cp backend/.env.test_e2e backend/.env + cp webapp/.env.template webapp/.env + + - name: Build docker image + run: | + docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach webapp --build --no-deps + docker save "ghcr.io/ocelot-social-community/ocelot-social/webapp:test" > /tmp/webapp.tar + + - name: Cache docker image + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2 + with: + path: /tmp/webapp.tar + key: ${{ github.run_id }}-e2e-webapp-cache + + prepare_cypress: + name: Fullstack | prepare cypress runs-on: ubuntu-latest steps: - name: Checkout code @@ -13,18 +74,7 @@ jobs: - name: Copy env files run: | cp webapp/.env.template webapp/.env - cp frontend/.env.dist frontend/.env - cp backend/.env.template backend/.env - - - name: Build docker images - run: | - mkdir /tmp/images - docker build --target community -t "ocelotsocialnetwork/neo4j-community:test" neo4j/ - docker save "ocelotsocialnetwork/neo4j-community:test" > /tmp/images/neo4j.tar - docker build --target test -t "ocelotsocialnetwork/backend:test" backend/ - docker save "ocelotsocialnetwork/backend:test" > /tmp/images/backend.tar - docker build --target test -t "ocelotsocialnetwork/webapp:test" webapp/ - docker save "ocelotsocialnetwork/webapp:test" > /tmp/images/webapp.tar + cp backend/.env.test_e2e backend/.env - name: Install cypress requirements run: | @@ -35,21 +85,20 @@ jobs: cd .. yarn install - - name: Cache docker images - id: cache + - name: Cache docker image + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2 with: path: | /opt/cucumber-json-formatter /home/runner/.cache/Cypress /home/runner/work/Ocelot-Social/Ocelot-Social - /tmp/images/ - key: ${{ github.run_id }}-e2e-preparation-cache + key: ${{ github.run_id }}-e2e-cypress fullstack_tests: - name: Fullstack tests + name: Fullstack | tests if: success() - needs: docker_preparation + needs: [prepare_backend_environment, prepare_webapp_image, prepare_cypress] runs-on: ubuntu-latest env: jobs: 8 @@ -58,26 +107,44 @@ jobs: # run copies of the current job in parallel job: [1, 2, 3, 4, 5, 6, 7, 8] steps: - - name: Restore cache + - name: Restore cypress cache uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2 - id: cache with: path: | /opt/cucumber-json-formatter /home/runner/.cache/Cypress /home/runner/work/Ocelot-Social/Ocelot-Social - /tmp/images/ - key: ${{ github.run_id }}-e2e-preparation-cache - fail-on-cache-miss: true + key: ${{ github.run_id }}-e2e-cypress + restore-keys: ${{ github.run_id }}-e2e-cypress + - name: Restore backend environment cache + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2 + with: + path: | + /tmp/backend.tar + /tmp/neo4j.tar + /tmp/minio.tar + /tmp/minio-mc.tar + /tmp/mailserver.tar + key: ${{ github.run_id }}-e2e-backend-environment-cache + + - name: Restore webapp cache + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2 + with: + path: /tmp/webapp.tar + key: ${{ github.run_id }}-e2e-webapp-cache + - name: Boot up test system | docker compose run: | chmod +x /opt/cucumber-json-formatter sudo ln -fs /opt/cucumber-json-formatter /usr/bin/cucumber-json-formatter - docker load < /tmp/images/neo4j.tar - docker load < /tmp/images/backend.tar - docker load < /tmp/images/webapp.tar - docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp neo4j backend --build + docker load < /tmp/neo4j.tar + docker load < /tmp/backend.tar + docker load < /tmp/minio.tar + docker load < /tmp/minio-mc.tar + docker load < /tmp/mailserver.tar + docker load < /tmp/webapp.tar + docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach backend mailserver webapp sleep 90s - name: Full stack tests | run tests @@ -98,17 +165,24 @@ jobs: name: ocelot-e2e-test-report-pr${{ needs.docker_preparation.outputs.pr-number }} path: /home/runner/work/Ocelot-Social/Ocelot-Social/cypress/reports/cucumber_html_report - cleanup: - name: Cleanup - needs: [docker_preparation, fullstack_tests] + cleanup_cache: + name: Cleanup Cache + needs: fullstack_tests runs-on: ubuntu-latest - permissions: write-all continue-on-error: true steps: - - name: Delete cache + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7 + + - name: Full stack tests | cleanup cache + run: | + cacheKeys=$(gh cache list --json key --jq '.[] | select(.key | startswith("${{ github.run_id }}-e2e-")) | .key') + set +e + echo "Deleting caches..." + for cacheKey in $cacheKeys + do + gh cache delete "$cacheKey" + done + echo "Done" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh extension install actions/gh-actions-cache - KEY="${{ github.run_id }}-e2e-preparation-cache" - gh actions-cache delete $KEY -R Ocelot-Social-Community/Ocelot-Social --confirm \ No newline at end of file diff --git a/.github/workflows/test.lint_pr.yml b/.github/workflows/test.lint_pr.yml index 24d0c6395..b67fcab55 100644 --- a/.github/workflows/test.lint_pr.yml +++ b/.github/workflows/test.lint_pr.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest if: ${{ github.actor != 'dependabot[bot]' }} steps: - - uses: amannn/action-semantic-pull-request@04501d43b574e4c1d23c629ffe4dcec27acfdeff # v5.5.3 + - uses: amannn/action-semantic-pull-request@335288255954904a41ddda8947c8f2c844b8bfeb # v5.5.3 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.gitignore b/.gitignore index d9d081e31..6f71f582b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ yarn-error.log* kubeconfig.yaml backup-cron-job.log .vscode +.nuxt node_modules/ cypress/videos diff --git a/CHANGELOG.md b/CHANGELOG.md index f383034d5..74b161622 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,327 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [3.8.2](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.8.1...3.8.2) + +- feat(webapp): location on registration [`#8608`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8608) +- fix(webapp): group categories [`#8634`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8634) +- build(deps): bump @aws-sdk/lib-storage in /backend [`#8615`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8615) +- build(deps): bump validator from 13.15.0 to 13.15.15 in /backend [`#8628`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8628) +- build(deps): bump validator from 13.15.0 to 13.15.15 in /webapp [`#8622`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8622) +- build(deps-dev): bump eslint-plugin-prettier in /backend [`#8621`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8621) +- feat(backend): push posts [`#8609`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8609) +- build(deps): bump node from 24.0.2-alpine to 24.1.0-alpine in /backend [`#8613`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8613) +- build(deps-dev): bump eslint-plugin-jest in /backend [`#8623`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8623) +- build(deps): bump docker/build-push-action from 6.17.0 to 6.18.0 [`#8612`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8612) + +#### [3.8.1](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.8.0...3.8.1) + +> 2 June 2025 + +- v3.8.1 [`#8633`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8633) +- fix(backend): hotfix - allow more user fields to be queried [`#8632`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8632) + +#### [3.8.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.7.0...3.8.0) + +> 1 June 2025 + +- v3.8.0 [`#8631`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8631) +- feat(backend): migrate to s3 [`#8545`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8545) +- fix broken observe on post [`#8610`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8610) +- feat(webapp): shout comments [`#8600`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8600) +- refactor(webapp): make group form's location select available as a separate component [`#6245`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6245) +- feat(webapp): ask for real name [`#8605`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8605) +- feat(backend): pin public group posts [`#8606`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8606) +- feat(backend): pin more than one post [`#8598`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8598) +- refactor(webapp): remove warnings in unit tests [`#8556`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8556) +- refactor(webapp): store for categories [`#8551`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8551) + +#### [3.7.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.6.1...3.7.0) + +> 25 May 2025 + +- v3.7.0 [`#8597`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8597) +- fix(webapp): logo and top-menu optimization [`#8590`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8590) +- fix(webapp): hotfix dropdown menu placement [`#8594`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8594) +- fix(webapp): notifications - UI Improvements [`#8559`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8559) +- update user, write all values, query verification Badge [`#8593`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8593) +- refactor(webapp): remove email normalization [`#8580`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8580) +- feat(backend): autoselect badges when rewarding and the user still have free slots [`#8577`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8577) +- fix websocket - downgrade graphql [`#8592`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8592) +- build(deps-dev): bump @faker-js/faker from 9.7.0 to 9.8.0 in /webapp [`#8567`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8567) +- build(deps): bump @aws-sdk/client-s3 from 3.804.0 to 3.817.0 in /backend [`#8582`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8582) +- build(deps-dev): bump eslint-plugin-jsonc in /backend [`#8583`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8583) +- build(deps-dev): bump @types/lodash from 4.17.16 to 4.17.17 in /backend [`#8585`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8585) +- build(deps-dev): bump the cypress group across 1 directory with 2 updates [`#8561`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8561) +- build(deps-dev): bump @types/node from 22.15.18 to 22.15.21 in /backend [`#8586`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8586) +- build(deps): bump peter-evans/repository-dispatch [`#8589`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8589) +- build(deps-dev): bump eslint-plugin-prettier in /webapp [`#8520`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8520) +- improve login & invite mechanics [`#8574`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8574) +- build(backend): upgrade outdated S3 client [`#8463`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8463) +- refactor(docker): remove dead code [`#8554`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8554) +- build(deps-dev): bump eslint-plugin-prettier in /backend [`#8526`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8526) +- build(deps): bump linkify-html from 4.2.0 to 4.3.1 in /backend [`#8532`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8532) +- build(deps-dev): bump @faker-js/faker from 9.7.0 to 9.8.0 [`#8560`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8560) +- build(deps): bump peter-evans/repository-dispatch [`#8563`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8563) +- fix(webapp): fix popover flickering [`#8555`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8555) +- fix(backend): user role in group in notifications [`#8552`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8552) +- fix statistics: reports [`#8576`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8576) +- build(deps): bump docker/build-push-action from 6.16.0 to 6.17.0 [`#8562`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8562) +- build(deps): bump sanitize-html from 2.16.0 to 2.17.0 in /backend [`#8568`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8568) +- build(deps-dev): bump @faker-js/faker from 9.7.0 to 9.8.0 in /backend [`#8570`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8570) +- build(deps): bump node from 23.11.0-alpine to 24.0.2-alpine in /backend [`#8564`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8564) +- build(deps-dev): bump ts-jest from 29.3.2 to 29.3.4 in /backend [`#8569`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8569) +- build(deps-dev): bump @types/node from 22.15.3 to 22.15.18 in /backend [`#8571`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8571) +- build(deps-dev): bump eslint-config-prettier in /backend [`#8529`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8529) +- feat(backend): add support line to emails [`#8502`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8502) +- build(deps-dev): bump tsc-alias from 1.8.15 to 1.8.16 in /backend [`#8530`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8530) + +#### [3.6.1](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.6.0...3.6.1) + +> 12 May 2025 + +- v3.6.1 [`#8553`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8553) +- fix(webapp): fix flickering? [`#8549`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8549) +- fix(backend): fix statistics and introduce new values [`#8550`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8550) +- Fix typo in german translation [`#8548`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8548) +- feat(webapp): default language configurable [`#8546`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8546) +- fix(webapp): query categories on login to get the count [`#8542`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8542) + +#### [3.6.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.5.3...3.6.0) + +> 10 May 2025 + +- v3.6.0 [`#8541`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8541) +- Show invititation dropdown until user clicks somewhere else [`#8539`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8539) +- feat(webapp): redirect to group after registration with invite to group [`#8540`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8540) +- fix(webapp): fix layout break and hidden group name appearance [`#8538`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8538) +- feat(webapp): several group and personal invitation links [`#8504`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8504) +- fix(backend): category seed [`#8536`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8536) +- correct copy path in dockerfile [`#8519`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8519) +- feat(webapp): group invite after login [`#8518`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8518) +- feat(webapp): redirect on registration for invite links [`#8517`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8517) +- fix(webapp): mobile optimization [`#8516`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8516) +- feat(docu): update email snapshots [`#8514`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8514) +- fix(backend): fix user profile and group links in e-mails [`#8512`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8512) +- fix(backend): fix registration with invite code [`#8513`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8513) +- fix locales errors (german) [`#8510`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8510) +- fix(backend): invite codes - hotfix 1 [`#8508`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8508) +- refactor(backend): category seed [`#8505`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8505) +- feat(backend): group invite codes [`#8499`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8499) +- feat(webapp): change german to `du` and `dich` [`#8507`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8507) + +#### [3.5.3](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.5.2...3.5.3) + +> 7 May 2025 + +- chore(release): v3.5.3 [`#8503`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8503) +- fix(backend): correct email from [`#8501`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8501) +- refactor(backend): types for global config [`#8485`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8485) +- fix warning in workflow for lower case as [`#8494`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8494) + +#### [3.5.2](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.5.1...3.5.2) + +> 6 May 2025 + +- v3.5.2 [`#8498`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8498) +- fix emails2 [`#8497`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8497) + +#### [3.5.1](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.5.0...3.5.1) + +> 6 May 2025 + +- v3.5.1 [`#8496`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8496) +- fix emails in production [`#8493`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8493) + +#### [3.5.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.4.0...3.5.0) + +> 6 May 2025 + +- v3.5.0 [`#8492`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8492) +- feat(webapp): user teaser popover [`#8450`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8450) +- feat(backend): signup email localized [`#8459`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8459) +- lint json [`#8472`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8472) +- refactor(other): cypress: simplify cucumber preprocessor imports and some linting [`#8489`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8489) +- fix backend node23 [`#8488`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8488) +- refactor(workflow): parallelize e2e preparation [`#8481`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8481) +- refactor(backend): types for context + `slug` [`#8486`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8486) +- feat(backend): emails for notifications [`#8435`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8435) +- remove some dependabot groups & no alpine version to allow update [`#8475`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8475) +- build(deps-dev): bump the babel group with 3 updates [`#8478`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8478) +- build(deps-dev): bump @types/node from 22.15.2 to 22.15.3 in /backend [`#8479`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8479) +- refactor(backend): refactor context [`#8434`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8434) +- build(deps): bump amannn/action-semantic-pull-request [`#8480`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8480) +- build(deps-dev): bump eslint-plugin-prettier in /webapp [`#8332`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8332) +- remove all helpers on src/helpers [`#8469`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8469) +- move models into database folder [`#8471`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8471) +- also lint cjs files [`#8467`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8467) +- refactor(backend): refactor badges [`#8465`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8465) +- refactor(backend): move resolvers into graphql folder [`#8470`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8470) +- refactor(webapp): remove unused packages [`#8468`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8468) +- refactor(backend): remove unused packages [`#8466`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8466) +- fix(backend): fix backend dev and dev:debug command [`#8439`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8439) +- move distanceToMe onto Location [`#8464`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8464) +- feat(backend): distanceToMe [`#8462`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8462) +- fix(webapp): fixed padding for mobile in basic layout [`#8455`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8455) +- Fix ocelot.social link for imprint and donation [`#8461`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8461) + +#### [3.4.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.3.0...3.4.0) + +> 28 April 2025 + +- v3.4.0 [`#8454`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8454) +- fix(webapp): fix badge focus [`#8452`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8452) +- feat(backend): branding middlewares [`#8429`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8429) +- refactor(webapp): make login, registration, password-reset layout brandable [`#8440`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8440) +- fix(backend): fixes for branding [`#8449`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8449) +- Replace edit link by pencil button [`#8453`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8453) +- fix(webapp): refine little things [`#8382`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8382) +- fix(webapp): fix admin badges settings [`#8438`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8438) +- build(deps): bump peter-evans/repository-dispatch [`#8443`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8443) +- build(deps-dev): bump nodemon from 3.1.9 to 3.1.10 in /backend [`#8447`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8447) +- build(deps-dev): bump @types/node from 22.14.1 to 22.15.2 in /backend [`#8446`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8446) +- build(deps): bump docker/build-push-action from 6.15.0 to 6.16.0 [`#8444`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8444) +- build(deps-dev): bump cypress from 14.3.1 to 14.3.2 in the cypress group [`#8442`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8442) +- build(deps-dev): bump eslint-import-resolver-typescript in /backend [`#8445`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8445) +- build(deps-dev): bump eslint-config-prettier in /backend [`#8370`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8370) +- revokeBadge also removes selection [`#8437`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8437) +- feat(webapp): badges UI [`#8426`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8426) +- feat(backend): lint - detect unused typescript disables [`#8425`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8425) +- fix(docu): remove required but missing `frontend/.env` [`#8431`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8431) +- refactor(backend): types for neo4j & neode [`#8409`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8409) +- lint everything, disable some setup steps for jest [`#8423`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8423) +- lint n/no-sync [`#8405`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8405) +- fix(backend): fix notification emails with different name [`#8419`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8419) +- refactor(backend): default badges, always return a badge [`#8430`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8430) +- refactor(backend): allow to set selected badge-slot to null [`#8421`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8421) +- chore(frontend): run npm install [`#8432`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8432) +- refactor(webapp): refactor branding diverse v2 [`#8427`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8427) +- feat(webapp): badges admin settings [`#8401`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8401) +- move graphql types into graphql folder [`#8420`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8420) +- fix faker image seed [`#8422`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8422) +- build(deps-dev): bump @faker-js/faker from 9.6.0 to 9.7.0 in /webapp [`#8411`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8411) +- build(deps-dev): bump @faker-js/faker from 9.6.0 to 9.7.0 [`#8414`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8414) +- build(deps): bump sanitize-html from 2.15.0 to 2.16.0 in /backend [`#8418`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8418) +- build(deps-dev): bump cypress from 14.3.0 to 14.3.1 in the cypress group [`#8413`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8413) +- build(deps): bump actions/setup-node from 4.3.0 to 4.4.0 [`#8412`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8412) +- refactor(backend): separate queries [`#8358`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8358) +- refactor(backend): lint @typescript-eslint/strict [`#8408`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8408) +- refactor(backend): lint @typescript-eslint/recommended-requiring-type-checking [`#8407`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8407) +- lint @typescript-eslint/recommended [`#8406`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8406) +- build(deps): bump nodemailer from 6.10.0 to 6.10.1 in /backend [`#8417`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8417) +- build(deps-dev): bump @eslint-community/eslint-plugin-eslint-comments [`#8415`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8415) +- feat(backend): badges [`#8391`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8391) +- feat(backend): do not notify blocked or muted users [`#8403`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8403) +- feat(backend): only one email is sent although more notifications are triggered [`#8400`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8400) +- fix(backend): flaky notifications on mention in group unit test [`#8404`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8404) +- refactor(webapp): refactor branding of post ribbons and chat etc. [`#8395`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8395) +- downgrade sass to 1.77.6 [`#8399`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8399) +- mentiioned users in posts and comments of groups [`#8392`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8392) +- feat(backend): no notification mails to users online [`#8397`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8397) +- Add .nuxt to gitignore [`#8393`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8393) +- fix migrations [`#8390`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8390) +- chore(frontend): add '.nvmrc' file to new frontend [`#7112`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7112) +- refactor(backend): fix is muted by me query [`#8365`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8365) +- fix(backend): block/mute chat [`#8364`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8364) +- build(deps): bump graphql-upload from 11.0.0 to 13.0.0 in /backend [`#8375`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8375) +- build(deps-dev): bump the typescript group across 1 directory with 2 updates [`#8383`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8383) +- Bump graphql from 14.7.0 to 15.10.1 in /webapp [`#8157`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8157) +- fix(webapp): better settings ux [`#8347`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8347) +- Bump bcryptjs from 2.4.3 to 3.0.2 [`#8218`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8218) +- Bump bcryptjs from 2.4.3 to 3.0.2 in /backend [`#8224`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8224) +- build(deps-dev): bump cypress from 14.2.1 to 14.3.0 in the cypress group [`#8366`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8366) +- build(deps-dev): bump eslint-import-resolver-typescript in /backend [`#8369`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8369) +- build(deps-dev): bump dotenv from 16.4.7 to 16.5.0 [`#8367`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8367) +- build(deps): bump ioredis from 4.16.1 to 5.6.1 in /backend [`#8371`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8371) +- build(deps): bump dotenv from 16.4.7 to 16.5.0 in /backend [`#8372`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8372) +- build(deps-dev): bump eslint-config-prettier in /webapp [`#8377`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8377) +- build(deps-dev): bump @types/node from 22.14.0 to 22.14.1 in /backend [`#8374`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8374) + +#### [3.3.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.2.1...3.3.0) + +> 12 April 2025 + +- v3.3.0 [`#8380`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8380) +- fix(webapp): refine group muting locales [`#8378`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8378) +- chore(backend): add e-mail setting for our new 'mailserver' to our backend .env.template [`#8359`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8359) +- refactor(backend): set up smtp pooling for nodemailer [`#8167`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8167) +- release script does not include commits [`#8381`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8381) +- refactor(backend): user graphql [`#8354`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8354) +- fix block user [`#8363`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8363) +- feat(webapp): add mute/unumute group to menu [`#8335`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8335) +- test that there is no email sent when no notification [`#8362`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8362) +- feat(backend): notify posts in groups [`#8346`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8346) +- docker compose files: use current maildev image from the right repo [`#8351`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8351) +- feat(backend): notify users when a followed user posted [`#8313`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8313) +- fix(backend): fixing admin email notification settings [`#8356`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8356) +- fix(backend): error when there is an abandoned email [`#8315`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8315) +- migrate commenting users to observe commented posts [`#8308`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8308) +- refactor(backend): new chat message notification email [`#8357`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8357) +- refactor(backend): clean migrate scripts [`#8317`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8317) +- feat(webapp): notification settings frontend [`#8320`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8320) +- posts and comments created by the factory set the observe relation [`#8344`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8344) +- refactor(backend): lint - import/order [`#8350`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8350) +- lint - import/no-relative-parent-imports [`#8349`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8349) +- refactor(other): cleanup cypress configuration [`#8345`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8345) +- lint - update plugin promise and cleanup rules [`#8343`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8343) +- lint plugin n - update and cleanup [`#8342`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8342) +- lint import/no-extraneous-dependencies [`#8341`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8341) +- build(deps): bump express from 4.21.2 to 5.1.0 in /webapp [`#8334`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8334) +- build(deps): bump express from 4.21.2 to 5.1.0 in /backend [`#8322`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8322) +- build(deps): bump mime-types from 2.1.35 to 3.0.1 in /backend [`#8298`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8298) +- feat(backend): lint rules [`#8339`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8339) +- feat(backend): chat notify via email [`#8314`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8314) +- build(deps): Bump docker/metadata-action [`#8245`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8245) +- build(deps): Bump docker/build-push-action from 6.9.0 to 6.15.0 [`#8244`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8244) +- build(deps): bump docker/login-action from 3.3.0 to 3.4.0 [`#8271`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8271) +- build(deps): bump sass from 1.85.0 to 1.86.3 in /webapp [`#8333`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8333) +- build(deps): bump validator from 13.12.0 to 13.15.0 in /webapp [`#8305`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8305) +- build(deps-dev): bump @faker-js/faker from 9.5.0 to 9.6.0 [`#8257`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8257) +- build(deps-dev): bump @faker-js/faker from 9.5.0 to 9.6.0 in /webapp [`#8262`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8262) +- build(deps): bump crazy-max/ghaction-github-pages from 4.1.0 to 4.2.0 [`#8328`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8328) +- build(deps): bump peter-evans/repository-dispatch [`#8330`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8330) +- build(deps): Bump actions/checkout from 4.2.1 to 4.2.2 [`#8247`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8247) +- build(deps-dev): bump eslint-config-prettier in /webapp [`#8259`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8259) +- build(deps-dev): bump eslint-import-resolver-typescript in /backend [`#8326`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8326) +- build(deps): bump actions/cache from 4.2.1 to 4.2.3 [`#8287`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8287) +- build(deps-dev): bump @types/node from 22.13.5 to 22.14.0 in /backend [`#8325`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8325) +- build(deps-dev): bump typescript from 5.7.3 to 5.8.3 in /backend [`#8324`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8324) +- build(deps): bump the babel group across 1 directory with 2 updates [`#8296`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8296) +- build(deps-dev): bump @faker-js/faker from 9.5.0 to 9.6.0 in /backend [`#8268`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8268) +- build(deps-dev): Bump ts-jest in /backend in the typescript group [`#8237`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8237) +- build(deps): bump helmet from 8.0.0 to 8.1.0 in /backend [`#8284`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8284) +- build(deps): bump validator from 13.12.0 to 13.15.0 in /backend [`#8297`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8297) +- build(deps): bump sanitize-html from 2.14.0 to 2.15.0 in /backend [`#8285`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8285) +- build(deps): bump the metascraper group across 1 directory with 12 updates [`#8327`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8327) +- build(deps-dev): bump @babel/core in the babel group [`#8277`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8277) +- build(deps-dev): bump prettier from 3.5.2 to 3.5.3 in /webapp [`#8260`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8260) +- build(deps-dev): bump eslint-plugin-vue from 9.32.0 to 9.33.0 in /webapp [`#8264`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8264) +- build(deps-dev): bump prettier from 3.5.2 to 3.5.3 in /backend [`#8266`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8266) +- fix(other): leftover hetzner deployment [`#8254`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8254) +- refactor(backend): comment on observed post notification [`#8311`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8311) +- feat(frontend): observe posts [`#8293`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8293) +- expose port so the mailserver can actually be used [`#8319`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8319) +- build(deps-dev): bump eslint-config-prettier in /backend [`#8269`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8269) +- build(deps-dev): bump eslint-plugin-prettier in /backend [`#8300`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8300) +- build(deps): bump amannn/action-semantic-pull-request [`#8303`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8303) +- build(deps): bump actions/setup-node from 4.2.0 to 4.3.0 [`#8288`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8288) +- build(deps): bump actions/upload-artifact from 4.6.1 to 4.6.2 [`#8289`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8289) +- fix(backend): typo in groups resolver [`#8318`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8318) +- fix frontend package lock [`#8316`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8316) +- migration adds OBSERVES relation between author and post [`#8307`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8307) +- fix(backend): users observing post count [`#8295`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8295) +- feat(webapp): track online status [`#8312`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8312) +- Replace argfile by slurpfile [`#8309`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8309) +- build(deps-dev): Bump cypress from 14.0.3 to 14.1.0 in the cypress group [`#8229`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8229) +- provide information of the observing status of the post after creating a comment [`#8310`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8310) +- feat(backend): observe posts [`#8292`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8292) +- release: also update helmchart versions [`#8256`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8256) + #### [3.2.1](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.2.0...3.2.1) +> 3 March 2025 + +- v3.2.1 [`#8248`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8248) - remove the requirement for non-existant job in publish workflow [`#8251`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8251) - removed dockerhub related stuff [`#8249`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8249) - feat(webapp): implement configurable custom button in header [`#8215`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8215) @@ -295,9 +614,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Bump dotenv from 16.3.1 to 16.4.4 [`#7010`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7010) - Bump dorny/paths-filter from 3.0.0 to 3.0.1 [`#7006`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7006) - chore(other): update packages 08.02 [`#75`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/75) -- update frontend packages [`fef8157`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/fef8157b0cdf9081678365e9a7f6339ef787423c) -- update frontend packages [`dce59c1`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/dce59c11089e23f3ed97dfbfea39507e7c098c30) -- Add converted SVG icons with properties 'fill' and 'stroke' on 'svg' tag [`b1a7e91`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/b1a7e916314a0d59a65d2cdb244e6117d796dda1) #### [3.2.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.1.2...3.2.0) @@ -453,9 +769,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - fix(other): fix link in `deployment/TODO-next-update.md` to `deployment/deployment-values.md` [`#6729`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6729) - Bump cypress from 13.2.0 to 13.3.1 [`#6789`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6789) - fix(other): remove buggy space in filename 'neo4j-data-snapshot.yaml ' to 'neo4j-data-snapshot.yaml' [`#6777`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6777) -- storybook tests [`ff7a1c5`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/ff7a1c548405c3a60341844679e70d93aebb2e3c) -- update vuepress [`cbf7bdc`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/cbf7bdc84ccf02c9658748b945dac7ebc405e5b2) -- update storybook [`a0b77d7`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/a0b77d78a3e7a483d204337e3d42dd9dd56ad69a) #### [3.1.2](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.1.1...3.1.2) @@ -464,9 +777,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - chore(release): release v3.1.2 - fix kubernetes deployment by degrade Node version from v20.7.0 to v20.2.0 [`#6782`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6782) - fix(other): kubernetes error by degrading node version from v20.7.0 to v20.2.0 [`#6779`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6779) - fix(backend): cypher statement in user locales unit test [`#6780`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6780) -- Release v3.1.2 - Fix Kubernetes Deployment by degrade Node version from v20.7.0 to v20.2.0 [`aa2d27e`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/aa2d27e8caefeac578fb30fc5b426dbd51e84b34) -- Fix Dockerfile labels [`5d2c41d`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/5d2c41d12ec9c892a38d8f8e2325194618fc35e4) -- Degrade Node version from v20.7.0 to v20.2.0 [`988e874`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/988e874934dcf0caa060600e843138664fb367c5) #### [3.1.1](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.1.0...3.1.1) @@ -496,9 +806,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - build(other): bump node from 20.2.0-alpine3.17 to 20.6.0-alpine3.17 in /backend [`#6710`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6710) - build(other): bump actions/cache from 3.3.1 to 3.3.2 [`#6714`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6714) - build(other): bump actions/checkout from 3 to 4 [`#6713`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6713) -- Bump @storybook/vue from 6.3.6 to 7.4.0 in /webapp [`8113d47`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/8113d47dfcbfa0a63ae1f035d9d24a4c548d2089) -- Revert "update cypress packages" [`2229baf`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/2229baff6fb74d92ed395fb90a1421f994368a20) -- update cypress packages [`dbedcf9`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/dbedcf901d3040d1a6a8db62170bdea86d020bb1) #### [3.1.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.0.4...3.1.0) @@ -507,9 +814,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - chore(release): release v3.1.0 – use of dkim in deployment [`#6733`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6733) - chore(other): use dkim values in helm script of backend [`#6731`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6731) - fix(other): add global package.json to workflow file filters [`#6706`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6706) -- Release v3.1.0 [`6276d87`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/6276d87c6a427438c5fbf56c0f7bc414293e671d) -- Fix super fluid spaces [`ecdaf83`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/ecdaf83ac57bd4186f064d891d2570ce2438d47f) -- Use DKIM values in Helm script of backend [`2ea98b1`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/2ea98b1900c1b33712a2456dde6ba3dfb6d95b00) #### [3.0.4](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.0.3...3.0.4) @@ -525,9 +829,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - build(other): bump @babel/preset-env from 7.9.5 to 7.22.9 in /backend [`#6578`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6578) - build(other): bump @babel/preset-env from 7.22.7 to 7.22.9 [`#6572`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6572) - build(other): bump dotenv from 8.6.0 to 16.3.1 [`#6483`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6483) -- Bump @babel/preset-env from 7.9.5 to 7.22.9 in /backend [`ae0c6f1`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/ae0c6f12bd86c39b33a2b48c2eeb0eccb880821c) -- Revert "Revert "Revert "update metascraper packages""" [`7fdc5e8`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/7fdc5e8f5ed8e191763acfcd74c510138145f612) -- Revert "Revert "update metascraper packages"" [`d5c1421`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/d5c142129291264d5508b3f32b8500c2451a5f39) #### [3.0.3](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.0.2...3.0.3) @@ -539,9 +840,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - fix(webapp): filtermenu mobile bug [`#6694`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6694) - fix(webapp): fix proxyapiurl in chat rooms for the avatars [`#6693`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6693) - fix(webapp): fix the group link in the map [`#6698`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6698) -- Bump @babel/core from 7.22.8 to 7.22.9 [`209390a`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/209390a7e026b03ee92f7b1ecb3b1a7b3b2e0232) -- Write documentation for DKIM e-mail setting [`9f5d32e`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9f5d32e527151854f1888614c59e0aa5f2b504d0) -- Release v3.0.3 [`7d761c2`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/7d761c262a6e7bf6382cc49148c55ac8037db12a) #### [3.0.2](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.0.1...3.0.2) @@ -550,9 +848,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - chore(release): release v3.0.2 – fix chat avatar error and wrong font in network [`#6674`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6674) - fix(webapp): fix wrong font in whole network comming from chat component [`#6672`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6672) - fix(webapp): fix 'm.avatar is null' error message [`#6671`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6671) -- Release v3.0.2 [`7bcad81`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/7bcad81785de39115ba03aa37fb26caeff7775f2) -- Remove font 'Quicksand' from the chat [`5b95419`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/5b954197dc93bd5715f657539ca1b9b32d7d557a) -- Fix 'm.avatar is null' error message [`2c12331`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/2c123313ed6267bb1c88470ba7ae3f4f693cfc15) #### [3.0.1](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.0.0...3.0.1) @@ -560,9 +855,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - chore(release): release v3.0.1 – fix chat avatars [`#6667`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6667) - fix(webapp): try to fix avatars [`#6660`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6660) -- Releasde v3.0.1 – fix chat avatars [`0daeb5b`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/0daeb5b957e11d206ceddc3e8b275929e1850d18) -- conditional url replacement [`5c1ab88`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/5c1ab880127a5f4c1b457598fafd3ae9fe1a0d12) -- try to fix avatars [`6f43321`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/6f43321abad945602529b962e536546ab541cd70) ### [3.0.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/2.7.0...3.0.0) @@ -656,9 +948,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Bump node from 20.2.0-alpine3.17 to 20.3.0-alpine3.17 in /webapp [`#6408`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6408) - fix(backend): typescript fix [`#6448`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6448) - refactor(backend): migrate completely to typescript [`#6434`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6434) -- remove package cypress-file-upload from e2e testing [`73f6bc6`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/73f6bc642194b0c73769d4e8d8e53645b6e80adf) -- fixed cypress [`5f545f3`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/5f545f3b8fc6927954e036b27ad2e123bcd36149) -- fix seed to not use promise all where easily refactored [`1b0f512`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/1b0f5124159033214f99bfbc4cebe9dfaa7dd76e) #### [2.7.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/2.6.0...2.7.0) @@ -716,9 +1005,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - feat(backend): event parameters [`#6198`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6198) - feat(backend): create and update posts with labels [`#6197`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6197) - feat(backend): add article label to posts [`#6196`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6196) -- Cypress: update packaage info [`b38769b`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/b38769b048e9cb9ca07862a61ea810f21b4ce82a) -- update cypress related packageges in package.json [`692ec2a`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/692ec2a11555600647ec8d95b8296c9869948b02) -- fixed coverage reporting [`540cd40`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/540cd40e10ec0461ef17379cb93d914839f3a84f) #### [2.6.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/2.5.1...2.6.0) @@ -752,9 +1038,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Bump @babel/core from 7.9.0 to 7.21.4 [`#6200`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6200) - Bump @babel/preset-env from 7.12.7 to 7.21.4 [`#6204`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6204) - Bump expect from 25.3.0 to 29.5.0 [`#6098`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6098) -- separate test workflows [`3533a36`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/3533a36cdc811c0e1dae218fbc2184f7c4bc3951) -- get it working [`8df7d5d`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/8df7d5d265b0c5ba16f167a213631d765d2f985e) -- feat(webapp): group categories on posts [`3244f3f`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/3244f3f86d1e8c09e0fd49f43c49f0a3aa8b85ab) #### [2.5.1](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/2.5.0...2.5.1) @@ -763,9 +1046,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - chore(other): release v2.5.1 – fix filter menu width [`#6180`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6180) - feat(webapp): add tooltips to all menu icons [`#6185`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6185) - fix(webapp): popup filter max-width [`#6177`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6177) -- Add tooltip to header notifications menu [`28505a5`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/28505a5b181008ebcde6fa58b7a4a8459a492018) -- Add tooltip to header avatar menu [`4c0469f`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/4c0469f61a3c2fae23e50c6a5a2a91b63fac149a) -- Release v2.5.1 - fix filter menu width [`08def14`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/08def14cafef7816d8e43f1896430400bda9635d) #### [2.5.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/2.4.0...2.5.0) @@ -885,9 +1165,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - brand_as_default [`#4`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4) - limit_replicasets [`#3`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/3) - Deployment [`#2`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/2) -- moved example into stage.ocelot.social [`61b5112`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/61b5112b8b547a7491d644c7c4dbfead39b61d79) -- feat(backand): upgrade jest to 29.4 [`4390d72`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/4390d72477fb941f69bc9bdc24ac7713ef06e827) -- pages tests nearly working [`4850e45`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/4850e456fe5b7c158f23acc7f153576472604300) #### [2.4.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/2.3.0...2.4.0) @@ -910,9 +1187,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - fix(backend): do not expose registered emails on registration [`#5909`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5909) - refactor(backend): node 19 with fixed image upload [`#5897`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5897) - refactor(webapp): nump docker version to `16.19.0` in webapp & maintenance [`#5842`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5842) -- updated required packages for node19, fix fs-capacitator [`11087cb`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/11087cbaefde604668ac192b710666df09cb813c) -- fixed build error [`3889204`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/3889204f871df97b307401032900db7940913038) -- linting [`8bfe486`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/8bfe486034badd55a8096982f81aba08207b9e83) #### [2.3.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/2.2.0...2.3.0) @@ -931,9 +1205,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - feat(other): semantic pullrequest workflow [`#5634`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5634) - fix: Cannot Add Group Members as New Members to Group [`#5635`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5635) - fix: My Groups Count Includes Pending Membership [`#5631`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5631) -- frontend: jest coverage [`f57e11d`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/f57e11dd021dbb156b33fbd5538cf5ca32df7334) -- reverted yarn lock [`a01aee8`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/a01aee89a92bbc9885e2895c577eb3c63042ba22) -- reverted all package updates [`a520089`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/a5200893f4508d45619447231b6789178a51daf7) #### [2.2.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/2.1.0...2.2.0) @@ -953,9 +1224,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - fix: 🍰 Fix Group Teaser [`#5584`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5584) - feat: 🍰 List All Groups [`#5582`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5582) - feat: 🍰 Header Logo Routing Update [`#5579`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5579) -- add header menu to component, central variabl for screen width [`401f59a`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/401f59ae8de5b1c27f0e26e1f71778d3257d2180) -- comment out LanguagesFilter, EmotionsFilter, fix tests, fix lint [`52dcd77`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/52dcd772fa81e02a0d95e89a9fc8232e70a09d28) -- fix lint [`15561cb`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/15561cb94f8768e93846c25945c935ae83977553) #### [2.1.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/2.0.0...2.1.0) @@ -965,9 +1233,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - feat: 🍰 EPIC Groups [`#5132`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5132) - chore: 🍰 Remove Group Branchs `5059-epic-groups` Separate Auto-Deployment [`#5552`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5552) - fix: [WIP] 🍰 Long Words Are Being Wrapped Now [`#5559`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5559) -- Remove groups separate auto-deployment [`c8d8168`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/c8d816887b2d49293d1b8ee2805d452fe10d907e) -- Release v2.1.0 [`dc085e9`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/dc085e9e69b288fce6dd06e8d7eb05ef34bd9a7b) -- Add database migration to auto-deployment on publish [`ef06f1a`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/ef06f1a67d2654aaeb55d0434d3324a3ac37a380) ### [2.0.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.1.1...2.0.0) @@ -999,9 +1264,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - feat: 🍰 Implement `JoinGroup`, `GroupMember`, `SwitchGroupMemberRole` Resolvers [`#5199`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5199) - chore: 🍰 Add Groups To Seeding [`#5185`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5185) - feat: 🍰 Implement Group GQL Model And CRUD Resolvers – First Step [`#5139`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5139) -- Refine design and functionality of group list and create, edit group [`7b11122`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/7b11122bea4868624dd1c1641219e71070412e20) -- improved code and tests as suggested by @tirokk, thanks for the great review! [`631f34a`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/631f34a2e5224d68279337a92e7535794b670d70) -- implement and test post visibilty when leaving or changing the role in a group [`76bfe48`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/76bfe484768cf9b20b2dced865d5d3e3eb999235) #### [1.1.1](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.1.0...1.1.1) @@ -1030,9 +1292,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Bump cross-env from 7.0.2 to 7.0.3 in /webapp [`#5168`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5168) - chore: 🍰 Add `--logHeapUsage` To Jest Test Call [`#5182`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5182) - refactor: 🍰 Rename `UserGroup` To `UserRole` [`#5143`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5143) -- add new yunite icons [`bb0d632`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/bb0d6329e7e36ea03671318ea8dd128a6d5a5a7a) -- cleanup refactor rebranding [`5f5c0fa`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/5f5c0faa1f28cd4df7681eba335ae5998b2d9cca) -- change color and scss in branding [`52070b8`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/52070b8c570970bf48df561134bf67cb4111b640) #### [1.1.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.0.9...1.1.0) @@ -1042,9 +1301,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - feat: Make Categories Optional [`#5102`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5102) - Update issue templates [`#5101`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5101) - chore: 🍰 Betters Automatic Deployment To `stage.ocelot.social` On Push To `master` Branch [`#5097`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5097) -- add optional categories to teaser and post [`bc95500`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/bc955003f7c33aabe592bee782aca973b4f00cba) -- env vatiable for CATEGORIES_ACTIVE and switch for categories in contribution form [`e31f250`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/e31f250ea5e1949f4f08e72fe82622d41ecd85f1) -- fix some tests [`5393c2a`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/5393c2aeaaf070a637390c430d5f03057030ff52) #### [1.0.9](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.0.8...1.0.9) @@ -1058,9 +1314,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - chore: 🍰 Change `image` Entries In Docker Compose Files And Fix Apple M1 Problem [`#5073`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5073) - chore: 🍰 Rename Neo4j Docker Image In General To `neo4j-community:*` [`#5078`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5078) - chore: 🍰 Fix `ocelotsocialnetwork/webapp:latest` And `ocelotsocialnetwork/backend:latest` On Start In Cluster [`#5076`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5076) -- Add documentation for Apple M1 Docker Compose override files [`2f3f37c`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/2f3f37c158cfc9b300540d3c8f016548b15a5277) -- Add documentation for Docker build analyzes [`fbbcc5b`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/fbbcc5bb854d53b5fa658b83d56d381a3cbc2b1a) -- Implement DigitalOcean Kubernetes deployment on publishing [`485e698`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/485e6986b88a14db5ab75ed12bab5cdc73592ca6) #### [1.0.8](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.0.7...1.0.8) @@ -1077,9 +1330,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - bug: 🍰 Replace Deleted Faker Package [`#4973`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4973) - fix: 🍰 Change Tip Tap Editor Legend Hover To Click [`#4911`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4911) - fix: 🍰 Fix Embed iframe Width And Height CSS [`#4897`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4897) -- Implement MySomethingList for social media, use list item slot [`d3cc49d`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/d3cc49d37ba260f9a285c078c57e673a32a76732) -- Split social media page and list component [`b740033`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/b7400339aba22d5fb5506dc3b25f082d7f09edfc) -- Remove input addSocialMedia [`58464fd`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/58464fd53ef6aab52af1c2477c2615648ad889e3) #### [1.0.7](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.0.6...1.0.7) @@ -1096,9 +1346,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - doc: 🍰 Update README.md Etc. [`#4733`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4733) - feat: 🍰 New CSS For Internal Pages [`#4741`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4741) - fix: 🍰 Change Notification E-Mails Settings Page Link [`#4742`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4742) -- Refactor internal pages to new CSS [`acad80c`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/acad80c3c8262934dd2e38961c08c0fde769099a) -- Renew JWT in decode test [`46eb6b8`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/46eb6b82ea802d4d6ca7294cd32d1fe16425bfea) -- Revert "Renew JWT in decode test" only for changing the Neode version [`a0d92b4`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/a0d92b4853d09d725c1fb7886cbfed2a00e1f05c) #### [1.0.6](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.0.5...1.0.6) @@ -1111,9 +1358,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Bump codecov from 3.7.1 to 3.8.2 [`#4401`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4401) - Centered the login-form [`#4660`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4660) - Spelling Change [`#4654`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4654) -- suggested solutions [`5699620`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/5699620c858925101f561f315034c57fae878964) -- Implement or move sort by date filter in filter menu [`16d8a42`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/16d8a42d9b4f90e8a1c22da0637f95adb25b4e8f) -- Translate order by [`245be52`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/245be52ac8d9e41998da10cf2d1a752bbefc0c0c) #### [1.0.5](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.0.4...1.0.5) @@ -1172,9 +1416,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - feat: 🍰 Post Editor Legend [`#4492`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4492) - New Issue type EPIC [`#4536`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4536) - Upgrade to GitHub-native Dependabot [`#4399`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4399) -- Remove superfluous package-lock.json [`c7e4e6d`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/c7e4e6d2b26d448b3d2787aff4a0bde32c24e919) -- Delete superfluous and always conflicting file package-lock.json [`9932796`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9932796d233789c76b917de533ec2efc99f6aa4a) -- Refactor pageParams, second step [`e8a0a5d`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/e8a0a5d13c0610066c50c98d5e0d661ee8139217) #### [1.0.4](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.0.3...1.0.4) @@ -1186,9 +1427,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - feat: 🍰 Flexible Footer Links [`#4468`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4468) - docs: 🍰 Correct 'Contribution.md' [`#4466`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4466) - docs: 🍰 Correct Discord Links And Divers [`#4461`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4461) -- Implement flexible page footer links [`1bd4af6`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/1bd4af6fd3b5db167575910948a0a72461a1129a) -- Implement tests for flexible page footer links [`627a20f`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/627a20f66a65450996a5fe3128fd37769fdfd629) -- Correct Discord links and divers [`0318910`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/0318910488e245c4a1d09181265de63d05a89cf1) #### [1.0.3](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.0.2...1.0.3) @@ -1204,18 +1442,12 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - frontend + backend coverage tests [`#4367`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4367) - refactor: 🍰 Refactor E-Mail Templates [`#4350`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4350) - feat: 🍰 Remove More-Info Of Post [`#4316`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4316) -- have cypress running locally - the tests still fail [`e3e0341`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/e3e03415e1bd9e0be88f33930a52e63d8af64ee1) -- have cypress running locally - the tests still fail [`0ec0574`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/0ec05743751cfef0cb86c17b87e9e4ef9a2c9e47) -- Refactor logos, first step [`ff6cc30`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/ff6cc306aff6150a924f1a647387e498d050ea9f) #### [1.0.2](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.0.1...1.0.2) > 6 April 2021 - fix: Email Confirmation-Link When An Invite-Code Is Given [`#4336`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4336) -- release v1.0.2 [`e583010`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/e5830101e4b449905fe2d0018627d75af62b2a20) -- slider jumps to enter-nonce when link contains invite-code, email and nonce and method is invite-code. Thanks @tirokk [`c80b3a2`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/c80b3a212835f76cfc2f11542345d6c3b226995b) -- fix enail confirmation link when an invite-code is given [`27f0de9`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/27f0de9464685c8f960bde9d07986fdc5b20f8f9) #### [1.0.1](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.0.0...1.0.1) @@ -1225,9 +1457,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - jwt_expiretime [`#4330`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4330) - Clean env, docker & workflow [`#4337`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4337) - fix: 🍰 Fixing The Avatars unwanted Border [`#4320`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4320) -- Change background color of avatar image to white [`e48a99a`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/e48a99afe63e96cffafe16db7bf5ae35cfdebd7a) -- removed config warning [`f6c070a`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/f6c070a3cb36c5593123b27b4d1b6a5b7a10aba3) -- include env files in build process [`dfe6f67`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/dfe6f679b3bb0f440aee986d6d12925cccee9050) ### [1.0.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.15...1.0.0) @@ -1241,9 +1470,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - fix: JWT Expires In 2 Years [`#4278`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4278) - Deployment [`#4263`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4263) - feat: 🍰 Redesign Registration Process Frontend [`#4168`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4168) -- Delete unnecessary code [`3fa7e04`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/3fa7e04d4895161db1f764ccad58e35188c9d065) -- basic invite button in frontend [`356f026`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/356f02622655a57561fcdecfcc8a735d23cac79f) -- setting up invite button [`e6dc3f4`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/e6dc3f42cedaf9953d737cf30cf7ed317b634be7) #### [0.6.15](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.14...0.6.15) @@ -1255,18 +1481,12 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - dashboard restructuring image [`#4266`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4266) - fix: 🐛 Adapted Editor List Styles [`#4239`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4239) - fix: Scrolling On Profile Page [`#4234`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4234) -- release 0.6.15 [`ef4265d`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/ef4265d9387d94aa09db2b80461c2ec90b4623ae) -- count views of post teaser [`1c3f628`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/1c3f628fb2e161400319b32da274952c1b57836e) -- tests fixed for clickedCount [`96066ea`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/96066eae02e659a00bce280f9f97a28ac1446ce0) #### [0.6.14](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.13...0.6.14) > 17 February 2021 - fix: Add Null Migration [`#4233`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4233) -- release 0.6.14 [`394860c`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/394860c1d0d6ff23f6653b81288890a67720deab) -- add null mutation [`a7489a0`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/a7489a044c4b4b556f6b26d555657478ebd6409b) -- fixed misspelling in changelog [`3ad6d73`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/3ad6d738fd5d9ed25c0cccc6ae60ed5cc37540ba) #### [0.6.13](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.12...0.6.13) @@ -1274,9 +1494,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - fix: css so follow button isn't blocked by adblock [`#4230`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4230) - fix: Query Available Roles As Admin [`#4225`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4225) -- release 0.6.13 [`e2503d4`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/e2503d496606aa79b5ab59319f76466e04a79bd0) -- avoid introspection to get available roles as admin [`a5df793`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/a5df793c55ec8792066e61547ae287f7702675fd) -- css fix to trick adblock on follow button by @nila99 [`13931c9`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/13931c90e7642da93de0e884afd1649de8181c6c) #### [0.6.12](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.11...0.6.12) @@ -1284,8 +1501,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Smtp secure option [`#4223`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4223) - chore: 🐛 Fix Migrations By Migrations Folder Having .gitkeep [`#4222`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4222) -- SMTP_SECURE option [`8e2d8a7`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/8e2d8a77c3a825880deb4c668d647ee6503edc7f) -- New file .gitkeep in migrations [`814c1b8`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/814c1b88aed2e6e353f2af2f277229629cc2e788) #### [0.6.11](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.10...0.6.11) @@ -1294,16 +1509,12 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - chore: 🍰 Resolve WEBSOCKETS_URI .env Problem [`#4219`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4219) - chore: 🐛 'db:migrate up' By Moving Examples Outside Of Migrations Folder [`#4221`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4221) - fix: Location Tests In Backend [`#4220`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4220) -- MAPBOX introduced district for US-Cities. So I used a German City instead [`7a31334`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/7a313344cbddaca7bcb073b01a549bfe2a5c3852) -- Resolve WEBSOCKETS_URI config problem [`ac27d6a`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/ac27d6ac9653046d98e559fe1532648d7ddfa92e) -- Fix 'db:migrate up' by moving examples outside of migrations folder [`063c730`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/063c730e5fb3e3ea363deb5adebdbf69589ec7b4) #### [0.6.10](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.9...0.6.10) > 12 February 2021 - Use original images [`#4217`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4217) -- original images instead of broken whitelabled ones [`2303b92`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/2303b92146d71d8ec12b6fa75a4a0c513db96594) #### [0.6.9](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.8...0.6.9) @@ -1311,25 +1522,18 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - fix_webapp_production [`#4216`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4216) - Move Old Migrations From HC To Examples Folder To Avoid Conflicts [`#4215`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4215) -- typo [`7d21196`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/7d21196973282ce904db8776fbef8e0961dac6e5) -- fixed prodution stage of webapp container [`02d31d0`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/02d31d0a94aa73a5026d2d9c183c63d1a7e43b6c) -- moved old migrations from HC to examples folder to avoid conflicts with new migrations [`efec46d`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/efec46d9d485ec439a25adcac6da105683a221ad) #### [0.6.8](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.7...0.6.8) > 11 February 2021 - refactor: 🍰 Remove Emojis From Post Page [`#4208`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4208) -- release 0.6.8 [`b947918`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/b94791873bb3c9524daff78fc9d0f7bdb385127d) -- Refinied design [`58728df`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/58728df97b3a21d34e9476f2be139add33c75b42) -- remove emojis from post page [`4097cc1`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/4097cc16f723dfd2af64c4adcfef978b1c81e07b) #### [0.6.7](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.6...0.6.7) > 10 February 2021 - Support newest docker [`#4210`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4210) -- push all tags again, since docker is updated on github (...) [`c491fd6`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/c491fd692155b16822426372c58b5770daf2c0aa) #### [0.6.6](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.5...0.6.6) @@ -1339,9 +1543,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - feat: 🍰 Switch User Role As Admin [`#4136`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4136) - feat: Image Cropping Is Optional [`#4199`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4199) - publish workflow [`#4195`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4195) -- change user roles is working, test fails [`c528269`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/c528269cb2972e6ea937d31ba22d0e11168141f2) -- file upload: refactored [`650e83f`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/650e83f4c250389477933a2e7d21d8245b0ce882) -- change user role: tests are working [`14dfe2a`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/14dfe2ae2cd4a24c06c9229893b33586dfceae4f) #### [0.6.5](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.4...0.6.5) @@ -1366,9 +1567,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - feat: 🍰 Allow Only Supported Image File Formats [`#3928`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/3928) - refactor: Disbale Emoji, Language And Catgeory Filter [`#4193`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4193) - refactor: Remove Catgeories From Post Teaser [`#4191`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4191) -- - adjusted changelog to ocelot-social repo [`9603882`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9603882edebf8967e05abfa94e4e1ebf452d4e24) -- - first steps towards docker image deployment & github autotagging [`5503216`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/5503216ad4a0230ac533042e4a69806590fc2a5a) -- - lots of additional tests [`0ba37aa`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/0ba37aab18f537d722aede7b87fa0b8e79f80e66) #### [0.6.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/v0.6.0...0.6.0) @@ -1547,9 +1745,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - build(deps-dev): bump babel-jest from 25.2.3 to 25.2.4 in /backend [`#3363`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/3363) - build(deps-dev): bump expect from 25.2.3 to 25.2.4 [`#3360`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/3360) - build(deps-dev): bump auto-changelog from 1.16.2 to 1.16.3 [`#3334`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/3334) -- snapshot changed / facebook link stays the same for testing purposes [`71f4e2b`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/71f4e2bd6e8427de8ddab410ef0f8fc476309bf8) -- design data up to now [`72b8c22`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/72b8c2263496ac640ce4cba60da2159092ad2ae0) -- Change maintenance svg [`11aabf0`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/11aabf0b16ad69fa9d3e84a1628775d357c24985) #### [v0.6.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/v0.5.0...v0.6.0) @@ -1575,9 +1770,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - build(deps-dev): bump babel-jest from 25.2.0 to 25.2.1 in /backend [`#3341`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/3341) - build(deps-dev): bump jest from 25.1.0 to 25.2.1 in /webapp [`#3342`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/3342) - feat(backend): upload original image files on S3 object storage [`#3262`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/3262) -- refactor: CategoriesFilter to not use ds-flex [`1ffde6b`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/1ffde6bf1034d1a1d9b7bca62fe66fe64527314c) -- chore: fix lint w/ new linting rules [`06985c2`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/06985c2dbbc6699cc5c3dfd6e55cac8d28e9019b) -- chore: Fix lint w/ new linting rules [`cb0a3f5`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/cb0a3f5cdd5e7fe47f46ae08798628eecb28e9b5) #### [v0.5.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/v0.4.2...v0.5.0) @@ -1632,9 +1824,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - build(deps-dev): bump date-fns from 2.10.0 to 2.11.0 [`#3273`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/3273) - build(deps): [security] bump acorn from 6.3.0 to 6.4.1 in /backend [`#3270`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/3270) - build(deps): [security] bump acorn from 6.1.1 to 6.4.1 [`#3269`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/3269) -- Update prettier to v2 [`276ea79`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/276ea79e8ff2de2d02698b486671aee7cfda860e) -- Changes requested by @mattwr18 [`9c08db2`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9c08db22dcd0ca1ad6e59be8fb0f287935b45537) -- search specs refactored [`46fca22`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/46fca229ec35047eda9ac7809e7bc456785a6c70) #### [v0.4.2](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/v0.4.1...v0.4.2) @@ -1667,9 +1856,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - build(deps): bump metascraper-description from 5.11.1 to 5.11.6 in /backend [`#3233`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/3233) - build(deps): bump cross-env from 7.0.1 to 7.0.2 in /backend [`#3245`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/3245) - build(deps): bump metascraper-title from 5.11.1 to 5.11.6 in /backend [`#3244`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/3244) -- DRY user.spec.js [`da16590`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/da165906e2ed12baddd902b43064103ab3adfa06) -- test deleteuser as admin, moderator, another user and as I myself, fix lint [`3983612`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/3983612c56ac92473a192a318959e4c691a3e7b8) -- feature: test delete user as admin [`84c1547`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/84c154798efac0cec4c13dfefae18a6a9542058a) #### [v0.4.1](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/v0.4.0...v0.4.1) @@ -1744,9 +1930,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - build(deps): bump sanitize-html from 1.21.1 to 1.22.0 in /backend [`#3145`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/3145) - build(deps): bump nodemailer from 6.4.2 to 6.4.3 in /backend [`#3144`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/3144) - build(deps): bump metascraper-video from 5.10.7 to 5.11.1 in /backend [`#3143`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/3143) -- build(deps-dev): bump @storybook/addon-notes in /webapp [`5ef2b25`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/5ef2b25ee6a3402a2ebe2f3f55dd65a6e0a1111e) -- build(deps-dev): bump @storybook/addon-a11y in /webapp [`f209436`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/f209436147fbf9afacfbd6edb6135847e6c4faed) -- Lokalise: update of webapp/locales/fr.json [`28e2967`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/28e2967119f1b47752afc959a4c3ae741fdad0b0) #### [v0.4.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/v0.3.1...v0.4.0) @@ -1811,9 +1994,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - fix: Increase body parser limit [`#3037`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/3037) - chore: Update to v0.3.1 [`#3035`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/3035) - fix(subscriptions): Don't publish undefined [`#3088`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/3088) -- Upgrade cypress, remove log out step [`0df4038`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/0df40386dd866c6b9ce540b966dfe00089507d31) -- use BaseCard in Comment component and refactor [`e7bf499`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/e7bf499d373ec9d5d46be20fd838fbc4dbecb273) -- Refactor GQL and tests, first approach [`f380915`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/f380915b2c679d42e5db136ea1d923cf00bbcf10) #### [v0.3.1](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/v0.3.0...v0.3.1) @@ -1866,9 +2046,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - fix: Remove github release script breaking build [`#2971`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/2971) - Use original createdAt for merged users/emails [`#2969`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/2969) - Fix typo [`#2966`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/2966) -- Get rid of different factory files [`fc36729`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/fc367297e3e054f09b7f8f31788ab68d87f6babf) -- Refactor factory for comments [`2fc71d7`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/2fc71d75a5d5eab9c3467e94e00257ef6dd7d8a0) -- Refactor user factory [`2a79c53`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/2a79c53765b73f9b91691eb75f55cf8c9e48306e) #### [v0.3.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.2.2...v0.3.0) @@ -1940,9 +2117,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - docs(deployment): Explain how to setup metrics [`#2825`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/2825) - refactor(styleguide): Migrate Avatar component to monorepo [`#2700`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/2700) - docs(deployment): Explain how to setup metrics (#2825) [`#2411`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/2411) [`#2777`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/2777) -- Improve styling per @alina-beck review [`bcc1ab1`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/bcc1ab167e8b1dfdac1ec0a05a0c14e8234bcabc) -- test(cypress): Cover "Pinned post" feature [`d49afc2`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/d49afc25cfa1c1f98ed04f78dd3ff826cd85ae25) -- build(deps-dev): bump @storybook/addon-notes in /webapp [`d6b78e4`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/d6b78e4794ac60dee9d9fd3e3c9c53a95fcea152) #### [0.2.2](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/v0.2.2...0.2.2) @@ -1965,9 +2139,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - build(deps): bump @hapi/joi from 17.0.2 to 17.1.0 in /backend [`#2846`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/2846) - Release 0.2.2 [`#2844`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/2844) - build(deps-dev): bump @storybook/addon-actions in /webapp [`#2842`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/2842) -- Convert block/unblock to blacklist/whitelist [`c297b83`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/c297b83f873edc61ddec370633b9b65896c56591) -- Rename blacklist/whitelist to mute/unmute [`ba3e9e1`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/ba3e9e1025bf432151c9bf1002045179b338ff7f) -- Generate changelog with auto-changelog [`02367f9`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/02367f93e0ad635d5f43adf01695f85f06f4c0d2) #### [v0.2.2](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.2.1...v0.2.2) @@ -1976,7 +2147,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - build(deps): bump metascraper-title from 5.10.3 to 5.10.5 in /backend [`#2835`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/2835) - build(deps): bump metascraper-publisher in /backend [`#2836`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/2836) - build(deps): bump metascraper-audio from 5.10.3 to 5.10.5 in /backend [`#2840`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/2840) -- chore(release): 0.2.2 [`7e26e56`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/7e26e56654d391b0f910a59e28ce67fd1ec0b4a8) #### [0.2.1](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/v0.2.1...0.2.1) @@ -2071,9 +2241,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - refactor(modules): Various import fixes [`#2773`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/2773) [`#2774`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/2774) - feat(webapp): Display deployed version in footer [`#1831`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/1831) - fix #2229 [`#2229`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/2229) -- build(deps-dev): bump @storybook/addon-actions in /webapp [`d0124bf`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/d0124bf2b4b4a641c9af76d6d2f7b5aa075ade90) -- refactor and use base-button in SearchableInput [`fcbe612`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/fcbe6125f35c0dd23e2ba1ae63f539f5ef5990ea) -- manage button states and color schemes with mixin [`1b9249c`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/1b9249c685e34eb2e94b31ee0ec22421c6aa6a73) #### [v0.2.1](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.2.0...v0.2.1) @@ -2717,9 +2884,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - feat(webapp): Display deployed version in footer [`#1831`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/1831) - fix #2229 [`#2229`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/2229) - fixes #2659 [`#2659`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/2659) -- locales sorted. [`fa906ef`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/fa906efb1f40dc5bd80c9678f33c7b607a320099) -- sorting locations files yarn run locales --fix [`3343e14`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/3343e1435f320e1d403e16f5d4563aa11cae49a9) -- tests user.spec.js tests once clean once broken always alternating [`9d5c192`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9d5c192869ec0b088fa071d827b27cf6ca442b78) #### [v0.2.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.1.13...v0.2.0) @@ -3417,9 +3581,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - feat(webapp): Display deployed version in footer [`#1831`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/1831) - fix #2229 [`#2229`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/2229) - fixes #2659 [`#2659`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/2659) -- tests user.spec.js tests once clean once broken always alternating [`9d5c192`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9d5c192869ec0b088fa071d827b27cf6ca442b78) -- test check test WIP [`a4be3f7`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/a4be3f7adfc36ba7517a0f5399606b1f3d3dccfe) -- fix lint: tests and lint ok [`d2a43ce`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/d2a43cecd74ee3bec291109a85ddb6986315763b) #### [v0.1.13](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.1.12...v0.1.13) @@ -4138,9 +4299,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - feat(webapp): Display deployed version in footer [`#1831`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/1831) - fix #2229 [`#2229`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/2229) - fixes #2659 [`#2659`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/2659) -- Get rid of different factory files [`fc36729`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/fc367297e3e054f09b7f8f31788ab68d87f6babf) -- tests user.spec.js tests once clean once broken always alternating [`9d5c192`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9d5c192869ec0b088fa071d827b27cf6ca442b78) -- Refactor factory for comments [`2fc71d7`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/2fc71d75a5d5eab9c3467e94e00257ef6dd7d8a0) #### [v0.1.12](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.1.11...v0.1.12) @@ -4968,9 +5126,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Merge pull request #2443 from Human-Connection/2237-longer-comments [`#2237`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/2237) - fix #2329: Normalize email on login in the backend [`#2329`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/2329) - Fix #2294 [`#2294`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/2294) -- sorting locations files yarn run locales --fix [`3343e14`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/3343e1435f320e1d403e16f5d4563aa11cae49a9) -- tests user.spec.js tests once clean once broken always alternating [`9d5c192`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9d5c192869ec0b088fa071d827b27cf6ca442b78) -- test check test WIP [`a4be3f7`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/a4be3f7adfc36ba7517a0f5399606b1f3d3dccfe) #### [v0.1.11](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.1.10...v0.1.11) @@ -5849,9 +6004,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Fix #2294 [`#2294`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/2294) - Merge pull request #2078 from Human-Connection/fix-2042-back-link [`#2042`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/2042) - Fix #2042 Back Link To Login Page [`#2042`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/2042) -- locales sorted. [`fa906ef`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/fa906efb1f40dc5bd80c9678f33c7b607a320099) -- tests user.spec.js tests once clean once broken always alternating [`9d5c192`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9d5c192869ec0b088fa071d827b27cf6ca442b78) -- test check test WIP [`a4be3f7`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/a4be3f7adfc36ba7517a0f5399606b1f3d3dccfe) #### [v0.1.10](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.1.9...v0.1.10) @@ -6783,9 +6935,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Fix #2294 [`#2294`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/2294) - Merge pull request #2078 from Human-Connection/fix-2042-back-link [`#2042`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/2042) - Fix #2042 Back Link To Login Page [`#2042`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/2042) -- sorting locations files yarn run locales --fix [`3343e14`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/3343e1435f320e1d403e16f5d4563aa11cae49a9) -- tests user.spec.js tests once clean once broken always alternating [`9d5c192`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9d5c192869ec0b088fa071d827b27cf6ca442b78) -- test check test WIP [`a4be3f7`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/a4be3f7adfc36ba7517a0f5399606b1f3d3dccfe) #### [v0.1.9](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.1.8...v0.1.9) @@ -7774,9 +7923,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Fix #2042 Back Link To Login Page [`#2042`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/2042) - Merge pull request #2043 from Human-Connection/fix-1993 [`#1993`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/1993) - fix #1993 [`#1993`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/1993) -- sorting locations files yarn run locales --fix [`3343e14`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/3343e1435f320e1d403e16f5d4563aa11cae49a9) -- tests user.spec.js tests once clean once broken always alternating [`9d5c192`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9d5c192869ec0b088fa071d827b27cf6ca442b78) -- test check test WIP [`a4be3f7`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/a4be3f7adfc36ba7517a0f5399606b1f3d3dccfe) #### [v0.1.8](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.1.7...v0.1.8) @@ -8779,9 +8925,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Fix #2042 Back Link To Login Page [`#2042`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/2042) - Merge pull request #2043 from Human-Connection/fix-1993 [`#1993`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/1993) - fix #1993 [`#1993`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/1993) -- sorting locations files yarn run locales --fix [`3343e14`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/3343e1435f320e1d403e16f5d4563aa11cae49a9) -- tests user.spec.js tests once clean once broken always alternating [`9d5c192`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9d5c192869ec0b088fa071d827b27cf6ca442b78) -- test check test WIP [`a4be3f7`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/a4be3f7adfc36ba7517a0f5399606b1f3d3dccfe) #### [v0.1.7](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.1.6...v0.1.7) @@ -9792,9 +9935,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Fix #2042 Back Link To Login Page [`#2042`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/2042) - Merge pull request #2043 from Human-Connection/fix-1993 [`#1993`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/1993) - fix #1993 [`#1993`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/1993) -- sorting locations files yarn run locales --fix [`3343e14`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/3343e1435f320e1d403e16f5d4563aa11cae49a9) -- tests user.spec.js tests once clean once broken always alternating [`9d5c192`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9d5c192869ec0b088fa071d827b27cf6ca442b78) -- test check test WIP [`a4be3f7`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/a4be3f7adfc36ba7517a0f5399606b1f3d3dccfe) #### [v0.1.6](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.1.5...v0.1.6) @@ -10831,9 +10971,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Fix #2042 Back Link To Login Page [`#2042`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/2042) - Merge pull request #2043 from Human-Connection/fix-1993 [`#1993`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/1993) - fix #1993 [`#1993`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/1993) -- locales sorted. [`fa906ef`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/fa906efb1f40dc5bd80c9678f33c7b607a320099) -- sorting locations files yarn run locales --fix [`3343e14`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/3343e1435f320e1d403e16f5d4563aa11cae49a9) -- Get rid of different factory files [`fc36729`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/fc367297e3e054f09b7f8f31788ab68d87f6babf) #### [v0.1.5](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.1.4...v0.1.5) @@ -11920,9 +12057,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Fix #2042 Back Link To Login Page [`#2042`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/2042) - Merge pull request #2043 from Human-Connection/fix-1993 [`#1993`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/1993) - fix #1993 [`#1993`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/1993) -- tests user.spec.js tests once clean once broken always alternating [`9d5c192`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9d5c192869ec0b088fa071d827b27cf6ca442b78) -- test check test WIP [`a4be3f7`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/a4be3f7adfc36ba7517a0f5399606b1f3d3dccfe) -- fix lint: tests and lint ok [`d2a43ce`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/d2a43cecd74ee3bec291109a85ddb6986315763b) #### [v0.1.4](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.1.3...v0.1.4) @@ -13039,9 +13173,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Fix #2042 Back Link To Login Page [`#2042`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/2042) - Merge pull request #2043 from Human-Connection/fix-1993 [`#1993`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/1993) - fix #1993 [`#1993`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/1993) -- remove package-lock.json [`3cf3c31`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/3cf3c31808dc6ae59fb9c6ec33e9e178c5556438) -- tests user.spec.js tests once clean once broken always alternating [`9d5c192`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9d5c192869ec0b088fa071d827b27cf6ca442b78) -- test check test WIP [`a4be3f7`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/a4be3f7adfc36ba7517a0f5399606b1f3d3dccfe) #### [v0.1.3](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.1.2...v0.1.3) @@ -14170,9 +14301,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Fix #2042 Back Link To Login Page [`#2042`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/2042) - Merge pull request #2043 from Human-Connection/fix-1993 [`#1993`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/1993) - fix #1993 [`#1993`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/1993) -- remove package-lock.json [`3cf3c31`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/3cf3c31808dc6ae59fb9c6ec33e9e178c5556438) -- tests user.spec.js tests once clean once broken always alternating [`9d5c192`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9d5c192869ec0b088fa071d827b27cf6ca442b78) -- test check test WIP [`a4be3f7`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/a4be3f7adfc36ba7517a0f5399606b1f3d3dccfe) #### [v0.1.2](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.1.1...v0.1.2) @@ -15352,9 +15480,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Fix #2042 Back Link To Login Page [`#2042`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/2042) - Merge pull request #2043 from Human-Connection/fix-1993 [`#1993`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/1993) - fix #1993 [`#1993`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/1993) -- tests user.spec.js tests once clean once broken always alternating [`9d5c192`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9d5c192869ec0b088fa071d827b27cf6ca442b78) -- test check test WIP [`a4be3f7`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/a4be3f7adfc36ba7517a0f5399606b1f3d3dccfe) -- fix lint: tests and lint ok [`d2a43ce`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/d2a43cecd74ee3bec291109a85ddb6986315763b) #### [v0.1.1](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.1.0...v0.1.1) @@ -16600,9 +16725,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Merge pull request #1641 from Human-Connection/1639_no_limits_for_post_length [`#1639`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/1639) - Fix #1639 [`#1639`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/1639) - Add slug to User component, fix #1486 [`#1486`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/1486) -- sorting locations files yarn run locales --fix [`3343e14`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/3343e1435f320e1d403e16f5d4563aa11cae49a9) -- tests user.spec.js tests once clean once broken always alternating [`9d5c192`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9d5c192869ec0b088fa071d827b27cf6ca442b78) -- test check test WIP [`a4be3f7`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/a4be3f7adfc36ba7517a0f5399606b1f3d3dccfe) #### v0.1.0 @@ -17741,6 +17863,3 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Merge pull request #104 from DakshMiglani/master [`#41`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/41) - Merge pull request #93 from Gerald1614/500_error_on_login [`#49`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/49) - Update schema.graphql [`#7`](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/7) -- Fix resolve function returns undefind [`657a5ac`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/657a5ac1f59861a205ddfc21bc72ff3801c8fad0) -- Change strategy, only build docker image [`d6b7374`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/d6b7374ddbf497bdb5cbc935b88ae085c38b3237) -- Copy package.json from webapp/ [`f3a9996`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/f3a9996962e5dd8b2e365a032c1a5766fe666103) diff --git a/README.md b/README.md index b1fe0ea14..910eae5a4 100644 --- a/README.md +++ b/README.md @@ -187,10 +187,6 @@ $ cp .env.template .env # in folder backend/ $ cp .env.template .env -# in folder frontend/ -$ cp .env.template .env -``` - For Development: ```bash diff --git a/backend/.codecov.yml b/backend/.codecov.yml deleted file mode 100644 index 97bec0084..000000000 --- a/backend/.codecov.yml +++ /dev/null @@ -1,2 +0,0 @@ -coverage: - range: "60...100" diff --git a/backend/.dockerignore b/backend/.dockerignore index a0883bf4d..af1b934a3 100644 --- a/backend/.dockerignore +++ b/backend/.dockerignore @@ -17,5 +17,4 @@ build/ maintenance-worker/ neo4j/ -public/uploads/* !.gitkeep diff --git a/backend/.env.template b/backend/.env.template index d398c2265..e8c1f4168 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -6,18 +6,28 @@ NEO4J_PASSWORD=letmein GRAPHQL_URI=http://localhost:4000 CLIENT_URI=http://localhost:3000 -# EMail -EMAIL_SUPPORT="devops@ocelot.social" +# E-Mail default settings +SUPPORT_EMAIL="devops@ocelot.social" EMAIL_DEFAULT_SENDER="devops@ocelot.social" SMTP_HOST= SMTP_PORT= SMTP_IGNORE_TLS=true +SMTP_MAX_CONNECTIONS=5 +SMTP_MAX_MESSAGES=Infinity SMTP_USERNAME= SMTP_PASSWORD= SMTP_SECURE="false" # true for 465, false for other ports SMTP_DKIM_DOMAINNAME= SMTP_DKIM_KEYSELECTOR= SMTP_DKIM_PRIVATKEY= +# E-Mail settings for our 'docker compose up mailserver' +# SMTP_HOST=localhost +# SMTP_PORT=1025 +# SMTP_IGNORE_TLS=true +# SMTP_USERNAME= +# SMTP_PASSWORD= +# SMTP_MAX_CONNECTIONS=1 +# SMTP_MAX_MESSAGES= 10 JWT_SECRET="b/&&7b78BF&fv/Vd" JWT_EXPIRES="2y" @@ -30,10 +40,12 @@ COMMIT= PUBLIC_REGISTRATION=false INVITE_REGISTRATION=true -AWS_ACCESS_KEY_ID= -AWS_SECRET_ACCESS_KEY= -AWS_ENDPOINT= -AWS_REGION= -AWS_BUCKET= +AWS_ACCESS_KEY_ID=minio +AWS_SECRET_ACCESS_KEY=12341234 +AWS_ENDPOINT=http://localhost:9000 +AWS_REGION=local +AWS_BUCKET=ocelot +S3_PUBLIC_GATEWAY=http://localhost:8000 CATEGORIES_ACTIVE=false +MAX_PINNED_POSTS=1 diff --git a/backend/.env.test_e2e b/backend/.env.test_e2e new file mode 100644 index 000000000..833f45b8b --- /dev/null +++ b/backend/.env.test_e2e @@ -0,0 +1,43 @@ +DEBUG=true + +NEO4J_URI=bolt://localhost:7687 +NEO4J_USERNAME=neo4j +NEO4J_PASSWORD=letmein +GRAPHQL_URI=http://localhost:4000 +CLIENT_URI=http://localhost:3000 + +# E-Mail default settings +SUPPORT_EMAIL="devops@ocelot.social" +EMAIL_DEFAULT_SENDER="devops@ocelot.social" +SMTP_HOST=mailserver +SMTP_PORT=1025 +SMTP_IGNORE_TLS=true +SMTP_MAX_CONNECTIONS=5 +SMTP_MAX_MESSAGES=Infinity +SMTP_USERNAME= +SMTP_PASSWORD= +SMTP_SECURE="false" # true for 465, false for other ports +SMTP_DKIM_DOMAINNAME= +SMTP_DKIM_KEYSELECTOR= +SMTP_DKIM_PRIVATKEY= + +JWT_SECRET="b/&&7b78BF&fv/Vd" +JWT_EXPIRES="2y" +MAPBOX_TOKEN="pk.eyJ1IjoiYnVzZmFrdG9yIiwiYSI6ImNraDNiM3JxcDBhaWQydG1uczhpZWtpOW4ifQ.7TNRTO-o9aK1Y6MyW_Nd4g" + +PRIVATE_KEY_PASSPHRASE="a7dsf78sadg87ad87sfagsadg78" + +SENTRY_DSN_BACKEND= +COMMIT= +PUBLIC_REGISTRATION=false +INVITE_REGISTRATION=true + +AWS_ACCESS_KEY_ID=minio +AWS_SECRET_ACCESS_KEY=12341234 +AWS_ENDPOINT=http://localhost:9000 +AWS_REGION=local +AWS_BUCKET=ocelot +S3_PUBLIC_GATEWAY=http://localhost:8000 + +CATEGORIES_ACTIVE=false +MAX_PINNED_POSTS=1 diff --git a/backend/.eslintignore b/backend/.eslintignore new file mode 100644 index 000000000..e19e2338d --- /dev/null +++ b/backend/.eslintignore @@ -0,0 +1,3 @@ +node_modules/ +build/ +coverage/ \ No newline at end of file diff --git a/backend/.eslintrc.cjs b/backend/.eslintrc.cjs new file mode 100644 index 000000000..9883fae83 --- /dev/null +++ b/backend/.eslintrc.cjs @@ -0,0 +1,232 @@ +// eslint-disable-next-line import/no-commonjs +module.exports = { + root: true, + env: { + node: true, + }, + parser: '@typescript-eslint/parser', + plugins: ['prettier', '@typescript-eslint', 'import', 'n', 'promise', 'security', 'no-catch-all'], + extends: [ + 'standard', + 'eslint:recommended', + 'plugin:n/recommended', + 'plugin:prettier/recommended', + 'plugin:import/recommended', + 'plugin:import/typescript', + 'plugin:promise/recommended', + 'plugin:security/recommended-legacy', + 'plugin:@eslint-community/eslint-comments/recommended', + 'prettier', + ], + settings: { + 'import/parsers': { + '@typescript-eslint/parser': ['.ts', '.tsx'], + }, + 'import/resolver': { + typescript: { + project: ['./tsconfig.json', './backend/tsconfig.json'], + }, + node: true, + }, + }, + rules: { + 'no-catch-all/no-catch-all': 'error', + 'no-console': 'error', + camelcase: 'error', + 'no-debugger': 'error', + 'prettier/prettier': [ + 'error', + { + htmlWhitespaceSensitivity: 'ignore', + }, + ], + // import + 'import/export': 'error', + // 'import/no-deprecated': 'error', + 'import/no-empty-named-blocks': 'error', + 'import/no-extraneous-dependencies': 'error', + 'import/no-mutable-exports': 'error', + 'import/no-unused-modules': 'error', + 'import/no-named-as-default': 'error', + 'import/no-named-as-default-member': 'error', + 'import/no-amd': 'error', + 'import/no-commonjs': 'error', + 'import/no-import-module-exports': 'error', + 'import/no-nodejs-modules': 'off', + 'import/unambiguous': 'off', // not compatible with .eslintrc.cjs + 'import/default': 'error', + 'import/named': 'off', // has false positives + 'import/namespace': 'error', + 'import/no-absolute-path': 'error', + 'import/no-cycle': 'error', + 'import/no-dynamic-require': 'error', + 'import/no-internal-modules': 'off', + 'import/no-relative-packages': 'error', + 'import/no-relative-parent-imports': ['error', { ignore: ['@/*'] }], + 'import/no-self-import': 'error', + 'import/no-unresolved': 'error', + 'import/no-useless-path-segments': 'error', + 'import/no-webpack-loader-syntax': 'error', + 'import/consistent-type-specifier-style': 'error', + 'import/exports-last': 'off', + 'import/extensions': 'error', + 'import/first': 'error', + 'import/group-exports': 'off', + 'import/newline-after-import': 'error', + 'import/no-anonymous-default-export': 'off', // not compatible with neode + 'import/no-default-export': 'off', // not compatible with neode + 'import/no-duplicates': 'error', + 'import/no-named-default': 'error', + 'import/no-namespace': 'error', + 'import/no-unassigned-import': 'error', + 'import/order': [ + 'error', + { + groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'], + 'newlines-between': 'always', + pathGroups: [ + { + pattern: '@?*/**', + group: 'external', + position: 'after', + }, + { + pattern: '@/**', + group: 'external', + position: 'after', + }, + ], + alphabetize: { + order: 'asc' /* sort in ascending order. Options: ['ignore', 'asc', 'desc'] */, + caseInsensitive: true /* ignore case. Options: [true, false] */, + }, + distinctGroup: true, + }, + ], + 'import/prefer-default-export': 'off', + + // n + // 'n/callback-return': 'error', + 'n/exports-style': 'error', + 'n/file-extension-in-import': ['error', 'never'], + 'n/global-require': 'error', + 'n/handle-callback-err': 'error', + // 'n/hashbang': 'error', // part of n/recommended + 'n/no-callback-literal': 'error', + // 'n/no-deprecated-api': 'error', // part of n/recommended + // 'n/no-exports-assign': 'error', // part of n/recommended + 'n/no-extraneous-import': 'off', // duplicate of import/no-extraneous-dependencies // part of n/recommended + // 'n/no-extraneous-require': 'error', // part of n/recommended + 'n/no-hide-core-modules': 'error', + 'n/no-missing-import': 'off', // not compatible with typescript // part of n/recommended + // 'n/no-missing-require': 'error', // part of n/recommended + 'n/no-mixed-requires': 'error', + 'n/no-new-require': 'error', + 'n/no-path-concat': 'error', + 'n/no-process-env': 'error', + // 'n/no-process-exit': 'error', // part of n/recommended + 'n/no-restricted-import': 'error', + 'n/no-restricted-require': 'error', + 'n/no-sync': 'error', + // 'n/no-unpublished-bin': 'error', // part of n/recommended + 'n/no-unpublished-import': [ + 'error', + { allowModules: ['apollo-server-testing', 'rosie', '@faker-js/faker', 'ts-jest'] }, + ], // part of n/recommended + 'n/no-unpublished-require': ['error', { allowModules: ['ts-jest', 'require-json5'] }], // part of n/recommended + // 'n/no-unsupported-features/es-builtins': 'error', // part of n/recommended + // 'n/no-unsupported-features/es-syntax': 'error', // part of n/recommended + // 'n/no-unsupported-features/node-builtins': 'error', // part of n/recommended + 'n/prefer-global/buffer': 'error', + 'n/prefer-global/console': 'error', + 'n/prefer-global/process': 'error', + 'n/prefer-global/text-decoder': 'error', + 'n/prefer-global/text-encoder': 'error', + 'n/prefer-global/url': 'error', + 'n/prefer-global/url-search-params': 'error', + 'n/prefer-node-protocol': 'error', + 'n/prefer-promises/dns': 'error', + 'n/prefer-promises/fs': 'error', + // 'n/process-exit-as-throw': 'error', // part of n/recommended + 'n/shebang': 'error', + + // promise + // 'promise/always-return': 'error', // part of promise/recommended + 'promise/avoid-new': 'error', + // 'promise/catch-or-return': 'error', // part of promise/recommended + // 'promise/no-callback-in-promise': 'warn', // part of promise/recommended + 'promise/no-multiple-resolved': 'error', + 'promise/no-native': 'off', // ES5 only + // 'promise/no-nesting': 'warn', // part of promise/recommended + // 'promise/no-new-statics': 'error', // part of promise/recommended + // 'promise/no-promise-in-callback': 'warn', // part of promise/recommended + // 'promise/no-return-in-finally': 'warn', // part of promise/recommended + // 'promise/no-return-wrap': 'error', // part of promise/recommended + // 'promise/param-names': 'error', // part of promise/recommended + 'promise/prefer-await-to-callbacks': 'error', + 'promise/prefer-catch': 'error', + 'promise/spec-only': 'error', + // 'promise/valid-params': 'error', // part of promise/recommended + + // eslint comments + '@eslint-community/eslint-comments/disable-enable-pair': ['error', { allowWholeFile: true }], + '@eslint-community/eslint-comments/no-restricted-disable': 'error', + '@eslint-community/eslint-comments/no-use': 'off', + '@eslint-community/eslint-comments/require-description': 'off', + }, + overrides: [ + // only for ts files + { + files: ['*.ts', '*.tsx'], + extends: [ + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking', + 'plugin:@typescript-eslint/strict', + 'prettier', + ], + rules: { + // allow explicitly defined dangling promises + // '@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: true }], + 'no-void': ['error', { allowAsStatement: true }], + // ignore prefer-regexp-exec rule to allow string.match(regex) + '@typescript-eslint/prefer-regexp-exec': 'off', + // this should not run on ts files: https://github.com/import-js/eslint-plugin-import/issues/2215#issuecomment-911245486 + 'import/unambiguous': 'off', + // this is not compatible with typeorm, due to joined tables can be null, but are not defined as nullable + '@typescript-eslint/no-unnecessary-condition': 'off', + // respect underscore as acceptable unused variable + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, + ], + }, + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'], + // this is to properly reference the referenced project database without requirement of compiling it + EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true, + }, + }, + { + files: ['*.spec.ts'], + plugins: ['jest'], + env: { + jest: true, + }, + rules: { + 'jest/no-disabled-tests': 'error', + 'jest/no-focused-tests': 'error', + 'jest/no-identical-title': 'error', + 'jest/prefer-to-have-length': 'error', + 'jest/valid-expect': 'error', + '@typescript-eslint/unbound-method': 'off', + 'jest/unbound-method': 'error', + }, + }, + { + extends: ['plugin:jsonc/recommended-with-jsonc'], + files: ['*.json', '*.json5', '*.jsonc'], + parser: 'jsonc-eslint-parser', + }, + ], +} diff --git a/backend/.eslintrc.js b/backend/.eslintrc.js deleted file mode 100644 index cc5440d82..000000000 --- a/backend/.eslintrc.js +++ /dev/null @@ -1,219 +0,0 @@ -module.exports = { - root: true, - env: { - // es6: true, - node: true, - }, - /* parserOptions: { - parser: 'babel-eslint' - },*/ - parser: '@typescript-eslint/parser', - plugins: ['prettier', '@typescript-eslint' /*, 'import', 'n', 'promise'*/], - extends: [ - 'standard', - // 'eslint:recommended', - 'plugin:prettier/recommended', - // 'plugin:import/recommended', - // 'plugin:import/typescript', - // 'plugin:security/recommended', - // 'plugin:@eslint-community/eslint-comments/recommended', - ], - settings: { - 'import/parsers': { - '@typescript-eslint/parser': ['.ts', '.tsx'], - }, - 'import/resolver': { - typescript: { - project: ['./tsconfig.json'], - }, - node: true, - }, - }, - /* rules: { - //'indent': [ 'error', 2 ], - //'quotes': [ "error", "single"], - // 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', - > 'no-console': ['error'], - > 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', - > 'prettier/prettier': ['error'], - }, */ - rules: { - 'no-console': 'error', - camelcase: 'error', - 'no-debugger': 'error', - 'prettier/prettier': [ - 'error', - { - htmlWhitespaceSensitivity: 'ignore', - }, - ], - // import - // 'import/export': 'error', - // 'import/no-deprecated': 'error', - // 'import/no-empty-named-blocks': 'error', - // 'import/no-extraneous-dependencies': 'error', - // 'import/no-mutable-exports': 'error', - // 'import/no-unused-modules': 'error', - // 'import/no-named-as-default': 'error', - // 'import/no-named-as-default-member': 'error', - // 'import/no-amd': 'error', - // 'import/no-commonjs': 'error', - // 'import/no-import-module-exports': 'error', - // 'import/no-nodejs-modules': 'off', - // 'import/unambiguous': 'error', - // 'import/default': 'error', - // 'import/named': 'error', - // 'import/namespace': 'error', - // 'import/no-absolute-path': 'error', - // 'import/no-cycle': 'error', - // 'import/no-dynamic-require': 'error', - // 'import/no-internal-modules': 'off', - // 'import/no-relative-packages': 'error', - // 'import/no-relative-parent-imports': ['error', { ignore: ['@/*'] }], - // 'import/no-self-import': 'error', - // 'import/no-unresolved': 'error', - // 'import/no-useless-path-segments': 'error', - // 'import/no-webpack-loader-syntax': 'error', - // 'import/consistent-type-specifier-style': 'error', - // 'import/exports-last': 'off', - // 'import/extensions': 'error', - // 'import/first': 'error', - // 'import/group-exports': 'off', - // 'import/newline-after-import': 'error', - // 'import/no-anonymous-default-export': 'error', - // 'import/no-default-export': 'error', - // 'import/no-duplicates': 'error', - // 'import/no-named-default': 'error', - // 'import/no-namespace': 'error', - // 'import/no-unassigned-import': 'error', - // 'import/order': [ - // 'error', - // { - // groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'], - // 'newlines-between': 'always', - // pathGroups: [ - // { - // pattern: '@?*/**', - // group: 'external', - // position: 'after', - // }, - // { - // pattern: '@/**', - // group: 'external', - // position: 'after', - // }, - // ], - // alphabetize: { - // order: 'asc' /* sort in ascending order. Options: ['ignore', 'asc', 'desc'] */, - // caseInsensitive: true /* ignore case. Options: [true, false] */, - // }, - // distinctGroup: true, - // }, - // ], - // 'import/prefer-default-export': 'off', - // n - // 'n/handle-callback-err': 'error', - // 'n/no-callback-literal': 'error', - // 'n/no-exports-assign': 'error', - // 'n/no-extraneous-import': 'error', - // 'n/no-extraneous-require': 'error', - // 'n/no-hide-core-modules': 'error', - // 'n/no-missing-import': 'off', // not compatible with typescript - // 'n/no-missing-require': 'error', - // 'n/no-new-require': 'error', - // 'n/no-path-concat': 'error', - // 'n/no-process-exit': 'error', - // 'n/no-unpublished-bin': 'error', - // 'n/no-unpublished-import': 'off', // TODO need to exclude seeds - // 'n/no-unpublished-require': 'error', - // 'n/no-unsupported-features': ['error', { ignores: ['modules'] }], - // 'n/no-unsupported-features/es-builtins': 'error', - // 'n/no-unsupported-features/es-syntax': 'error', - // 'n/no-unsupported-features/node-builtins': 'error', - // 'n/process-exit-as-throw': 'error', - // 'n/shebang': 'error', - // 'n/callback-return': 'error', - // 'n/exports-style': 'error', - // 'n/file-extension-in-import': 'off', - // 'n/global-require': 'error', - // 'n/no-mixed-requires': 'error', - // 'n/no-process-env': 'error', - // 'n/no-restricted-import': 'error', - // 'n/no-restricted-require': 'error', - // 'n/no-sync': 'error', - // 'n/prefer-global/buffer': 'error', - // 'n/prefer-global/console': 'error', - // 'n/prefer-global/process': 'error', - // 'n/prefer-global/text-decoder': 'error', - // 'n/prefer-global/text-encoder': 'error', - // 'n/prefer-global/url': 'error', - // 'n/prefer-global/url-search-params': 'error', - // 'n/prefer-promises/dns': 'error', - // 'n/prefer-promises/fs': 'error', - // promise - // 'promise/catch-or-return': 'error', - // 'promise/no-return-wrap': 'error', - // 'promise/param-names': 'error', - // 'promise/always-return': 'error', - // 'promise/no-native': 'off', - // 'promise/no-nesting': 'warn', - // 'promise/no-promise-in-callback': 'warn', - // 'promise/no-callback-in-promise': 'warn', - // 'promise/avoid-new': 'warn', - // 'promise/no-new-statics': 'error', - // 'promise/no-return-in-finally': 'warn', - // 'promise/valid-params': 'warn', - // 'promise/prefer-await-to-callbacks': 'error', - // 'promise/no-multiple-resolved': 'error', - // eslint comments - // '@eslint-community/eslint-comments/disable-enable-pair': ['error', { allowWholeFile: true }], - // '@eslint-community/eslint-comments/no-restricted-disable': 'error', - // '@eslint-community/eslint-comments/no-use': 'off', - // '@eslint-community/eslint-comments/require-description': 'off', - }, - overrides: [ - // only for ts files - { - files: ['*.ts', '*.tsx'], - extends: [ - // 'plugin:@typescript-eslint/recommended', - // 'plugin:@typescript-eslint/recommended-requiring-type-checking', - // 'plugin:@typescript-eslint/strict', - ], - rules: { - // allow explicitly defined dangling promises - // '@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: true }], - 'no-void': ['error', { allowAsStatement: true }], - // ignore prefer-regexp-exec rule to allow string.match(regex) - '@typescript-eslint/prefer-regexp-exec': 'off', - // this should not run on ts files: https://github.com/import-js/eslint-plugin-import/issues/2215#issuecomment-911245486 - 'import/unambiguous': 'off', - // this is not compatible with typeorm, due to joined tables can be null, but are not defined as nullable - '@typescript-eslint/no-unnecessary-condition': 'off', - }, - parserOptions: { - tsconfigRootDir: __dirname, - project: ['./tsconfig.json'], - // this is to properly reference the referenced project database without requirement of compiling it - // eslint-disable-next-line camelcase - EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true, - }, - }, - { - files: ['*.spec.ts'], - plugins: ['jest'], - env: { - jest: true, - }, - rules: { - 'jest/no-disabled-tests': 'error', - 'jest/no-focused-tests': 'error', - 'jest/no-identical-title': 'error', - 'jest/prefer-to-have-length': 'error', - 'jest/valid-expect': 'error', - '@typescript-eslint/unbound-method': 'off', - // 'jest/unbound-method': 'error', - }, - }, - ], -}; diff --git a/backend/.gitignore b/backend/.gitignore index 833f7e34e..a5d3db1fa 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -6,8 +6,7 @@ yarn-error.log build/* coverage.lcov .nyc_output/ -public/uploads/* !.gitkeep # Apple macOS folder attribute file -.DS_Store \ No newline at end of file +.DS_Store diff --git a/backend/.graphqlconfig b/backend/.graphqlconfig deleted file mode 100644 index ca328bc83..000000000 --- a/backend/.graphqlconfig +++ /dev/null @@ -1,3 +0,0 @@ -{ - "schemaPath": "./src/schema.graphql" -} diff --git a/backend/.prettierrc.js b/backend/.prettierrc.cjs similarity index 100% rename from backend/.prettierrc.js rename to backend/.prettierrc.cjs diff --git a/backend/Dockerfile b/backend/Dockerfile index 40b78225a..24d8d5224 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20.12.1-alpine3.19 AS base +FROM node:24.1.0-alpine AS base LABEL org.label-schema.name="ocelot.social:backend" LABEL org.label-schema.description="Backend of the Social Network Software ocelot.social" LABEL org.label-schema.usage="https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/master/README.md" @@ -10,7 +10,7 @@ LABEL maintainer="devops@ocelot.social" ENV NODE_ENV="production" ENV PORT="4000" EXPOSE ${PORT} -RUN apk --no-cache add git python3 make g++ bash +RUN apk --no-cache add git python3 make g++ bash linux-headers RUN mkdir -p /app WORKDIR /app CMD ["/bin/bash", "-c", "yarn run start"] @@ -21,13 +21,18 @@ CMD ["/bin/sh", "-c", "yarn install && yarn run dev"] FROM base AS build COPY . . ONBUILD COPY ./branding/constants/ src/config/tmp +# copy categories to brand them (use yarn prod:db:data:categories) +ONBUILD COPY ./branding/constants/ src/constants/ ONBUILD RUN tools/replace-constants.sh ONBUILD COPY ./branding/email/ src/middleware/helpers/email/ +ONBUILD COPY ./branding/middlewares/ src/middleware/branding/ +ONBUILD COPY ./branding/data/ src/db/data +ONBUILD COPY ./branding/public/ public/ ONBUILD RUN yarn install --production=false --frozen-lockfile --non-interactive ONBUILD RUN yarn run build ONBUILD RUN mkdir /build ONBUILD RUN cp -r ./build /build -ONBUILD RUN cp -r ./public /build/build +ONBUILD RUN cp -r ./public /build ONBUILD RUN cp -r ./package.json yarn.lock /build ONBUILD RUN cd /build && yarn install --production=true --frozen-lockfile --non-interactive diff --git a/backend/README.md b/backend/README.md index 8fc05779e..e6a828848 100644 --- a/backend/README.md +++ b/backend/README.md @@ -6,12 +6,12 @@ Run the following command to install everything through docker. The installation takes a bit longer on the first pass or on rebuild ... -```bash +```sh # in main folder -$ docker-compose up +$ docker compose up # or # rebuild the containers for a cleanup -$ docker-compose up --build +$ docker compose up --build ``` Wait a little until your backend is up and running at [http://localhost:4000/](http://localhost:4000/). @@ -26,7 +26,7 @@ some known problems with more recent node versions). You can use the [node version manager](https://github.com/nvm-sh/nvm) `nvm` to switch between different local Node versions: -```bash +```sh # install Node $ cd backend $ nvm install v20.12.1 @@ -35,7 +35,7 @@ $ nvm use v20.12.1 Install node dependencies with [yarn](https://yarnpkg.com/en/): -```bash +```sh # in main folder $ cd backend $ yarn install @@ -47,7 +47,7 @@ $ nvm use && yarn Copy Environment Variables: -```bash +```sh # in backend/ $ cp .env.template .env ``` @@ -57,14 +57,14 @@ a [local Neo4J](http://localhost:7474) instance is up and running. Start the backend for development with: -```bash +```sh # in backend/ $ yarn run dev ``` or start the backend in production environment with: -```bash +```sh # in backend/ $ yarn run start ``` @@ -79,154 +79,141 @@ More details about our GraphQL playground and how to use it with ocelot.social c ![GraphQL Playground](../.gitbook/assets/graphql-playground.png) -### Database Indexes and Constraints +## Database -Database indexes and constraints need to be created and upgraded when the database and the backend are running: +A fresh database needs to be initialized and migrated. -::: tabs -@tab:active Docker - -```bash -# in main folder while docker-compose is running -$ docker exec backend yarn run db:migrate init - -# only once: init admin user and create indexes and constraints in Neo4j database -# for development -$ docker compose exec backend yarn prod:migrate init -# in production mode use command -$ docker compose exec backend /bin/sh -c "yarn prod:migrate init" +```sh +# in folder backend while database is running +yarn db:migrate init +# for docker environments: +docker exec ocelot-social-backend-1 yarn db:migrate init +# for docker production: +docker exec ocelot-social-backend-1 yarn prod:migrate init ``` -```bash -# in main folder with docker compose running -$ docker exec backend yarn run db:migrate up +```sh +# in backend with database running (In docker or local) +yarn db:migrate up + +# for docker development: +docker exec ocelot-social-backend-1 yarn db:migrate up +# for docker production +docker exec ocelot-social-backend-1 yarn prod:migrate up ``` -@tab Without Docker +### Optional Data -```bash -# in folder backend/ while database is running -# make sure your database is running on http://localhost:7474/browser/ -yarn run db:migrate init +You can seed some optional data into the database. + +To create the default admin with password `1234` use: + +```sh +# in backend with database running (In docker or local) +yarn db:data:admin ``` -```bash -# in backend/ with database running (In docker or local) -yarn run db:migrate up +When using `CATEGORIES_ACTIVE=true` you also want to seed the categories with: + +```sh +# in backend with database running (In docker or local) +yarn db:data:categories ``` -::: +### Branding Data -#### Seed Database +You might need to seed some branding specific data into the database. -If you want your backend to return anything else than an empty response, you -need to seed your database: +To do so, run: -::: tabs -@tab:active Docker +```sh +# in backend with database running (In docker or local) +yarn db:data:branding -In another terminal run: - -```bash -# in main folder while docker-compose is running -$ docker exec backend yarn run db:seed +# for docker +docker exec ocelot-social-backend-1 yarn db:data:branding ``` -To reset the database run: +### Seed Data -```bash -# in main folder while docker-compose is running -$ docker exec backend yarn run db:reset +For a predefined set of test data you can seed the database with: + +```sh +# in backend with database running (In docker or local) +yarn db:seed + +# for docker +docker exec ocelot-social-backend-1 yarn db:seed +``` + +### Reset Data + +In order to reset the database you can run: + +```sh +# in backend with database running (In docker or local) +yarn db:reset +# or deleting the migrations as well +yarn db:reset:withmigrations + +# for docker +docker exec ocelot-social-backend-1 yarn db:reset +# or deleting the migrations as well +docker exec ocelot-social-backend-1 yarn db:reset:withmigrations # you could also wipe out your neo4j database and delete all volumes with: -$ docker-compose down -v -# if container is not running, run this command to set up your database indexes and constraints -$ docker exec backend yarn run db:migrate init -# And then upgrade the indexes and const -$ docker exec backend yarn run db:migrate up +docker compose down -v ``` -@tab Without Docker - -Run: - -```bash -# in backend/ while database is running -$ yarn run db:seed -``` - -To reset the database run: - -```bash -# in backend/ while database is running -$ yarn run db:reset -``` - -::: +> Note: This just deletes the data and not the constraints, hence you do not need to rerun `yarn db:migrate init` or `yarn db:migrate up`. ### Data migrations Although Neo4J is schema-less,you might find yourself in a situation in which you have to migrate your data e.g. because your data modeling has changed. -::: tabs -@tab:active Docker - Generate a data migration file: -```bash -# in main folder while docker-compose is running -$ docker-compose exec backend yarn run db:migrate:create your_data_migration -# Edit the file in ./src/db/migrations/ -``` - -To run the migration: - -```bash -# in main folder while docker-compose is running -$ docker exec backend yarn run db:migrate up -``` - -@tab Without Docker - -Generate a data migration file: - -```bash -# in backend/ +```sh +# in backend $ yarn run db:migrate:create your_data_migration # Edit the file in ./src/db/migrations/ + +# for docker +# in main folder while docker compose is running +$ docker compose exec ocelot-social-backend-1 yarn run db:migrate:create your_data_migration +# Edit the file in ./src/db/migrations/ ``` To run the migration: -```bash +```sh # in backend/ while database is running $ yarn run db:migrate up -``` -::: +# for docker +# in main folder while docker compose is running +$ docker exec backend yarn run db:migrate up +``` ## Testing **Beware**: We have no multiple database setup at the moment. We clean the database after each test, running the tests will wipe out all your data! -::: tabs -@tab:active Docker - Run the unit tests: -```bash -# in main folder while docker-compose is running -$ docker exec backend yarn run test -``` - -@tab Without Docker - -Run the unit tests: - -```bash +```sh # in backend/ while database is running $ yarn run test + +# for docker +# in main folder while docker compose is running +$ docker exec ocelot-social-backend-1 yarn run test ``` -::: +If the snapshots of the emails must be updated, you have to run the tests in docker! Otherwise the CI will fail. + +```sh +# in main folder while docker compose is running +$ docker exec ocelot-social-backend-1 yarn run test -u src/emails/ +``` diff --git a/backend/babel.config.json b/backend/babel.config.json deleted file mode 100644 index f36dbeadb..000000000 --- a/backend/babel.config.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "presets": [ - [ - "@babel/preset-env", - { - "targets": { - "node": "10" - } - } - ] - ], - "plugins": [ - "@babel/plugin-proposal-throw-expressions" - ] -} diff --git a/backend/branding/data/.gitkeep b/backend/branding/data/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/backend/branding/middlewares/.gitkeep b/backend/branding/middlewares/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/backend/branding/public/.gitkeep b/backend/branding/public/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/backend/jest.config.cjs b/backend/jest.config.cjs new file mode 100644 index 000000000..3441db428 --- /dev/null +++ b/backend/jest.config.cjs @@ -0,0 +1,27 @@ +/* eslint-disable import/no-commonjs */ +const requireJSON5 = require('require-json5') +const { pathsToModuleNameMapper } = require('ts-jest') + +const { compilerOptions } = requireJSON5('./tsconfig.json') + +module.exports = { + verbose: true, + preset: 'ts-jest', + collectCoverage: true, + collectCoverageFrom: [ + '**/*.ts', + '!**/node_modules/**', + '!**/test/**', + '!**/build/**', + '!**/src/**/?(*.)+(spec|test).ts?(x)', + '!**/src/db/**', + ], + coverageThreshold: { + global: { + lines: 90, + }, + }, + testMatch: ['**/src/**/?(*.)+(spec|test).ts?(x)'], + setupFilesAfterEnv: ['/test/setup.ts'], + moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '/' }), +} diff --git a/backend/jest.config.js b/backend/jest.config.js deleted file mode 100644 index 15eb22477..000000000 --- a/backend/jest.config.js +++ /dev/null @@ -1,20 +0,0 @@ -module.exports = { - verbose: true, - preset: 'ts-jest', - collectCoverage: true, - collectCoverageFrom: [ - '**/*.ts', - '!**/node_modules/**', - '!**/test/**', - '!**/build/**', - '!**/src/**/?(*.)+(spec|test).ts?(x)', - '!**/src/db/**' - ], - coverageThreshold: { - global: { - lines: 90, - }, - }, - testMatch: ['**/src/**/?(*.)+(spec|test).ts?(x)'], - setupFilesAfterEnv: ['/test/setup.ts'] -} diff --git a/backend/package.json b/backend/package.json index 9c52815a1..34bd99ebf 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "ocelot-social-backend", - "version": "3.2.1", + "version": "3.8.2", "description": "GraphQL Backend for ocelot.social", "repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social", "author": "ocelot.social Community", @@ -8,56 +8,52 @@ "private": false, "main": "src/index.ts", "scripts": { - "__migrate": "migrate --compiler 'ts:./src/db/compiler.ts' --migrations-dir ./src/db/migrations", - "prod:migrate": "migrate --migrations-dir ./build/src/db/migrations --store ./build/src/db/migrate/store.js", "start": "node build/src/", - "build": "tsc && ./scripts/build.copy.files.sh", - "dev": "nodemon --exec ts-node src/ -e js,ts,gql", - "dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/ -e js,ts,gql", - "lint": "eslint --max-warnings=0 --ext .js,.ts ./src", + "build": "tsc && tsc-alias && ./scripts/build.copy.files.sh", + "dev": "nodemon --exec ts-node --require tsconfig-paths/register src/index.ts -e js,ts,gql", + "dev:debug": "nodemon --exec node --inspect=0.0.0.0:9229 build/src/index.js -e js,ts,gql", + "lint": "eslint --max-warnings=0 --report-unused-disable-directives --ext .js,.ts,.cjs,.json,.json5,.jsonc .", "test": "cross-env NODE_ENV=test NODE_OPTIONS=--max-old-space-size=8192 jest --runInBand --coverage --forceExit --detectOpenHandles", - "db:clean": "ts-node src/db/clean.ts", - "db:reset": "yarn run db:clean", - "db:seed": "ts-node src/db/seed.ts", - "db:migrate": "yarn run __migrate --store ./src/db/migrate/store.ts", - "db:migrate:create": "yarn run __migrate --template-file ./src/db/migrate/template.ts --date-format 'yyyymmddHHmmss' create" + "db:reset": "ts-node --require tsconfig-paths/register src/db/reset.ts", + "db:reset:withmigrations": "ts-node --require tsconfig-paths/register src/db/reset-with-migrations.ts", + "db:seed": "ts-node --require tsconfig-paths/register src/db/seed.ts", + "db:data:admin": "ts-node --require tsconfig-paths/register src/db/admin.ts", + "db:data:badges": "ts-node --require tsconfig-paths/register src/db/badges.ts", + "db:data:branding": "ts-node --require tsconfig-paths/register src/db/data-branding.ts", + "db:data:categories": "ts-node --require tsconfig-paths/register src/db/categories.ts", + "db:migrate": "migrate --compiler 'ts:./src/db/compiler.ts' --migrations-dir ./src/db/migrations --store ./src/db/migrate/store.ts", + "db:migrate:create": "migrate --compiler 'ts:./src/db/compiler.ts' --migrations-dir ./src/db/migrations --template-file ./src/db/migrate/template.ts --date-format 'yyyymmddHHmmss' create", + "prod:migrate": "migrate --migrations-dir ./build/src/db/migrations --store ./build/src/db/migrate/store.js", + "prod:db:data:branding": "node build/src/db/data-branding.js", + "prod:db:data:categories": "node build/src/db/categories.js" }, "dependencies": { - "@babel/cli": "~7.27.0", - "@babel/core": "^7.26.10", - "@babel/node": "~7.26.0", - "@babel/plugin-proposal-throw-expressions": "^7.25.9", - "@babel/preset-env": "~7.26.9", - "@babel/register": "^7.23.7", + "@aws-sdk/client-s3": "^3.826.0", + "@aws-sdk/lib-storage": "^3.817.0", "@sentry/node": "^5.15.4", - "apollo-cache-inmemory": "~1.6.6", - "apollo-client": "~2.6.10", - "apollo-link-context": "~1.0.20", - "apollo-link-http": "~1.5.17", + "@types/mime-types": "^2.1.4", "apollo-server": "~2.14.2", "apollo-server-express": "^2.14.2", - "aws-sdk": "^2.1692.0", - "babel-core": "~7.0.0-0", - "babel-eslint": "~10.1.0", - "babel-jest": "~29.7.0", - "babel-plugin-transform-runtime": "^6.23.0", - "bcryptjs": "~2.4.3", + "bcryptjs": "~3.0.2", + "body-parser": "^1.20.3", "cheerio": "~1.0.0", - "cors": "~2.8.5", "cross-env": "~7.0.3", - "dotenv": "~16.4.7", - "express": "^4.21.2", + "dotenv": "~16.5.0", + "email-templates": "^12.0.3", + "express": "^5.1.0", "graphql": "^14.6.0", "graphql-middleware": "~4.0.2", "graphql-middleware-sentry": "^3.2.1", "graphql-redis-subscriptions": "^2.7.0", "graphql-shield": "~7.2.2", + "graphql-subscriptions": "^1.1.0", "graphql-tag": "~2.10.3", + "graphql-upload": "^13.0.0", "helmet": "~8.1.0", - "ioredis": "^4.16.1", + "ioredis": "^5.6.1", "jsonwebtoken": "~8.5.1", "languagedetect": "^2.0.0", - "linkify-html": "^4.2.0", + "linkify-html": "^4.3.1", "linkifyjs": "^4.2.0", "lodash": "~4.17.21", "merge-graphql-schemas": "^1.7.8", @@ -76,52 +72,68 @@ "metascraper-video": "^5.46.11", "metascraper-youtube": "^5.46.11", "migrate": "^2.1.0", - "mime-types": "^2.1.35", + "mime-types": "^3.0.1", "minimatch": "^9.0.4", "mustache": "^4.2.0", "neo4j-driver": "^4.4.11", "neo4j-graphql-js": "^2.11.5", "neode": "^0.4.9", "node-fetch": "^2.7.0", - "nodemailer": "^6.10.0", + "nodemailer": "^6.10.1", "nodemailer-html-to-text": "^3.2.0", + "preview-email": "^3.1.0", + "pug": "^3.0.3", "request": "~2.88.2", - "sanitize-html": "~2.15.0", + "sanitize-html": "~2.17.0", "slug": "~9.1.0", - "subscriptions-transport-ws": "^0.9.19", "trunc-html": "~1.1.2", "uuid": "~9.0.1", - "validator": "^13.15.0", + "validator": "^13.15.15", "xregexp": "^5.1.2" }, "devDependencies": { - "@faker-js/faker": "9.6.0", + "@eslint-community/eslint-plugin-eslint-comments": "^4.5.0", + "@faker-js/faker": "9.8.0", + "@types/email-templates": "^10.0.4", "@types/jest": "^29.5.14", - "@types/node": "^22.14.0", + "@types/lodash": "^4.17.17", + "@types/node": "^22.15.30", + "@types/slug": "^5.0.9", + "@types/uuid": "~9.0.1", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "apollo-server-testing": "~2.11.0", "eslint": "^8.57.1", - "eslint-config-prettier": "^10.1.1", + "eslint-config-prettier": "^10.1.5", "eslint-config-standard": "^17.1.0", - "eslint-import-resolver-typescript": "^4.3.1", + "eslint-import-resolver-typescript": "^4.4.3", "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jest": "^28.11.0", - "eslint-plugin-n": "^16.6.2", - "eslint-plugin-prettier": "^5.2.6", - "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-jest": "^28.13.0", + "eslint-plugin-jsonc": "^2.20.1", + "eslint-plugin-n": "^17.19.0", + "eslint-plugin-no-catch-all": "^1.1.0", + "eslint-plugin-prettier": "^5.4.1", + "eslint-plugin-promise": "^7.2.1", "eslint-plugin-security": "^3.0.1", "jest": "^29.7.0", - "nodemon": "~3.1.9", + "nodemon": "~3.1.10", "prettier": "^3.5.3", + "require-json5": "^1.3.0", "rosie": "^2.1.1", - "ts-jest": "^29.3.1", + "ts-jest": "^29.3.4", "ts-node": "^10.9.2", + "tsc-alias": "^1.8.16", + "tsconfig-paths": "^4.2.0", "typescript": "^5.8.3" }, "resolutions": { "**/**/fs-capacitor": "^6.2.0", "**/graphql-upload": "^11.0.0", - "nan": "2.17.0" + "**/strip-ansi": "6.0.1", + "**/string-width": "4.2.0", + "**/wrap-ansi": "7.0.0" + }, + "engines": { + "node": ">=20.12.1" } } diff --git a/backend/public/img/badges/default_trophy.svg b/backend/public/img/badges/default_trophy.svg new file mode 100644 index 000000000..b203cdfc6 --- /dev/null +++ b/backend/public/img/badges/default_trophy.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/backend/public/img/badges/default_verification.svg b/backend/public/img/badges/default_verification.svg new file mode 100644 index 000000000..c138d734c --- /dev/null +++ b/backend/public/img/badges/default_verification.svg @@ -0,0 +1,29 @@ + + + + + + + + + + diff --git a/backend/public/img/badges/indiegogo_en_bear.svg b/backend/public/img/badges/trophy_blue_bear.svg similarity index 100% rename from backend/public/img/badges/indiegogo_en_bear.svg rename to backend/public/img/badges/trophy_blue_bear.svg diff --git a/backend/public/img/badges/indiegogo_en_panda.svg b/backend/public/img/badges/trophy_blue_panda.svg similarity index 100% rename from backend/public/img/badges/indiegogo_en_panda.svg rename to backend/public/img/badges/trophy_blue_panda.svg diff --git a/backend/public/img/badges/indiegogo_en_rabbit.svg b/backend/public/img/badges/trophy_blue_rabbit.svg similarity index 100% rename from backend/public/img/badges/indiegogo_en_rabbit.svg rename to backend/public/img/badges/trophy_blue_rabbit.svg diff --git a/backend/public/img/badges/indiegogo_en_racoon.svg b/backend/public/img/badges/trophy_blue_racoon.svg similarity index 100% rename from backend/public/img/badges/indiegogo_en_racoon.svg rename to backend/public/img/badges/trophy_blue_racoon.svg diff --git a/backend/public/img/badges/indiegogo_en_rhino.svg b/backend/public/img/badges/trophy_blue_rhino.svg similarity index 100% rename from backend/public/img/badges/indiegogo_en_rhino.svg rename to backend/public/img/badges/trophy_blue_rhino.svg diff --git a/backend/public/img/badges/indiegogo_en_tiger.svg b/backend/public/img/badges/trophy_blue_tiger.svg similarity index 100% rename from backend/public/img/badges/indiegogo_en_tiger.svg rename to backend/public/img/badges/trophy_blue_tiger.svg diff --git a/backend/public/img/badges/indiegogo_en_turtle.svg b/backend/public/img/badges/trophy_blue_turtle.svg similarity index 100% rename from backend/public/img/badges/indiegogo_en_turtle.svg rename to backend/public/img/badges/trophy_blue_turtle.svg diff --git a/backend/public/img/badges/indiegogo_en_whale.svg b/backend/public/img/badges/trophy_blue_whale.svg similarity index 100% rename from backend/public/img/badges/indiegogo_en_whale.svg rename to backend/public/img/badges/trophy_blue_whale.svg diff --git a/backend/public/img/badges/indiegogo_en_wolf.svg b/backend/public/img/badges/trophy_blue_wolf.svg similarity index 100% rename from backend/public/img/badges/indiegogo_en_wolf.svg rename to backend/public/img/badges/trophy_blue_wolf.svg diff --git a/backend/public/img/badges/fundraisingbox_de_airship.svg b/backend/public/img/badges/trophy_green_airship.svg similarity index 100% rename from backend/public/img/badges/fundraisingbox_de_airship.svg rename to backend/public/img/badges/trophy_green_airship.svg diff --git a/backend/public/img/badges/fundraisingbox_de_alienship.svg b/backend/public/img/badges/trophy_green_alienship.svg similarity index 100% rename from backend/public/img/badges/fundraisingbox_de_alienship.svg rename to backend/public/img/badges/trophy_green_alienship.svg diff --git a/backend/public/img/badges/fundraisingbox_de_balloon.svg b/backend/public/img/badges/trophy_green_balloon.svg similarity index 100% rename from backend/public/img/badges/fundraisingbox_de_balloon.svg rename to backend/public/img/badges/trophy_green_balloon.svg diff --git a/backend/public/img/badges/wooold_de_bee.svg b/backend/public/img/badges/trophy_green_bee.svg similarity index 100% rename from backend/public/img/badges/wooold_de_bee.svg rename to backend/public/img/badges/trophy_green_bee.svg diff --git a/backend/public/img/badges/fundraisingbox_de_bigballoon.svg b/backend/public/img/badges/trophy_green_bigballoon.svg similarity index 100% rename from backend/public/img/badges/fundraisingbox_de_bigballoon.svg rename to backend/public/img/badges/trophy_green_bigballoon.svg diff --git a/backend/public/img/badges/wooold_de_butterfly.svg b/backend/public/img/badges/trophy_green_butterfly.svg similarity index 100% rename from backend/public/img/badges/wooold_de_butterfly.svg rename to backend/public/img/badges/trophy_green_butterfly.svg diff --git a/backend/public/img/badges/fundraisingbox_de_crane.svg b/backend/public/img/badges/trophy_green_crane.svg similarity index 100% rename from backend/public/img/badges/fundraisingbox_de_crane.svg rename to backend/public/img/badges/trophy_green_crane.svg diff --git a/backend/public/img/badges/wooold_de_double_rainbow.svg b/backend/public/img/badges/trophy_green_doublerainbow.svg similarity index 100% rename from backend/public/img/badges/wooold_de_double_rainbow.svg rename to backend/public/img/badges/trophy_green_doublerainbow.svg diff --git a/backend/public/img/badges/wooold_de_end_of_rainbow.svg b/backend/public/img/badges/trophy_green_endrainbow.svg similarity index 100% rename from backend/public/img/badges/wooold_de_end_of_rainbow.svg rename to backend/public/img/badges/trophy_green_endrainbow.svg diff --git a/backend/public/img/badges/wooold_de_flower.svg b/backend/public/img/badges/trophy_green_flower.svg similarity index 100% rename from backend/public/img/badges/wooold_de_flower.svg rename to backend/public/img/badges/trophy_green_flower.svg diff --git a/backend/public/img/badges/fundraisingbox_de_glider.svg b/backend/public/img/badges/trophy_green_glider.svg similarity index 100% rename from backend/public/img/badges/fundraisingbox_de_glider.svg rename to backend/public/img/badges/trophy_green_glider.svg diff --git a/backend/public/img/badges/fundraisingbox_de_helicopter.svg b/backend/public/img/badges/trophy_green_helicopter.svg similarity index 100% rename from backend/public/img/badges/fundraisingbox_de_helicopter.svg rename to backend/public/img/badges/trophy_green_helicopter.svg diff --git a/backend/public/img/badges/wooold_de_lifetree.svg b/backend/public/img/badges/trophy_green_lifetree.svg similarity index 100% rename from backend/public/img/badges/wooold_de_lifetree.svg rename to backend/public/img/badges/trophy_green_lifetree.svg diff --git a/backend/public/img/badges/wooold_de_magic_rainbow.svg b/backend/public/img/badges/trophy_green_magicrainbow.svg similarity index 100% rename from backend/public/img/badges/wooold_de_magic_rainbow.svg rename to backend/public/img/badges/trophy_green_magicrainbow.svg diff --git a/backend/public/img/badges/fundraisingbox_de_starter.svg b/backend/public/img/badges/trophy_green_starter.svg similarity index 100% rename from backend/public/img/badges/fundraisingbox_de_starter.svg rename to backend/public/img/badges/trophy_green_starter.svg diff --git a/backend/public/img/badges/wooold_de_super_founder.svg b/backend/public/img/badges/trophy_green_superfounder.svg similarity index 100% rename from backend/public/img/badges/wooold_de_super_founder.svg rename to backend/public/img/badges/trophy_green_superfounder.svg diff --git a/backend/public/img/badges/user_role_admin.svg b/backend/public/img/badges/user_role_admin.svg deleted file mode 100644 index 101e7458d..000000000 --- a/backend/public/img/badges/user_role_admin.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/backend/public/img/badges/user_role_developer.svg b/backend/public/img/badges/user_role_developer.svg deleted file mode 100644 index 55d363c9a..000000000 --- a/backend/public/img/badges/user_role_developer.svg +++ /dev/null @@ -1 +0,0 @@ -</> \ No newline at end of file diff --git a/backend/public/img/badges/user_role_moderator.svg b/backend/public/img/badges/user_role_moderator.svg deleted file mode 100644 index bb2e5fde6..000000000 --- a/backend/public/img/badges/user_role_moderator.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/backend/public/img/badges/verification_red_admin.svg b/backend/public/img/badges/verification_red_admin.svg new file mode 100644 index 000000000..c33cfb72b --- /dev/null +++ b/backend/public/img/badges/verification_red_admin.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + diff --git a/backend/public/img/badges/verification_red_developer.svg b/backend/public/img/badges/verification_red_developer.svg new file mode 100644 index 000000000..4cdb47793 --- /dev/null +++ b/backend/public/img/badges/verification_red_developer.svg @@ -0,0 +1,38 @@ + + + + + + + </> + + diff --git a/backend/public/img/badges/verification_red_moderator.svg b/backend/public/img/badges/verification_red_moderator.svg new file mode 100644 index 000000000..ee1b87605 --- /dev/null +++ b/backend/public/img/badges/verification_red_moderator.svg @@ -0,0 +1,29 @@ + + + + + + + + + diff --git a/backend/public/providers.json b/backend/public/providers.json index ef9f04bff..28b10de5b 100644 --- a/backend/public/providers.json +++ b/backend/public/providers.json @@ -1,257 +1,234 @@ [ - { - "provider_name": "Codepen", - "provider_url": "https:\/\/codepen.io", - "endpoints": [ - { - "schemes": [ - "http:\/\/codepen.io\/*", - "https:\/\/codepen.io\/*" - ], - "url": "http:\/\/codepen.io\/api\/oembed" - } - ] - }, - { - "provider_name": "DTube", - "provider_url": "https:\/\/d.tube\/", - "endpoints": [ - { - "schemes": [ - "https:\/\/d.tube\/v\/*" - ], - "url": "https:\/\/api.d.tube\/oembed", - "discovery": true - } - ] - }, - { - "provider_name": "Facebook (Post)", - "provider_url": "https:\/\/www.facebook.com\/", - "endpoints": [ - { - "schemes": [ - "https:\/\/www.facebook.com\/*\/posts\/*", - "https:\/\/www.facebook.com\/photos\/*", - "https:\/\/www.facebook.com\/*\/photos\/*", - "https:\/\/www.facebook.com\/photo.php*", - "https:\/\/www.facebook.com\/photo.php", - "https:\/\/www.facebook.com\/*\/activity\/*", - "https:\/\/www.facebook.com\/permalink.php", - "https:\/\/www.facebook.com\/media\/set?set=*", - "https:\/\/www.facebook.com\/questions\/*", - "https:\/\/www.facebook.com\/notes\/*\/*\/*" - ], - "url": "https:\/\/www.facebook.com\/plugins\/post\/oembed.json", - "discovery": true - } - ] - }, - { - "provider_name": "Facebook (Video)", - "provider_url": "https:\/\/www.facebook.com\/", - "endpoints": [ - { - "schemes": [ - "https:\/\/www.facebook.com\/*\/videos\/*", - "https:\/\/www.facebook.com\/video.php" - ], - "url": "https:\/\/www.facebook.com\/plugins\/video\/oembed.json", - "discovery": true - } - ] - }, - { - "provider_name": "Flickr", - "provider_url": "https:\/\/www.flickr.com\/", - "endpoints": [ - { - "schemes": [ - "http:\/\/*.flickr.com\/photos\/*", - "http:\/\/flic.kr\/p\/*", - "https:\/\/*.flickr.com\/photos\/*", - "https:\/\/flic.kr\/p\/*" - ], - "url": "https:\/\/www.flickr.com\/services\/oembed\/", - "discovery": true - } - ] - }, - { - "provider_name": "GIPHY", - "provider_url": "https:\/\/giphy.com", - "endpoints": [ - { - "schemes": [ - "https:\/\/giphy.com\/gifs\/*", - "http:\/\/gph.is\/*", - "https:\/\/media.giphy.com\/media\/*\/giphy.gif" - ], - "url": "https:\/\/giphy.com\/services\/oembed", - "discovery": true - } - ] - }, - { - "provider_name": "Instagram", - "provider_url": "https:\/\/instagram.com", - "endpoints": [ - { - "schemes": [ - "http:\/\/instagram.com\/p\/*", - "http:\/\/instagr.am\/p\/*", - "http:\/\/www.instagram.com\/p\/*", - "http:\/\/www.instagr.am\/p\/*", - "https:\/\/instagram.com\/p\/*", - "https:\/\/instagr.am\/p\/*", - "https:\/\/www.instagram.com\/p\/*", - "https:\/\/www.instagr.am\/p\/*" - ], - "url": "https:\/\/api.instagram.com\/oembed", - "formats": [ - "json" - ] - } - ] - }, - { - "provider_name": "Meetup", - "provider_url": "http:\/\/www.meetup.com", - "endpoints": [ - { - "schemes": [ - "http:\/\/meetup.com\/*", - "https:\/\/www.meetup.com\/*", - "https:\/\/meetup.com\/*", - "http:\/\/meetu.ps\/*" - ], - "url": "https:\/\/api.meetup.com\/oembed", - "formats": [ - "json" - ] - } - ] - }, - { - "provider_name": "MixCloud", - "provider_url": "https:\/\/mixcloud.com\/", - "endpoints": [ - { - "schemes": [ - "http:\/\/www.mixcloud.com\/*\/*\/", - "https:\/\/www.mixcloud.com\/*\/*\/" - ], - "url": "https:\/\/www.mixcloud.com\/oembed\/" - } - ] - }, - { - "provider_name": "Reddit", - "provider_url": "https:\/\/reddit.com\/", - "endpoints": [ - { - "schemes": [ - "https:\/\/reddit.com\/r\/*\/comments\/*\/*", - "https:\/\/www.reddit.com\/r\/*\/comments\/*\/*" - ], - "url": "https:\/\/www.reddit.com\/oembed" - } - ] - }, - { - "provider_name": "SlideShare", - "provider_url": "http:\/\/www.slideshare.net\/", - "endpoints": [ - { - "schemes": [ - "http:\/\/www.slideshare.net\/*\/*", - "http:\/\/fr.slideshare.net\/*\/*", - "http:\/\/de.slideshare.net\/*\/*", - "http:\/\/es.slideshare.net\/*\/*", - "http:\/\/pt.slideshare.net\/*\/*" - ], - "url": "http:\/\/www.slideshare.net\/api\/oembed\/2", - "discovery": true - } - ] - }, - { - "provider_name": "SoundCloud", - "provider_url": "http:\/\/soundcloud.com\/", - "endpoints": [ - { - "schemes": [ - "http:\/\/soundcloud.com\/*", - "https:\/\/soundcloud.com\/*" - ], - "url": "https:\/\/soundcloud.com\/oembed" - } - ] - }, - { - "provider_name": "Twitch", - "provider_url": "https:\/\/www.twitch.tv", - "endpoints": [ - { - "schemes": [ - "http:\/\/clips.twitch.tv\/*", - "https:\/\/clips.twitch.tv\/*", - "http:\/\/www.twitch.tv\/*", - "https:\/\/www.twitch.tv\/*", - "http:\/\/twitch.tv\/*", - "https:\/\/twitch.tv\/*" - ], - "url": "https:\/\/api.twitch.tv\/v4\/oembed", - "formats": [ - "json" - ] - } - ] - }, - { - "provider_name": "Twitter", - "provider_url": "http:\/\/www.twitter.com\/", - "endpoints": [ - { - "schemes": [ - "https:\/\/twitter.com\/*\/status\/*", - "https:\/\/*.twitter.com\/*\/status\/*" - ], - "url": "https:\/\/publish.twitter.com\/oembed" - } - ] - }, - { - "provider_name": "Vimeo", - "provider_url": "https:\/\/vimeo.com\/", - "endpoints": [ - { - "schemes": [ - "https:\/\/vimeo.com\/*", - "https:\/\/vimeo.com\/album\/*\/video\/*", - "https:\/\/vimeo.com\/channels\/*\/*", - "https:\/\/vimeo.com\/groups\/*\/videos\/*", - "https:\/\/vimeo.com\/ondemand\/*\/*", - "https:\/\/player.vimeo.com\/video\/*" - ], - "url": "https:\/\/vimeo.com\/api\/oembed.{format}", - "discovery": true - } - ] - }, - { - "provider_name": "YouTube", - "provider_url": "https:\/\/www.youtube.com\/", - "endpoints": [ - { - "schemes": [ - "https:\/\/*.youtube.com\/watch*", - "https:\/\/*.youtube.com\/v\/*", - "https:\/\/youtu.be\/*" - ], - "url": "https:\/\/www.youtube.com\/oembed", - "discovery": true - } - ] - } -] \ No newline at end of file + { + "provider_name": "Codepen", + "provider_url": "https://codepen.io", + "endpoints": [ + { + "schemes": ["http://codepen.io/*", "https://codepen.io/*"], + "url": "http://codepen.io/api/oembed" + } + ] + }, + { + "provider_name": "DTube", + "provider_url": "https://d.tube/", + "endpoints": [ + { + "schemes": ["https://d.tube/v/*"], + "url": "https://api.d.tube/oembed", + "discovery": true + } + ] + }, + { + "provider_name": "Facebook (Post)", + "provider_url": "https://www.facebook.com/", + "endpoints": [ + { + "schemes": [ + "https://www.facebook.com/*/posts/*", + "https://www.facebook.com/photos/*", + "https://www.facebook.com/*/photos/*", + "https://www.facebook.com/photo.php*", + "https://www.facebook.com/photo.php", + "https://www.facebook.com/*/activity/*", + "https://www.facebook.com/permalink.php", + "https://www.facebook.com/media/set?set=*", + "https://www.facebook.com/questions/*", + "https://www.facebook.com/notes/*/*/*" + ], + "url": "https://www.facebook.com/plugins/post/oembed.json", + "discovery": true + } + ] + }, + { + "provider_name": "Facebook (Video)", + "provider_url": "https://www.facebook.com/", + "endpoints": [ + { + "schemes": ["https://www.facebook.com/*/videos/*", "https://www.facebook.com/video.php"], + "url": "https://www.facebook.com/plugins/video/oembed.json", + "discovery": true + } + ] + }, + { + "provider_name": "Flickr", + "provider_url": "https://www.flickr.com/", + "endpoints": [ + { + "schemes": [ + "http://*.flickr.com/photos/*", + "http://flic.kr/p/*", + "https://*.flickr.com/photos/*", + "https://flic.kr/p/*" + ], + "url": "https://www.flickr.com/services/oembed/", + "discovery": true + } + ] + }, + { + "provider_name": "GIPHY", + "provider_url": "https://giphy.com", + "endpoints": [ + { + "schemes": [ + "https://giphy.com/gifs/*", + "http://gph.is/*", + "https://media.giphy.com/media/*/giphy.gif" + ], + "url": "https://giphy.com/services/oembed", + "discovery": true + } + ] + }, + { + "provider_name": "Instagram", + "provider_url": "https://instagram.com", + "endpoints": [ + { + "schemes": [ + "http://instagram.com/p/*", + "http://instagr.am/p/*", + "http://www.instagram.com/p/*", + "http://www.instagr.am/p/*", + "https://instagram.com/p/*", + "https://instagr.am/p/*", + "https://www.instagram.com/p/*", + "https://www.instagr.am/p/*" + ], + "url": "https://api.instagram.com/oembed", + "formats": ["json"] + } + ] + }, + { + "provider_name": "Meetup", + "provider_url": "http://www.meetup.com", + "endpoints": [ + { + "schemes": [ + "http://meetup.com/*", + "https://www.meetup.com/*", + "https://meetup.com/*", + "http://meetu.ps/*" + ], + "url": "https://api.meetup.com/oembed", + "formats": ["json"] + } + ] + }, + { + "provider_name": "MixCloud", + "provider_url": "https://mixcloud.com/", + "endpoints": [ + { + "schemes": ["http://www.mixcloud.com/*/*/", "https://www.mixcloud.com/*/*/"], + "url": "https://www.mixcloud.com/oembed/" + } + ] + }, + { + "provider_name": "Reddit", + "provider_url": "https://reddit.com/", + "endpoints": [ + { + "schemes": [ + "https://reddit.com/r/*/comments/*/*", + "https://www.reddit.com/r/*/comments/*/*" + ], + "url": "https://www.reddit.com/oembed" + } + ] + }, + { + "provider_name": "SlideShare", + "provider_url": "http://www.slideshare.net/", + "endpoints": [ + { + "schemes": [ + "http://www.slideshare.net/*/*", + "http://fr.slideshare.net/*/*", + "http://de.slideshare.net/*/*", + "http://es.slideshare.net/*/*", + "http://pt.slideshare.net/*/*" + ], + "url": "http://www.slideshare.net/api/oembed/2", + "discovery": true + } + ] + }, + { + "provider_name": "SoundCloud", + "provider_url": "http://soundcloud.com/", + "endpoints": [ + { + "schemes": ["http://soundcloud.com/*", "https://soundcloud.com/*"], + "url": "https://soundcloud.com/oembed" + } + ] + }, + { + "provider_name": "Twitch", + "provider_url": "https://www.twitch.tv", + "endpoints": [ + { + "schemes": [ + "http://clips.twitch.tv/*", + "https://clips.twitch.tv/*", + "http://www.twitch.tv/*", + "https://www.twitch.tv/*", + "http://twitch.tv/*", + "https://twitch.tv/*" + ], + "url": "https://api.twitch.tv/v4/oembed", + "formats": ["json"] + } + ] + }, + { + "provider_name": "Twitter", + "provider_url": "http://www.twitter.com/", + "endpoints": [ + { + "schemes": ["https://twitter.com/*/status/*", "https://*.twitter.com/*/status/*"], + "url": "https://publish.twitter.com/oembed" + } + ] + }, + { + "provider_name": "Vimeo", + "provider_url": "https://vimeo.com/", + "endpoints": [ + { + "schemes": [ + "https://vimeo.com/*", + "https://vimeo.com/album/*/video/*", + "https://vimeo.com/channels/*/*", + "https://vimeo.com/groups/*/videos/*", + "https://vimeo.com/ondemand/*/*", + "https://player.vimeo.com/video/*" + ], + "url": "https://vimeo.com/api/oembed.{format}", + "discovery": true + } + ] + }, + { + "provider_name": "YouTube", + "provider_url": "https://www.youtube.com/", + "endpoints": [ + { + "schemes": [ + "https://*.youtube.com/watch*", + "https://*.youtube.com/v/*", + "https://youtu.be/*" + ], + "url": "https://www.youtube.com/oembed", + "discovery": true + } + ] + } +] diff --git a/backend/scripts/build.copy.files.sh b/backend/scripts/build.copy.files.sh index 9d17f46ae..1daf19b55 100755 --- a/backend/scripts/build.copy.files.sh +++ b/backend/scripts/build.copy.files.sh @@ -1,24 +1,24 @@ #!/bin/sh -# html files -mkdir -p build/src/middleware/helpers/email/templates/ -cp -r src/middleware/helpers/email/templates/*.html build/src/middleware/helpers/email/templates/ +# public +cp -r public/ build/public/ -mkdir -p build/src/middleware/helpers/email/templates/en/ -cp -r src/middleware/helpers/email/templates/en/*.html build/src/middleware/helpers/email/templates/en/ +# email files +mkdir -p build/src/emails/templates/ +cp -r src/emails/templates/ build/src/emails/ -mkdir -p build/src/middleware/helpers/email/templates/de/ -cp -r src/middleware/helpers/email/templates/de/*.html build/src/middleware/helpers/email/templates/de/ +mkdir -p build/src/emails/locales/ +cp -r src/emails/locales/ build/src/emails/ # gql files -mkdir -p build/src/schema/types/ -cp -r src/schema/types/*.gql build/src/schema/types/ +mkdir -p build/src/graphql/types/ +cp -r src/graphql/types/*.gql build/src/graphql/types/ -mkdir -p build/src/schema/types/enum/ -cp -r src/schema/types/enum/*.gql build/src/schema/types/enum/ +mkdir -p build/src/graphql/types/enum/ +cp -r src/graphql/types/enum/*.gql build/src/graphql/types/enum/ -mkdir -p build/src/schema/types/scalar/ -cp -r src/schema/types/scalar/*.gql build/src/schema/types/scalar/ +mkdir -p build/src/graphql/types/scalar/ +cp -r src/graphql/types/scalar/*.gql build/src/graphql/types/scalar/ -mkdir -p build/src/schema/types/type/ -cp -r src/schema/types/type/*.gql build/src/schema/types/type/ \ No newline at end of file +mkdir -p build/src/graphql/types/type/ +cp -r src/graphql/types/type/*.gql build/src/graphql/types/type/ \ No newline at end of file diff --git a/backend/src/config/emails.ts b/backend/src/config/emails.ts index 34daaecb0..8a746f085 100644 --- a/backend/src/config/emails.ts +++ b/backend/src/config/emails.ts @@ -1,8 +1,5 @@ // this file is duplicated in `backend/src/config/` and `webapp/constants/` and replaced on rebranding by https://github.com/Ocelot-Social-Community/Ocelot-Social-Deploy-Rebranding/tree/master/branding/constants/ export default { - SUPPORT_EMAIL: 'devops@ocelot.social', - MODERATION_EMAIL: 'devops@ocelot.social', - // ATTENTION: the following links have to be defined even for internal pages with full URLs as example like 'https://staging.ocelot.social/support', because they are used in e-mails! ORGANIZATION_LINK: 'https://ocelot.social', SUPPORT_LINK: 'https://ocelot.social', } diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 9f03622a5..0aee79626 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -1,31 +1,34 @@ -import dotenv from 'dotenv' +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ + +/* eslint-disable n/no-process-env */ +import { config } from 'dotenv' +// eslint-disable-next-line import/no-namespace +import * as SMTPTransport from 'nodemailer/lib/smtp-pool' + import emails from './emails' import metadata from './metadata' // Load env file -if (require.resolve) { - try { - dotenv.config({ path: require.resolve('../../.env') }) - } catch (error) { - // This error is thrown when the .env is not found - if (error.code !== 'MODULE_NOT_FOUND') { - throw error - } - } -} +config() // Use Cypress env or process.env +// eslint-disable-next-line @typescript-eslint/no-explicit-any declare let Cypress: any | undefined -const env = typeof Cypress !== 'undefined' ? Cypress.env() : process.env // eslint-disable-line no-undef +const env = (typeof Cypress !== 'undefined' ? Cypress.env() : process.env) as typeof process.env const environment = { - NODE_ENV: env.NODE_ENV || process.env.NODE_ENV, + NODE_ENV: env.NODE_ENV ?? process.env.NODE_ENV, DEBUG: env.NODE_ENV !== 'production' && env.DEBUG, TEST: env.NODE_ENV === 'test', PRODUCTION: env.NODE_ENV === 'production', // used for staging enviroments if 'PRODUCTION=true' and 'PRODUCTION_DB_CLEAN_ALLOW=true' PRODUCTION_DB_CLEAN_ALLOW: env.PRODUCTION_DB_CLEAN_ALLOW === 'true' || false, // default = false - DISABLED_MIDDLEWARES: (env.NODE_ENV !== 'production' && env.DISABLED_MIDDLEWARES) || false, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + DISABLED_MIDDLEWARES: ['test', 'development'].includes(env.NODE_ENV!) + ? (env.DISABLED_MIDDLEWARES?.split(',') ?? []) + : [], + SEND_MAIL: env.NODE_ENV !== 'test', } const required = { @@ -35,30 +38,51 @@ const required = { } const server = { - CLIENT_URI: env.CLIENT_URI || 'http://localhost:3000', - GRAPHQL_URI: env.GRAPHQL_URI || 'http://localhost:4000', - JWT_EXPIRES: env.JWT_EXPIRES || '2y', + CLIENT_URI: env.CLIENT_URI ?? 'http://localhost:3000', + GRAPHQL_URI: env.GRAPHQL_URI ?? 'http://localhost:4000', + JWT_EXPIRES: env.JWT_EXPIRES ?? '2y', } -const hasDKIMData = env.SMTP_DKIM_DOMAINNAME && env.SMTP_DKIM_KEYSELECTOR && env.SMTP_DKIM_PRIVATKEY +const SMTP_HOST = env.SMTP_HOST +const SMTP_PORT = (env.SMTP_PORT && parseInt(env.SMTP_PORT)) || undefined +const SMTP_IGNORE_TLS = env.SMTP_IGNORE_TLS !== 'false' // default = true +const SMTP_SECURE = env.SMTP_SECURE === 'true' +const SMTP_USERNAME = env.SMTP_USERNAME +const SMTP_PASSWORD = env.SMTP_PASSWORD +const SMTP_DKIM_DOMAINNAME = env.SMTP_DKIM_DOMAINNAME +const SMTP_DKIM_KEYSELECTOR = env.SMTP_DKIM_KEYSELECTOR +// PEM format = https://docs.progress.com/bundle/datadirect-hybrid-data-pipeline-installation-46/page/PEM-file-format.html +const SMTP_DKIM_PRIVATKEY = env.SMTP_DKIM_PRIVATKEY?.replace(/\\n/g, '\n') // replace all "\n" in .env string by real line break +const SMTP_MAX_CONNECTIONS = (env.SMTP_MAX_CONNECTIONS && parseInt(env.SMTP_MAX_CONNECTIONS)) || 5 +const SMTP_MAX_MESSAGES = (env.SMTP_MAX_MESSAGES && parseInt(env.SMTP_MAX_MESSAGES)) || 100 -const smtp = { - SMTP_HOST: env.SMTP_HOST, - SMTP_PORT: env.SMTP_PORT, - SMTP_IGNORE_TLS: env.SMTP_IGNORE_TLS !== 'false', // default = true - SMTP_SECURE: env.SMTP_SECURE === 'true', - SMTP_USERNAME: env.SMTP_USERNAME, - SMTP_PASSWORD: env.SMTP_PASSWORD, - SMTP_DKIM_DOMAINNAME: hasDKIMData && env.SMTP_DKIM_DOMAINNAME, - SMTP_DKIM_KEYSELECTOR: hasDKIMData && env.SMTP_DKIM_KEYSELECTOR, - // PEM format: https://docs.progress.com/bundle/datadirect-hybrid-data-pipeline-installation-46/page/PEM-file-format.html - SMTP_DKIM_PRIVATKEY: hasDKIMData && env.SMTP_DKIM_PRIVATKEY.replace(/\\n/g, '\n'), // replace all "\n" in .env string by real line break +const nodemailerTransportOptions: SMTPTransport.Options = { + host: SMTP_HOST, + port: SMTP_PORT, + ignoreTLS: SMTP_IGNORE_TLS, + secure: SMTP_SECURE, // true for 465, false for other ports + pool: true, + maxConnections: SMTP_MAX_CONNECTIONS, + maxMessages: SMTP_MAX_MESSAGES, +} +if (SMTP_USERNAME && SMTP_PASSWORD) { + nodemailerTransportOptions.auth = { + user: SMTP_USERNAME, + pass: SMTP_PASSWORD, + } +} +if (SMTP_DKIM_DOMAINNAME && SMTP_DKIM_KEYSELECTOR && SMTP_DKIM_PRIVATKEY) { + nodemailerTransportOptions.dkim = { + domainName: SMTP_DKIM_DOMAINNAME, + keySelector: SMTP_DKIM_KEYSELECTOR, + privateKey: SMTP_DKIM_PRIVATKEY, + } } const neo4j = { - NEO4J_URI: env.NEO4J_URI || 'bolt://localhost:7687', - NEO4J_USERNAME: env.NEO4J_USERNAME || 'neo4j', - NEO4J_PASSWORD: env.NEO4J_PASSWORD || 'neo4j', + NEO4J_URI: env.NEO4J_URI ?? 'bolt://localhost:7687', + NEO4J_USERNAME: env.NEO4J_USERNAME ?? 'neo4j', + NEO4J_PASSWORD: env.NEO4J_PASSWORD ?? 'neo4j', } const sentry = { @@ -68,7 +92,7 @@ const sentry = { const redis = { REDIS_DOMAIN: env.REDIS_DOMAIN, - REDIS_PORT: env.REDIS_PORT, + REDIS_PORT: (env.REDIS_PORT && parseInt(env.REDIS_PORT)) || undefined, REDIS_PASSWORD: env.REDIS_PASSWORD, } @@ -78,22 +102,48 @@ const s3 = { AWS_ENDPOINT: env.AWS_ENDPOINT, AWS_REGION: env.AWS_REGION, AWS_BUCKET: env.AWS_BUCKET, - S3_CONFIGURED: - env.AWS_ACCESS_KEY_ID && - env.AWS_SECRET_ACCESS_KEY && - env.AWS_ENDPOINT && - env.AWS_REGION && - env.AWS_BUCKET, + S3_PUBLIC_GATEWAY: env.S3_PUBLIC_GATEWAY, +} + +export interface S3Configured { + AWS_ACCESS_KEY_ID: string + AWS_SECRET_ACCESS_KEY: string + AWS_ENDPOINT: string + AWS_REGION: string + AWS_BUCKET: string + S3_PUBLIC_GATEWAY: string | undefined +} + +export const isS3configured = (config: typeof s3): config is S3Configured => { + return !!( + config.AWS_ACCESS_KEY_ID && + config.AWS_SECRET_ACCESS_KEY && + config.AWS_ENDPOINT && + config.AWS_REGION && + config.AWS_BUCKET + ) } const options = { EMAIL_DEFAULT_SENDER: env.EMAIL_DEFAULT_SENDER, + SUPPORT_EMAIL: env.SUPPORT_EMAIL, SUPPORT_URL: emails.SUPPORT_LINK, APPLICATION_NAME: metadata.APPLICATION_NAME, ORGANIZATION_URL: emails.ORGANIZATION_LINK, PUBLIC_REGISTRATION: env.PUBLIC_REGISTRATION === 'true' || false, INVITE_REGISTRATION: env.INVITE_REGISTRATION !== 'false', // default = true + INVITE_CODES_PERSONAL_PER_USER: + (env.INVITE_CODES_PERSONAL_PER_USER && parseInt(env.INVITE_CODES_PERSONAL_PER_USER)) || 7, + INVITE_CODES_GROUP_PER_USER: + (env.INVITE_CODES_GROUP_PER_USER && parseInt(env.INVITE_CODES_GROUP_PER_USER)) || 7, CATEGORIES_ACTIVE: process.env.CATEGORIES_ACTIVE === 'true' || false, + MAX_PINNED_POSTS: Number.isNaN(Number(process.env.MAX_PINNED_POSTS)) + ? 1 + : Number(process.env.MAX_PINNED_POSTS), +} + +const language = { + LANGUAGE_DEFAULT: process.env.LANGUAGE_DEFAULT ?? 'en', } // Check if all required configs are present @@ -108,10 +158,12 @@ export default { ...environment, ...server, ...required, - ...smtp, ...neo4j, ...sentry, ...redis, ...s3, ...options, + ...language, } + +export { nodemailerTransportOptions } diff --git a/backend/src/config/logos.ts b/backend/src/config/logos.ts index 41b83b30c..3c1db1128 100644 --- a/backend/src/config/logos.ts +++ b/backend/src/config/logos.ts @@ -1,10 +1,3 @@ -// this file is duplicated in `backend/src/config/logos` and `webapp/constants/logos.js` and replaced on rebranding +// this file is duplicated in `backend/src/config/logos.ts` and `webapp/constants/logos.js` and replaced on rebranding // this are the paths in the webapp -export default { - LOGO_HEADER_PATH: '/img/custom/logo-horizontal.svg', - LOGO_SIGNUP_PATH: '/img/custom/logo-squared.svg', - LOGO_WELCOME_PATH: '/img/custom/logo-squared.svg', - LOGO_LOGOUT_PATH: '/img/custom/logo-squared.svg', - LOGO_PASSWORD_RESET_PATH: '/img/custom/logo-squared.svg', - LOGO_MAINTENACE_RESET_PATH: '/img/custom/logo-squared.svg', -} +export default {} diff --git a/backend/src/config/logosBranded.ts b/backend/src/config/logosBranded.ts new file mode 100644 index 000000000..348d3c41a --- /dev/null +++ b/backend/src/config/logosBranded.ts @@ -0,0 +1,34 @@ +// this file is duplicated in `backend/src/config/logos.ts` and `webapp/constants/logos.js` and replaced on rebranding +// this are the paths in the webapp +import { merge } from 'lodash' + +import logos from '@config/logos' + +const defaultLogos = { + LOGO_HEADER_PATH: '/img/custom/logo-horizontal.svg', + LOGO_HEADER_TABLET_PATH: '/img/custom/logo-horizontal.svg', + LOGO_HEADER_MOBILE_PATH: '/img/custom/logo-horizontal.svg', + LOGO_HEADER_WIDTH: '130px', + LOGO_HEADER_TABLET_WIDTH: '115px', + LOGO_HEADER_MOBILE_WIDTH: '100px', + LOGO_HEADER_CLICK: { + // externalLink: { + // url: 'https://ocelot.social', + // target: '_blank', + // }, + externalLink: null, + internalPath: { + to: { + name: 'index', + }, + scrollTo: '.main-navigation', + }, + }, + LOGO_SIGNUP_PATH: '/img/custom/logo-squared.svg', + LOGO_WELCOME_PATH: '/img/custom/logo-squared.svg', + LOGO_LOGOUT_PATH: '/img/custom/logo-squared.svg', + LOGO_PASSWORD_RESET_PATH: '/img/custom/logo-squared.svg', + LOGO_MAINTENACE_RESET_PATH: '/img/custom/logo-squared.svg', +} + +export default merge(defaultLogos, logos) diff --git a/backend/src/config/metadata.ts b/backend/src/config/metadata.ts index 282fcb655..9c87818ae 100644 --- a/backend/src/config/metadata.ts +++ b/backend/src/config/metadata.ts @@ -1,9 +1,10 @@ // this file is duplicated in `backend/src/config/metadata` and `webapp/constants/metadata.js` and replaced on rebranding export default { APPLICATION_NAME: 'ocelot.social', - APPLICATION_SHORT_NAME: 'ocelot', + APPLICATION_SHORT_NAME: 'ocelot.social', APPLICATION_DESCRIPTION: 'ocelot.social Community Network', COOKIE_NAME: 'ocelot-social-token', ORGANIZATION_NAME: 'ocelot.social Community', ORGANIZATION_JURISDICTION: 'City of Angels', + THEME_COLOR: 'rgb(23, 181, 63)', // $color-primary – as the main color in general. e.g. the color in the background of the app that is visible behind the transparent iPhone status bar to name one use case, or the current color of SVGs to name another use case } diff --git a/backend/src/constants/badges.ts b/backend/src/constants/badges.ts new file mode 100644 index 000000000..bccebb39a --- /dev/null +++ b/backend/src/constants/badges.ts @@ -0,0 +1,2 @@ +// this file is duplicated in `backend/src/constants/badges` and `webapp/constants/badges.js` +export const TROPHY_BADGES_SELECTED_MAX = 9 diff --git a/backend/src/constants/categories.ts b/backend/src/constants/categories.ts index 6365d268a..b6fce03ca 100644 --- a/backend/src/constants/categories.ts +++ b/backend/src/constants/categories.ts @@ -5,98 +5,116 @@ export const CATEGORIES_MAX = 3 export const categories = [ { icon: 'networking', + id: 'cat0', + slug: 'networking', name: 'networking', - description: 'Kooperation, Aktionsbündnisse, Solidarität, Hilfe', }, { icon: 'home', + id: 'cat1', + slug: 'home', name: 'home', - description: 'Bauen, Lebensgemeinschaften, Tiny Houses, Gemüsegarten', }, { icon: 'energy', + id: 'cat2', + slug: 'energy', name: 'energy', - description: 'Öl, Gas, Kohle, Wind, Wasserkraft, Biogas, Atomenergie, ...', }, { icon: 'psyche', + id: 'cat3', + slug: 'psyche', name: 'psyche', - description: 'Seele, Gefühle, Glück', }, { icon: 'movement', + id: 'cat4', + slug: 'body-and-excercise', name: 'body-and-excercise', - description: 'Sport, Yoga, Massage, Tanzen, Entspannung', }, { icon: 'balance-scale', + id: 'cat5', + slug: 'law', name: 'law', - description: 'Menschenrechte, Gesetze, Verordnungen', }, { icon: 'finance', + id: 'cat6', + slug: 'finance', name: 'finance', - description: 'Geld, Finanzsystem, Alternativwährungen, ...', }, { icon: 'child', + id: 'cat7', + slug: 'children', name: 'children', - description: 'Familie, Pädagogik, Schule, Prägung', }, { icon: 'mobility', + id: 'cat8', + slug: 'mobility', name: 'mobility', - description: 'Reise, Verkehr, Elektromobilität', }, { icon: 'shopping-cart', + id: 'cat9', + slug: 'economy', name: 'economy', - description: 'Handel, Konsum, Marketing, Lebensmittel, Lieferketten, ...', }, { icon: 'peace', + id: 'cat10', + slug: 'peace', name: 'peace', - description: 'Krieg, Militär, soziale Verteidigung, Waffen, Cyberattacken', }, { icon: 'politics', + id: 'cat11', + slug: 'politics', name: 'politics', - description: 'Demokratie, Mitbestimmung, Wahlen, Korruption, Parteien', }, { icon: 'nature', + id: 'cat12', + slug: 'nature', name: 'nature', - description: 'Tiere, Pflanzen, Landwirtschaft, Ökologie, Artenvielfalt', }, { icon: 'science', + id: 'cat13', + slug: 'science', name: 'science', - description: 'Bildung, Hochschule, Publikationen, ...', }, { icon: 'health', + id: 'cat14', + slug: 'health', name: 'health', - description: 'Medizin, Ernährung, WHO, Impfungen, Schadstoffe, ...', }, { icon: 'media', + id: 'cat15', + slug: 'it-and-media', name: 'it-and-media', - description: - 'Nachrichten, Manipulation, Datenschutz, Überwachung, Datenkraken, AI, Software, Apps', }, { icon: 'spirituality', + id: 'cat16', + slug: 'spirituality', name: 'spirituality', - description: 'Religion, Werte, Ethik', }, { icon: 'culture', + id: 'cat17', + slug: 'culture', name: 'culture', - description: 'Kunst, Theater, Musik, Fotografie, Film', }, { icon: 'miscellaneous', + id: 'cat18', + slug: 'miscellaneous', name: 'miscellaneous', - description: '', }, ] diff --git a/backend/src/constants/registration.ts b/backend/src/constants/registration.ts index a08be3521..8ebb40573 100644 --- a/backend/src/constants/registration.ts +++ b/backend/src/constants/registration.ts @@ -1,5 +1,2 @@ -// this file is duplicated in `backend/src/config/metadata` and `webapp/constants/metadata.js` -export default { - NONCE_LENGTH: 5, - INVITE_CODE_LENGTH: 6, -} +// this file is duplicated in `backend/src/config/registration.ts` and `webapp/constants/registration.js` +export default {} diff --git a/backend/src/constants/registrationBranded.ts b/backend/src/constants/registrationBranded.ts new file mode 100644 index 000000000..2ce1d6965 --- /dev/null +++ b/backend/src/constants/registrationBranded.ts @@ -0,0 +1,12 @@ +// this file is duplicated in `backend/src/config/registrationBranded.ts` and `webapp/constants/registrationBranded.js` +import { merge } from 'lodash' + +import registration from '@constants/registration' + +const defaultRegistration = { + NONCE_LENGTH: 5, + INVITE_CODE_LENGTH: 6, + LAYOUT: 'no-header', +} + +export default merge(defaultRegistration, registration) diff --git a/backend/src/constants/subscriptions.ts b/backend/src/constants/subscriptions.ts new file mode 100644 index 000000000..ec3e79e63 --- /dev/null +++ b/backend/src/constants/subscriptions.ts @@ -0,0 +1,3 @@ +export const NOTIFICATION_ADDED = 'NOTIFICATION_ADDED' +export const CHAT_MESSAGE_ADDED = 'CHAT_MESSAGE_ADDED' +export const ROOM_COUNT_UPDATED = 'ROOM_COUNT_UPDATED' diff --git a/backend/src/context/database.ts b/backend/src/context/database.ts new file mode 100644 index 000000000..dc623470d --- /dev/null +++ b/backend/src/context/database.ts @@ -0,0 +1,49 @@ +import { getDriver, getNeode } from '@db/neo4j' + +import type { Driver } from 'neo4j-driver' + +export const query = + (driver: Driver) => + async ({ query, variables = {} }: { query: string; variables?: object }) => { + const session = driver.session() + + const result = session.readTransaction(async (transaction) => { + const response = await transaction.run(query, variables) + return response + }) + + try { + return await result + } finally { + await session.close() + } + } + +export const write = + (driver: Driver) => + async ({ query, variables = {} }: { query: string; variables?: object }) => { + const session = driver.session() + + const result = session.writeTransaction(async (transaction) => { + const response = await transaction.run(query, variables) + return response + }) + + try { + return await result + } finally { + await session.close() + } + } + +export default () => { + const driver = getDriver() + const neode = getNeode() + + return { + driver, + neode, + query: query(driver), + write: write(driver), + } +} diff --git a/backend/src/context/pubsub.ts b/backend/src/context/pubsub.ts new file mode 100644 index 000000000..3d99bba6d --- /dev/null +++ b/backend/src/context/pubsub.ts @@ -0,0 +1,25 @@ +import { RedisPubSub } from 'graphql-redis-subscriptions' +import { PubSub } from 'graphql-subscriptions' +import Redis from 'ioredis' + +import CONFIG from '@config/index' + +export default () => { + const { REDIS_DOMAIN, REDIS_PORT, REDIS_PASSWORD } = CONFIG + if (!(REDIS_DOMAIN && REDIS_PORT && REDIS_PASSWORD)) { + return new PubSub() + } + + const options = { + host: REDIS_DOMAIN, + port: REDIS_PORT, + password: REDIS_PASSWORD, + retryStrategy: (times) => { + return Math.min(times * 50, 2000) + }, + } + return new RedisPubSub({ + publisher: new Redis(options), + subscriber: new Redis(options), + }) +} diff --git a/backend/src/db/admin.ts b/backend/src/db/admin.ts new file mode 100644 index 000000000..1f62c8733 --- /dev/null +++ b/backend/src/db/admin.ts @@ -0,0 +1,54 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ +/* eslint-disable @typescript-eslint/require-await */ + +import { hashSync } from 'bcryptjs' +import { v4 as uuid } from 'uuid' + +import { getDriver } from './neo4j' + +const defaultAdmin = { + email: 'admin@example.org', + // eslint-disable-next-line n/no-sync + password: hashSync('1234', 10), + name: 'admin', + id: uuid(), + slug: 'admin', +} + +const createDefaultAdminUser = async () => { + const driver = getDriver() + const session = driver.session() + const createAdminTxResultPromise = session.writeTransaction(async (txc) => { + txc.run( + `MERGE (e:EmailAddress { + email: "${defaultAdmin.email}", + createdAt: toString(datetime()) + })-[:BELONGS_TO]->(u:User { + name: "${defaultAdmin.name}", + encryptedPassword: "${defaultAdmin.password}", + role: "admin", + id: "${defaultAdmin.id}", + slug: "${defaultAdmin.slug}", + createdAt: toString(datetime()), + allowEmbedIframes: false, + showShoutsPublicly: false, + deleted: false, + disabled: false + })-[:PRIMARY_EMAIL]->(e)`, + ) + }) + try { + await createAdminTxResultPromise + console.log('Successfully created default admin user!') // eslint-disable-line no-console + // eslint-disable-next-line no-catch-all/no-catch-all + } catch (error) { + console.log(error) // eslint-disable-line no-console + } finally { + session.close() + driver.close() + } +} + +;(async function () { + await createDefaultAdminUser() +})() diff --git a/backend/src/db/badges.ts b/backend/src/db/badges.ts new file mode 100644 index 000000000..cbad0b004 --- /dev/null +++ b/backend/src/db/badges.ts @@ -0,0 +1,14 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ +import { getNeode } from './neo4j' +import { trophies, verification } from './seed/badges' + +// eslint-disable-next-line import/newline-after-import +;(async function () { + const neode = getNeode() + try { + await trophies() + await verification() + } finally { + neode.close() + } +})() diff --git a/backend/src/db/categories.ts b/backend/src/db/categories.ts new file mode 100644 index 000000000..8c8943bd4 --- /dev/null +++ b/backend/src/db/categories.ts @@ -0,0 +1,48 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ + +import { categories } from '@constants/categories' +import databaseContext from '@context/database' + +const { query, write, driver } = databaseContext() + +const createCategories = async () => { + const result = await query({ + query: 'MATCH (category:Category) RETURN category { .* }', + }) + + const categoryIds = categories.map((c) => c.id) + const categorySlugs = categories.map((c) => c.slug) + await write({ + query: `MATCH (category:Category) + WHERE NOT category.id IN $categoryIds + DETACH DELETE category`, + variables: { + categoryIds, + categorySlugs, + }, + }) + + const existingCategories = result.records.map((r) => r.get('category')) + + const newCategories = categories.filter((c) => !existingCategories.some((cat) => c.id === cat.id)) + + await write({ + query: `UNWIND $newCategories AS map + CREATE (category:Category) + SET category = map + SET category.createdAt = toString(datetime())`, + variables: { + newCategories, + }, + }) + + // eslint-disable-next-line no-console + console.log('Successfully created categories!') + await driver.close() +} + +;(async function () { + await createCategories() +})() diff --git a/backend/src/db/compiler.ts b/backend/src/db/compiler.ts index 8b09ac9c3..1b364f919 100644 --- a/backend/src/db/compiler.ts +++ b/backend/src/db/compiler.ts @@ -1,2 +1,9 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable import/no-commonjs */ +// eslint-disable-next-line n/no-unpublished-require, @typescript-eslint/no-var-requires const tsNode = require('ts-node') +// eslint-disable-next-line import/no-unassigned-import, n/no-unpublished-require +require('tsconfig-paths/register') + module.exports = tsNode.register diff --git a/backend/src/db/data-branding.ts b/backend/src/db/data-branding.ts new file mode 100644 index 000000000..eceaf391b --- /dev/null +++ b/backend/src/db/data-branding.ts @@ -0,0 +1,27 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-floating-promises */ +import { readdir } from 'node:fs/promises' +import path from 'node:path' + +import { getNeode } from './neo4j' + +const dataFolder = path.join(__dirname, 'data/') +const neode = getNeode() + +;(async function () { + const files = await readdir(dataFolder) + for await (const file of files) { + if (file.slice(0, -3).endsWith('-branding')) { + const importedModule = await import(path.join(dataFolder, file)) + if (!importedModule.default) { + throw new Error('Your data file must export a default function') + } + await importedModule.default() + } + } + + // close database connection + neode.close() +})() diff --git a/backend/src/db/data/.gitkeep b/backend/src/db/data/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/db/factories.ts b/backend/src/db/factories.ts index c75c92fdd..a5237dada 100644 --- a/backend/src/db/factories.ts +++ b/backend/src/db/factories.ts @@ -1,35 +1,43 @@ -import { v4 as uuid } from 'uuid' -import slugify from 'slug' +/* eslint-disable @typescript-eslint/unbound-method */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { faker } from '@faker-js/faker' import { hashSync } from 'bcryptjs' import { Factory } from 'rosie' -import { faker } from '@faker-js/faker' +import slugify from 'slug' +import { v4 as uuid } from 'uuid' + +import { generateInviteCode } from '@graphql/resolvers/inviteCodes' + import { getDriver, getNeode } from './neo4j' -import CONFIG from '../config/index' -import generateInviteCode from '../schema/resolvers/helpers/generateInviteCode' const neode = getNeode() const uniqueImageUrl = (imageUrl) => { - const newUrl = new URL(imageUrl, CONFIG.CLIENT_URI) + const newUrl = new URL(imageUrl) newUrl.search = `random=${uuid()}` return newUrl.toString() } -export const cleanDatabase = async (options: any = {}) => { - const { driver = getDriver() } = options +export const cleanDatabase = async ({ withMigrations } = { withMigrations: false }) => { + const driver = getDriver() const session = driver.session() + + const clean = ` + MATCH (everything) + ${withMigrations ? '' : "WHERE NOT 'Migration' IN labels(everything)"} + DETACH DELETE everything + ` + try { await session.writeTransaction((transaction) => { - return transaction.run( - ` - MATCH (everything) - WHERE NOT 'Migration' IN labels(everything) - DETACH DELETE everything - `, - ) + return transaction.run(clean) }) } finally { - session.close() + await session.close() } } @@ -37,25 +45,34 @@ Factory.define('category') .attr('id', uuid) .attr('icon', 'globe') .attr('name', 'Global Peace & Nonviolence') - .after((buildObject, options) => { + .after((buildObject, _options) => { return neode.create('Category', buildObject) }) Factory.define('badge') .attr('type', 'crowdfunding') .attr('status', 'permanent') - .after((buildObject, options) => { + .after((buildObject, _options) => { return neode.create('Badge', buildObject) }) Factory.define('image') - .attr('url', faker.image.url) - .attr('aspectRatio', 1.3333333333333333) + .attr('width', 400) + .attr('height', 300) + .attr('blur', 0) .attr('alt', faker.lorem.sentence) .attr('type', 'image/jpeg') - .after((buildObject, options) => { - const { url: imageUrl } = buildObject - if (imageUrl) buildObject.url = uniqueImageUrl(imageUrl) + .attr('url', null) + .after((buildObject, _options) => { + if (!buildObject.url) { + buildObject.url = faker.image.urlPicsumPhotos({ + width: buildObject.width, + height: buildObject.height, + blur: buildObject.blur, + }) + } + buildObject.url = uniqueImageUrl(buildObject.url) + buildObject.aspectRatio = buildObject.width / buildObject.height return neode.create('Image', buildObject) }) @@ -70,34 +87,34 @@ Factory.define('basicUser') termsAndConditionsAgreedAt: '2019-08-01T10:47:19.212Z', allowEmbedIframes: false, showShoutsPublicly: false, - sendNotificationEmails: true, locale: 'en', }) .attr('slug', ['slug', 'name'], (slug, name) => { return slug || slugify(name, { lower: true }) }) .attr('encryptedPassword', ['password'], (password) => { + // eslint-disable-next-line n/no-sync return hashSync(password, 10) }) Factory.define('userWithoutEmailAddress') .extend('basicUser') .option('about', faker.lorem.paragraph) - .after(async (buildObject, options) => { + .after(async (buildObject, _options) => { return neode.create('User', buildObject) }) Factory.define('userWithAboutNull') .extend('basicUser') .option('about', null) - .after(async (buildObject, options) => { + .after(async (buildObject, _options) => { return neode.create('User', buildObject) }) Factory.define('userWithAboutEmpty') .extend('basicUser') .option('about', '') - .after(async (buildObject, options) => { + .after(async (buildObject, _options) => { return neode.create('User', buildObject) }) @@ -173,6 +190,7 @@ Factory.define('post') ]) await Promise.all([ post.relateTo(author, 'author'), + post.relateTo(author, 'observes'), // Promise.all(categories.map((c) => c.relateTo(post, 'post'))), Promise.all(tags.map((t) => t.relateTo(post, 'post'))), ]) @@ -208,7 +226,11 @@ Factory.define('comment') options.author, options.post, ]) - await Promise.all([comment.relateTo(author, 'author'), comment.relateTo(post, 'post')]) + await Promise.all([ + comment.relateTo(author, 'author'), + comment.relateTo(post, 'post'), + post.relateTo(author, 'observes'), + ]) return comment }) @@ -217,7 +239,7 @@ Factory.define('donations') .attr('showDonations', true) .attr('goal', 15000) .attr('progress', 7000) - .after((buildObject, options) => { + .after((buildObject, _options) => { return neode.create('Donations', buildObject) }) @@ -228,13 +250,13 @@ const emailDefaults = { Factory.define('emailAddress') .attrs(emailDefaults) - .after((buildObject, options) => { + .after((buildObject, _options) => { return neode.create('EmailAddress', buildObject) }) Factory.define('unverifiedEmailAddress') .attr(emailDefaults) - .after((buildObject, options) => { + .after((buildObject, _options) => { return neode.create('UnverifiedEmailAddress', buildObject) }) @@ -246,17 +268,27 @@ const inviteCodeDefaults = { Factory.define('inviteCode') .attrs(inviteCodeDefaults) + .option('groupId', null) + .option('group', ['groupId'], (groupId) => { + if (groupId) { + return neode.find('Group', groupId) + } + }) .option('generatedById', null) .option('generatedBy', ['generatedById'], (generatedById) => { if (generatedById) return neode.find('User', generatedById) return Factory.build('user') }) .after(async (buildObject, options) => { - const [inviteCode, generatedBy] = await Promise.all([ + const [inviteCode, generatedBy, group] = await Promise.all([ neode.create('InviteCode', buildObject), options.generatedBy, + options.group, ]) - await Promise.all([inviteCode.relateTo(generatedBy, 'generated')]) + await inviteCode.relateTo(generatedBy, 'generated') + if (group) { + await inviteCode.relateTo(group, 'invitesTo') + } return inviteCode }) @@ -274,11 +306,11 @@ Factory.define('location') id: 'country.10743216036480410', type: 'country', }) - .after((buildObject, options) => { + .after((buildObject, _options) => { return neode.create('Location', buildObject) }) -Factory.define('report').after((buildObject, options) => { +Factory.define('report').after((buildObject, _options) => { return neode.create('Report', buildObject) }) @@ -286,7 +318,7 @@ Factory.define('tag') .attrs({ name: '#human-connection', }) - .after((buildObject, options) => { + .after((buildObject, _options) => { return neode.create('Tag', buildObject) }) @@ -294,7 +326,7 @@ Factory.define('socialMedia') .attrs({ url: 'https://mastodon.social/@Gargron', }) - .after((buildObject, options) => { + .after((buildObject, _options) => { return neode.create('SocialMedia', buildObject) }) diff --git a/backend/src/db/migrate/store.ts b/backend/src/db/migrate/store.ts index 0c0b63943..9976be8b4 100644 --- a/backend/src/db/migrate/store.ts +++ b/backend/src/db/migrate/store.ts @@ -1,104 +1,52 @@ -import { getDriver, getNeode } from '../../db/neo4j' -import { hashSync } from 'bcryptjs' -import { v4 as uuid } from 'uuid' -import { categories } from '../../constants/categories' -import CONFIG from '../../config' - -const defaultAdmin = { - email: 'admin@example.org', - password: hashSync('1234', 10), - name: 'admin', - id: uuid(), - slug: 'admin', -} - -const createCategories = async (session) => { - const createCategoriesTxResultPromise = session.writeTransaction(async (txc) => { - categories.forEach(({ icon, name }, index) => { - const id = `cat${index + 1}` - txc.run( - `MERGE (c:Category { - icon: "${icon}", - slug: "${name}", - name: "${name}", - id: "${id}", - createdAt: toString(datetime()) - })`, - ) - }) - }) - try { - await createCategoriesTxResultPromise - console.log('Successfully created categories!') // eslint-disable-line no-console - } catch (error) { - console.log(`Error creating categories: ${error}`) // eslint-disable-line no-console - } -} - -const createDefaultAdminUser = async (session) => { - const readTxResultPromise = session.readTransaction(async (txc) => { - const result = await txc.run('MATCH (user:User) RETURN count(user) AS userCount') - return result.records.map((r) => r.get('userCount')) - }) - let createAdmin = false - try { - const userCount = parseInt(String(await readTxResultPromise)) - if (userCount === 0) createAdmin = true - } catch (error) { - console.log(error) // eslint-disable-line no-console - } - if (createAdmin) { - const createAdminTxResultPromise = session.writeTransaction(async (txc) => { - txc.run( - `MERGE (e:EmailAddress { - email: "${defaultAdmin.email}", - createdAt: toString(datetime()) - })-[:BELONGS_TO]->(u:User { - name: "${defaultAdmin.name}", - encryptedPassword: "${defaultAdmin.password}", - role: "admin", - id: "${defaultAdmin.id}", - slug: "${defaultAdmin.slug}", - createdAt: toString(datetime()), - allowEmbedIframes: false, - showShoutsPublicly: false, - sendNotificationEmails: true, - deleted: false, - disabled: false - })-[:PRIMARY_EMAIL]->(e)`, - ) - }) - try { - await createAdminTxResultPromise - console.log('Successfully created default admin user!') // eslint-disable-line no-console - } catch (error) { - console.log(error) // eslint-disable-line no-console - } - } -} +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { getDriver, getNeode } from '@db/neo4j' class Store { - async init(next) { + async init(errFn) { const neode = getNeode() - const { driver } = neode - const session = driver.session() - await createDefaultAdminUser(session) - if (CONFIG.CATEGORIES_ACTIVE) await createCategories(session) - const writeTxResultPromise = session.writeTransaction(async (txc) => { - await txc.run('CALL apoc.schema.assert({},{},true)') // drop all indices and constraints + const session = neode.session() + const txFreshIndicesConstrains = session.writeTransaction(async (txc) => { + // drop all indices and constraints + await txc.run('CALL apoc.schema.assert({},{},true)') + /* + ############################################# + # ADD YOUR CUSTOM INDICES & CONSTRAINS HERE # + ############################################# + */ + // Search indexes (also part of migration 20230320130345-fulltext-search-indexes) + await txc.run( + `CALL db.index.fulltext.createNodeIndex("user_fulltext_search",["User"],["name", "slug"])`, + ) + await txc.run( + `CALL db.index.fulltext.createNodeIndex("post_fulltext_search",["Post"],["title", "content"])`, + ) + await txc.run(`CALL db.index.fulltext.createNodeIndex("tag_fulltext_search",["Tag"],["id"])`) // also part of migration 20200207080200-fulltext_index_for_tags + // Search indexes (also part of migration 20220803060819-create_fulltext_indices_and_unique_keys_for_groups) + await txc.run(` + CALL db.index.fulltext.createNodeIndex("group_fulltext_search",["Group"],["name", "slug", "about", "description"]) + `) }) try { - await writeTxResultPromise + // Due to limitations of neode in combination with the limitations of the community version of neo4j + // we need to have all constraints and indexes defined here. They can not be properly migrated + await txFreshIndicesConstrains + + // You have to wait for the schema to install, else the constraints will not be present. + // This is a type error of the library + // eslint-disable-next-line @typescript-eslint/await-thenable await getNeode().schema.install() // eslint-disable-next-line no-console console.log('Successfully created database indices and constraints!') - next() + // eslint-disable-next-line no-catch-all/no-catch-all } catch (error) { console.log(error) // eslint-disable-line no-console - next(error, null) + errFn(error) } finally { - session.close() - driver.close() + await session.close() + neode.close() } } @@ -122,11 +70,12 @@ class Store { } const [{ title: lastRun }] = migrations next(null, { lastRun, migrations }) + // eslint-disable-next-line no-catch-all/no-catch-all } catch (error) { console.log(error) // eslint-disable-line no-console next(error) } finally { - session.close() + await session.close() } } @@ -157,13 +106,14 @@ class Store { try { await writeTxResultPromise next() + // eslint-disable-next-line no-catch-all/no-catch-all } catch (error) { console.log(error) // eslint-disable-line no-console next(error) } finally { - session.close() + await session.close() } } } -module.exports = Store +export default Store diff --git a/backend/src/db/migrate/template.ts b/backend/src/db/migrate/template.ts index 72bfc9b1b..a7287dd42 100644 --- a/backend/src/db/migrate/template.ts +++ b/backend/src/db/migrate/template.ts @@ -1,8 +1,10 @@ -import { getDriver } from '../../db/neo4j' +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + +import { getDriver } from '@db/neo4j' export const description = '' -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -11,7 +13,6 @@ export async function up(next) { // Implement your migration here. await transaction.run(``) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -20,11 +21,11 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -33,7 +34,6 @@ export async function down(next) { // Implement your migration here. await transaction.run(``) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -42,6 +42,6 @@ export async function down(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations-examples/20200123150105-merge_duplicate_user_accounts.ts b/backend/src/db/migrations-examples/20200123150105-merge_duplicate_user_accounts.ts index 7d98d9dcc..ea64c58af 100644 --- a/backend/src/db/migrations-examples/20200123150105-merge_duplicate_user_accounts.ts +++ b/backend/src/db/migrations-examples/20200123150105-merge_duplicate_user_accounts.ts @@ -1,7 +1,16 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable promise/prefer-await-to-callbacks */ import { throwError, concat } from 'rxjs' import { flatMap, mergeMap, map, catchError, filter } from 'rxjs/operators' -import { getDriver } from '../neo4j' -import normalizeEmail from '../../schema/resolvers//helpers/normalizeEmail' + +import { getDriver } from '@db/neo4j' +import normalizeEmail from '@graphql/resolvers/helpers/normalizeEmail' export const description = ` This migration merges duplicate :User and :EmailAddress nodes. It became @@ -14,16 +23,19 @@ export const description = ` ` export function up(next) { const driver = getDriver() - const rxSession = driver.rxSession() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const rxSession = driver.rxSession() as any rxSession .beginTransaction() .pipe( + // eslint-disable-next-line @typescript-eslint/no-explicit-any flatMap((txc: any) => concat( txc .run('MATCH (email:EmailAddress) RETURN email {.email}') .records() .pipe( + // eslint-disable-next-line @typescript-eslint/no-explicit-any map((record: any) => { const { email } = record.get('email') const normalizedEmail = normalizeEmail(email) @@ -45,6 +57,7 @@ export function up(next) { ) .records() .pipe( + // eslint-disable-next-line @typescript-eslint/no-explicit-any map((r: any) => ({ oldEmail: email, email: r.get('email'), @@ -58,7 +71,7 @@ export function up(next) { ), ) .subscribe({ - next: ({ user, email, oldUser, oldEmail }) => + next: ({ user, email, _oldUser, oldEmail }) => // eslint-disable-next-line no-console console.log(` Merged: diff --git a/backend/src/db/migrations-examples/20200123150110-merge_duplicate_location_nodes.ts b/backend/src/db/migrations-examples/20200123150110-merge_duplicate_location_nodes.ts index 10b77c6dd..de73bdaae 100644 --- a/backend/src/db/migrations-examples/20200123150110-merge_duplicate_location_nodes.ts +++ b/backend/src/db/migrations-examples/20200123150110-merge_duplicate_location_nodes.ts @@ -1,6 +1,15 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable promise/prefer-await-to-callbacks */ import { throwError, concat } from 'rxjs' import { flatMap, mergeMap, map, catchError } from 'rxjs/operators' -import { getDriver } from '../neo4j' + +import { getDriver } from '@db/neo4j' export const description = ` This migration merges duplicate :Location nodes. It became @@ -8,10 +17,12 @@ export const description = ` ` export function up(next) { const driver = getDriver() - const rxSession = driver.rxSession() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const rxSession = driver.rxSession() as any rxSession .beginTransaction() .pipe( + // eslint-disable-next-line @typescript-eslint/no-explicit-any flatMap((transaction: any) => concat( transaction @@ -23,6 +34,7 @@ export function up(next) { ) .records() .pipe( + // eslint-disable-next-line @typescript-eslint/no-explicit-any map((record: any) => { const { id: locationId } = record.get('location') return { locationId } @@ -40,6 +52,7 @@ export function up(next) { ) .records() .pipe( + // eslint-disable-next-line @typescript-eslint/no-explicit-any map((record: any) => ({ location: record.get('location'), updatedLocation: record.get('updatedLocation'), diff --git a/backend/src/db/migrations-examples/20200127110135-create_muted_relationship_between_existing_blocked_relationships.ts b/backend/src/db/migrations-examples/20200127110135-create_muted_relationship_between_existing_blocked_relationships.ts index ce46be9d6..8be7bad07 100644 --- a/backend/src/db/migrations-examples/20200127110135-create_muted_relationship_between_existing_blocked_relationships.ts +++ b/backend/src/db/migrations-examples/20200127110135-create_muted_relationship_between_existing_blocked_relationships.ts @@ -1,4 +1,6 @@ -import { getDriver } from '../../db/neo4j' +/* eslint-disable @typescript-eslint/no-unsafe-call */ + +import { getDriver } from '@db/neo4j' export const description = ` This migration creates a MUTED relationship between two edges(:User) that have a pre-existing BLOCKED relationship. @@ -8,7 +10,7 @@ export const description = ` A blocked user will still be able to see your contributions, but will not be able to interact with them and vice versa. ` -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -21,6 +23,7 @@ export async function up(next) { `, ) await transaction.commit() + // eslint-disable-next-line no-catch-all/no-catch-all } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -28,19 +31,20 @@ export async function up(next) { // eslint-disable-next-line no-console console.log('rolled back') } finally { - session.close() + await session.close() } } -export function down(next) { +export async function down(next) { const driver = getDriver() const session = driver.session() try { // Rollback your migration here. - next() + // next() + // eslint-disable-next-line no-catch-all/no-catch-all } catch (err) { next(err) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations-examples/20200206190233-swap_latitude_with_longitude.ts b/backend/src/db/migrations-examples/20200206190233-swap_latitude_with_longitude.ts index 94a2f442d..f63be216d 100644 --- a/backend/src/db/migrations-examples/20200206190233-swap_latitude_with_longitude.ts +++ b/backend/src/db/migrations-examples/20200206190233-swap_latitude_with_longitude.ts @@ -1,4 +1,9 @@ -import { getDriver } from '../../db/neo4j' +/* eslint-disable @typescript-eslint/no-floating-promises */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ + +import { getDriver } from '@db/neo4j' export const description = ` This migration swaps the value stored in Location.lat with the value diff --git a/backend/src/db/migrations-examples/20200207080200-fulltext_index_for_tags.ts b/backend/src/db/migrations-examples/20200207080200-fulltext_index_for_tags.ts index ffcd3d4b6..f2e32d6f8 100644 --- a/backend/src/db/migrations-examples/20200207080200-fulltext_index_for_tags.ts +++ b/backend/src/db/migrations-examples/20200207080200-fulltext_index_for_tags.ts @@ -1,4 +1,8 @@ -import { getDriver } from '../../db/neo4j' +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { getDriver } from '@db/neo4j' export const description = 'This migration adds a fulltext index for the tags in order to search for Hasthags.' @@ -9,10 +13,13 @@ export async function up(next) { const transaction = session.beginTransaction() try { + // We do do this in /src/db/migrate/store.ts + /* await transaction.run(` CALL db.index.fulltext.createNodeIndex("tag_fulltext_search",["Tag"],["id"]) `) await transaction.commit() + */ next() } catch (error) { const { message } = error @@ -28,7 +35,7 @@ export async function up(next) { throw new Error(error) } } finally { - session.close() + await session.close() } } @@ -39,10 +46,13 @@ export async function down(next) { try { // Implement your migration here. + // We do do this in /src/db/migrate/store.ts + /* await transaction.run(` CALL db.index.fulltext.drop("tag_fulltext_search") `) await transaction.commit() + */ next() } catch (error) { // eslint-disable-next-line no-console @@ -52,6 +62,6 @@ export async function down(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations-examples/20200213230248-add_unique_index_to_image_url.ts b/backend/src/db/migrations-examples/20200213230248-add_unique_index_to_image_url.ts index 4582d938c..81d96f68c 100644 --- a/backend/src/db/migrations-examples/20200213230248-add_unique_index_to_image_url.ts +++ b/backend/src/db/migrations-examples/20200213230248-add_unique_index_to_image_url.ts @@ -1,4 +1,8 @@ -import { getDriver } from '../../db/neo4j' +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { getDriver } from '@db/neo4j' export const description = ` We introduced a new node label 'Image' and we need a primary key for it. Best @@ -32,7 +36,7 @@ export async function up(next) { throw new Error(error) } } finally { - session.close() + await session.close() } } @@ -48,6 +52,7 @@ export async function down(next) { `) await transaction.commit() next() + // eslint-disable-next-line no-catch-all/no-catch-all } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -55,6 +60,6 @@ export async function down(next) { // eslint-disable-next-line no-console console.log('rolled back') } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations-examples/20200312140328-bulk_upload_to_s3.ts b/backend/src/db/migrations-examples/20200312140328-bulk_upload_to_s3.ts deleted file mode 100644 index 356004237..000000000 --- a/backend/src/db/migrations-examples/20200312140328-bulk_upload_to_s3.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { getDriver } from '../../db/neo4j' -import { existsSync, createReadStream } from 'fs' -import path from 'path' -import { S3 } from 'aws-sdk' -import mime from 'mime-types' -import s3Configs from '../../config' -import https from 'https' - -export const description = ` -Upload all image files to a S3 compatible object storage in order to reduce -load on our backend. -` - -export async function up(next) { - const driver = getDriver() - const session = driver.session() - const transaction = session.beginTransaction() - const agent = new https.Agent({ - maxSockets: 5, - }) - - const { - AWS_ENDPOINT: endpoint, - AWS_REGION: region, - AWS_BUCKET: Bucket, - S3_CONFIGURED, - } = s3Configs - - if (!S3_CONFIGURED) { - // eslint-disable-next-line no-console - console.log('No S3 given, cannot upload image files') - return - } - - const s3 = new S3({ region, endpoint, httpOptions: { agent } }) - try { - // Implement your migration here. - const { records } = await transaction.run('MATCH (image:Image) RETURN image.url as url') - let urls = records.map((r) => r.get('url')) - urls = urls.filter((url) => url.startsWith('/uploads')) - const locations = await Promise.all( - urls - .map((url) => { - return async () => { - const { pathname } = new URL(url, 'http://example.org') - const fileLocation = path.join(__dirname, `../../../public/${pathname}`) - const s3Location = `original${pathname}` - if (existsSync(fileLocation)) { - const mimeType = mime.lookup(fileLocation) - const params = { - Bucket, - Key: s3Location, - ACL: 'public-read', - ContentType: mimeType || 'image/jpeg', - Body: createReadStream(fileLocation), - } - - const data = await s3.upload(params).promise() - const { Location: spacesUrl } = data - - const updatedRecord = await transaction.run( - 'MATCH (image:Image {url: $url}) SET image.url = $spacesUrl RETURN image.url as url', - { url, spacesUrl }, - ) - const [updatedUrl] = updatedRecord.records.map((record) => record.get('url')) - return updatedUrl - } - } - }) - .map((p) => p()), - ) - // eslint-disable-next-line no-console - console.log('this is locations', locations) - await transaction.commit() - next() - } catch (error) { - // eslint-disable-next-line no-console - console.log(error) - await transaction.rollback() - // eslint-disable-next-line no-console - console.log('rolled back') - throw new Error(error) - } finally { - session.close() - } -} - -export async function down(next) { - const driver = getDriver() - const session = driver.session() - const transaction = session.beginTransaction() - - try { - // Implement your migration here. - await transaction.run(``) - await transaction.commit() - next() - } catch (error) { - // eslint-disable-next-line no-console - console.log(error) - await transaction.rollback() - // eslint-disable-next-line no-console - console.log('rolled back') - } finally { - session.close() - } -} diff --git a/backend/src/db/migrations-examples/20200320200315-refactor_all_images_to_separate_type.ts b/backend/src/db/migrations-examples/20200320200315-refactor_all_images_to_separate_type.ts index 1ad5e645d..1d24bd141 100644 --- a/backend/src/db/migrations-examples/20200320200315-refactor_all_images_to_separate_type.ts +++ b/backend/src/db/migrations-examples/20200320200315-refactor_all_images_to_separate_type.ts @@ -1,5 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ + /* eslint-disable no-console */ -import { getDriver } from '../../db/neo4j' +import { getDriver } from '@db/neo4j' export const description = ` Refactor all our image properties on posts and users to a dedicated type @@ -58,7 +61,7 @@ export async function up() { console.log('Created image nodes from all user avatars and post images.') printSummaries(stats) } finally { - session.close() + await session.close() } } @@ -96,6 +99,6 @@ export async function down() { console.log('UNDO: Split images from users and posts.') printSummaries(stats) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations-examples/20200323140300-remove_deleted_users_obsolete_attributes.ts b/backend/src/db/migrations-examples/20200323140300-remove_deleted_users_obsolete_attributes.ts index e4852f79c..b75324a78 100644 --- a/backend/src/db/migrations-examples/20200323140300-remove_deleted_users_obsolete_attributes.ts +++ b/backend/src/db/migrations-examples/20200323140300-remove_deleted_users_obsolete_attributes.ts @@ -1,4 +1,9 @@ -import { getDriver } from '../../db/neo4j' +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ + +import { getDriver } from '@db/neo4j' export const description = 'We should not maintain obsolete attributes for users who have been deleted.' @@ -20,7 +25,7 @@ export async function up(next) { `) try { // Implement your migration here. - const users = await updateDeletedUserAttributes.records.map((record) => record.get('user')) + const users = updateDeletedUserAttributes.records.map((record) => record.get('user')) // eslint-disable-next-line no-console console.log(users) await transaction.commit() @@ -33,7 +38,7 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations-examples/20200323160336-remove_deleted_posts_obsolete_attributes.ts b/backend/src/db/migrations-examples/20200323160336-remove_deleted_posts_obsolete_attributes.ts index 8c1efe5c6..597eb1d83 100644 --- a/backend/src/db/migrations-examples/20200323160336-remove_deleted_posts_obsolete_attributes.ts +++ b/backend/src/db/migrations-examples/20200323160336-remove_deleted_posts_obsolete_attributes.ts @@ -1,4 +1,9 @@ -import { getDriver } from '../../db/neo4j' +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ + +import { getDriver } from '@db/neo4j' export const description = 'We should not maintain obsolete attributes for posts which have been deleted.' @@ -22,7 +27,7 @@ export async function up(next) { `) try { // Implement your migration here. - const posts = await updateDeletedPostsAttributes.records.map((record) => record.get('post')) + const posts = updateDeletedPostsAttributes.records.map((record) => record.get('post')) // eslint-disable-next-line no-console console.log(posts) await transaction.commit() @@ -35,7 +40,7 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations-examples/20200326160326-remove_dangling_image_urls.ts b/backend/src/db/migrations-examples/20200326160326-remove_dangling_image_urls.ts index a77ac360c..1109ac623 100644 --- a/backend/src/db/migrations-examples/20200326160326-remove_dangling_image_urls.ts +++ b/backend/src/db/migrations-examples/20200326160326-remove_dangling_image_urls.ts @@ -1,5 +1,13 @@ -import { getDriver } from '../../db/neo4j' -import { existsSync } from 'fs' +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ + +/* eslint-disable security/detect-non-literal-fs-filename */ +import { existsSync } from 'node:fs' + +import { getDriver } from '@db/neo4j' export const description = ` In this review: @@ -24,6 +32,7 @@ export async function up(next) { const urls = records.map((record) => record.get('url')) const danglingUrls = urls.filter((url) => { const fileLocation = `public${url}` + // eslint-disable-next-line n/no-sync return !existsSync(fileLocation) }) await transaction.run( @@ -52,7 +61,7 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/1613589876420-null_mutation.ts b/backend/src/db/migrations/1613589876420-null_mutation.ts index f158549de..daeba5dca 100644 --- a/backend/src/db/migrations/1613589876420-null_mutation.ts +++ b/backend/src/db/migrations/1613589876420-null_mutation.ts @@ -1,9 +1,6 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ 'use strict' -module.exports.up = function (next) { - next() -} +export async function up(_next) {} -module.exports.down = function (next) { - next() -} +export async function down(_next) {} diff --git a/backend/src/db/migrations/1614023644903-add-clickedCount-to-posts.ts b/backend/src/db/migrations/1614023644903-add-clickedCount-to-posts.ts index ff95a25df..7443b4749 100644 --- a/backend/src/db/migrations/1614023644903-add-clickedCount-to-posts.ts +++ b/backend/src/db/migrations/1614023644903-add-clickedCount-to-posts.ts @@ -1,10 +1,12 @@ -import { getDriver } from '../../db/neo4j' +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + +import { getDriver } from '@db/neo4j' export const description = ` This migration adds the clickedCount property to all posts, setting it to 0. ` -module.exports.up = async function (next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -15,7 +17,6 @@ module.exports.up = async function (next) { SET p.clickedCount = 0 `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -24,11 +25,11 @@ module.exports.up = async function (next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } -module.exports.down = async function (next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -39,7 +40,6 @@ module.exports.down = async function (next) { REMOVE p.clickedCount `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -48,6 +48,6 @@ module.exports.down = async function (next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/1614177130817-add-viewedTeaserCount-to-posts.ts b/backend/src/db/migrations/1614177130817-add-viewedTeaserCount-to-posts.ts index ee1fad124..b23bf96bf 100644 --- a/backend/src/db/migrations/1614177130817-add-viewedTeaserCount-to-posts.ts +++ b/backend/src/db/migrations/1614177130817-add-viewedTeaserCount-to-posts.ts @@ -1,10 +1,12 @@ -import { getDriver } from '../../db/neo4j' +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + +import { getDriver } from '@db/neo4j' export const description = ` This migration adds the viewedTeaserCount property to all posts, setting it to 0. ` -module.exports.up = async function (next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -15,7 +17,6 @@ module.exports.up = async function (next) { SET p.viewedTeaserCount = 0 `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -24,11 +25,11 @@ module.exports.up = async function (next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } -module.exports.down = async function (next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -39,7 +40,6 @@ module.exports.down = async function (next) { REMOVE p.viewedTeaserCount `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -48,6 +48,6 @@ module.exports.down = async function (next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20210506150512-add-donations-node.ts b/backend/src/db/migrations/20210506150512-add-donations-node.ts index 6cbc1e897..90f00e26f 100644 --- a/backend/src/db/migrations/20210506150512-add-donations-node.ts +++ b/backend/src/db/migrations/20210506150512-add-donations-node.ts @@ -1,10 +1,13 @@ -import { getDriver } from '../../db/neo4j' +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + import { v4 as uuid } from 'uuid' +import { getDriver } from '@db/neo4j' + export const description = 'This migration adds a Donations node with default settings to the database.' -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -26,7 +29,6 @@ export async function up(next) { { donationId }, ) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -35,11 +37,11 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -52,7 +54,6 @@ export async function down(next) { RETURN donationInfo `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -61,6 +62,6 @@ export async function down(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20210923140939-add-sendNotificationEmails-property-to-all-users.ts b/backend/src/db/migrations/20210923140939-add-sendNotificationEmails-property-to-all-users.ts index 0d1f4fb91..5bc6aed17 100644 --- a/backend/src/db/migrations/20210923140939-add-sendNotificationEmails-property-to-all-users.ts +++ b/backend/src/db/migrations/20210923140939-add-sendNotificationEmails-property-to-all-users.ts @@ -1,8 +1,10 @@ -import { getDriver } from '../../db/neo4j' +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + +import { getDriver } from '@db/neo4j' export const description = '' -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -17,7 +19,6 @@ export async function up(next) { `, ) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -26,11 +27,11 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -45,7 +46,6 @@ export async function down(next) { `, ) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -54,6 +54,6 @@ export async function down(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20220803060819-create_fulltext_indices_and_unique_keys_for_groups.ts b/backend/src/db/migrations/20220803060819-create_fulltext_indices_and_unique_keys_for_groups.ts index 63e40c72b..f06c10984 100644 --- a/backend/src/db/migrations/20220803060819-create_fulltext_indices_and_unique_keys_for_groups.ts +++ b/backend/src/db/migrations/20220803060819-create_fulltext_indices_and_unique_keys_for_groups.ts @@ -1,11 +1,13 @@ -import { getDriver } from '../../db/neo4j' +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + +import { getDriver } from '@db/neo4j' export const description = ` We introduced a new node label 'Group' and we need two primary keys 'id' and 'slug' for it. Additional we like to have fulltext indices the keys 'name', 'slug', 'about', and 'description'. ` -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -18,11 +20,14 @@ export async function up(next) { // await transaction.run(` // CREATE CONSTRAINT ON ( group:Group ) ASSERT group.slug IS UNIQUE // `) + + // We do do this in /src/db/migrate/store.ts + /* await transaction.run(` CALL db.index.fulltext.createNodeIndex("group_fulltext_search",["Group"],["name", "slug", "about", "description"]) `) + */ await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -31,17 +36,19 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() try { // Implement your migration here. + // We do do this in /src/db/migrate/store.ts + /* await transaction.run(` DROP CONSTRAINT ON ( group:Group ) ASSERT group.id IS UNIQUE `) @@ -52,7 +59,7 @@ export async function down(next) { CALL db.index.fulltext.drop("group_fulltext_search") `) await transaction.commit() - next() + */ } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -61,6 +68,6 @@ export async function down(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20230320130345-fulltext-search-indexes.ts b/backend/src/db/migrations/20230320130345-fulltext-search-indexes.ts index 40ebc6c2e..686d221de 100644 --- a/backend/src/db/migrations/20230320130345-fulltext-search-indexes.ts +++ b/backend/src/db/migrations/20230320130345-fulltext-search-indexes.ts @@ -1,13 +1,17 @@ -import { getDriver } from '../../db/neo4j' +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + +import { getDriver } from '@db/neo4j' export const description = '' -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() try { + // We do do this in /src/db/migrate/store.ts + /* // Drop indexes if they exist because due to legacy code they might be set already const indexesResponse = await transaction.run(`CALL db.indexes()`) const indexes = indexesResponse.records.map((record) => record.get('name')) @@ -31,7 +35,7 @@ export async function up(next) { `CALL db.index.fulltext.createNodeIndex("tag_fulltext_search",["Tag"],["id"])`, ) await transaction.commit() - next() + */ } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -40,21 +44,23 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() try { + // We do do this in /src/db/migrate/store.ts + /* await transaction.run(`CALL db.index.fulltext.drop("user_fulltext_search")`) await transaction.run(`CALL db.index.fulltext.drop("post_fulltext_search")`) await transaction.run(`CALL db.index.fulltext.drop("tag_fulltext_search")`) await transaction.commit() - next() + */ } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -63,6 +69,6 @@ export async function down(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20230329150329-article-label-for-posts.ts b/backend/src/db/migrations/20230329150329-article-label-for-posts.ts index 3cf435203..44433e56b 100644 --- a/backend/src/db/migrations/20230329150329-article-label-for-posts.ts +++ b/backend/src/db/migrations/20230329150329-article-label-for-posts.ts @@ -1,8 +1,10 @@ -import { getDriver } from '../../db/neo4j' +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + +import { getDriver } from '@db/neo4j' export const description = 'Add to all existing posts the Article label' -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -14,7 +16,6 @@ export async function up(next) { RETURN post `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -23,11 +24,11 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -39,7 +40,6 @@ export async function down(next) { RETURN post `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -48,6 +48,6 @@ export async function down(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20230608130637-add-postType-property.ts b/backend/src/db/migrations/20230608130637-add-postType-property.ts index 433577715..1e5474064 100644 --- a/backend/src/db/migrations/20230608130637-add-postType-property.ts +++ b/backend/src/db/migrations/20230608130637-add-postType-property.ts @@ -1,8 +1,10 @@ -import { getDriver } from '../../db/neo4j' +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + +import { getDriver } from '@db/neo4j' export const description = 'Add postType property Article to all posts' -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -14,7 +16,6 @@ export async function up(next) { RETURN post `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -23,11 +24,11 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -39,7 +40,6 @@ export async function down(next) { RETURN post `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -48,6 +48,6 @@ export async function down(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20231017141022-fix-event-dates.ts b/backend/src/db/migrations/20231017141022-fix-event-dates.ts index 3c4302f13..259e3ff65 100644 --- a/backend/src/db/migrations/20231017141022-fix-event-dates.ts +++ b/backend/src/db/migrations/20231017141022-fix-event-dates.ts @@ -1,11 +1,15 @@ -import { getDriver } from '../../db/neo4j' +/* eslint-disable @typescript-eslint/no-base-to-string */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ + +import { getDriver } from '@db/neo4j' export const description = ` Transform event start and end date of format 'YYYY-MM-DD HH:MM:SS' in CEST to ISOString in UTC. ` -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -18,11 +22,11 @@ export async function up(next) { `) for (const event of events.records) { let [id, eventStart, eventEnd] = event - let date = new Date(eventStart) + let date = new Date(eventStart as string) date.setHours(date.getHours() - 1) eventStart = date.toISOString() if (eventEnd) { - date = new Date(eventEnd) + date = new Date(eventEnd as string) date.setHours(date.getHours() - 1) eventEnd = date.toISOString() } @@ -34,7 +38,6 @@ export async function up(next) { `) } await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -43,18 +46,17 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() try { // No sense in running this down - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -63,6 +65,6 @@ export async function down(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20250331130323-author-observes-own-post.ts b/backend/src/db/migrations/20250331130323-author-observes-own-post.ts index 7343d5010..df6eebf23 100644 --- a/backend/src/db/migrations/20250331130323-author-observes-own-post.ts +++ b/backend/src/db/migrations/20250331130323-author-observes-own-post.ts @@ -1,10 +1,12 @@ -import { getDriver } from '../../db/neo4j' +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + +import { getDriver } from '@db/neo4j' export const description = ` All authors observe their posts. ` -export async function up(next) { +export async function up(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -21,7 +23,6 @@ export async function up(next) { RETURN post `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -30,11 +31,11 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } -export async function down(next) { +export async function down(_next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() @@ -47,7 +48,6 @@ export async function down(next) { RETURN p `) await transaction.commit() - next() } catch (error) { // eslint-disable-next-line no-console console.log(error) @@ -56,6 +56,6 @@ export async function down(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20250331140313-commenter-observes-post.ts b/backend/src/db/migrations/20250331140313-commenter-observes-post.ts new file mode 100644 index 000000000..ce1d32bc0 --- /dev/null +++ b/backend/src/db/migrations/20250331140313-commenter-observes-post.ts @@ -0,0 +1,62 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + +import { getDriver } from '@db/neo4j' + +export const description = ` +All users commenting a post observe the post. +` + +export async function up(_next) { + const driver = getDriver() + const session = driver.session() + const transaction = session.beginTransaction() + + try { + // Implement your migration here. + await transaction.run(` + MATCH (commenter:User)-[:WROTE]->(:Comment)-[:COMMENTS]->(post:Post) + MERGE (commenter)-[obs:OBSERVES]->(post) + ON CREATE SET + obs.active = true, + obs.createdAt = toString(datetime()), + obs.updatedAt = toString(datetime()) + RETURN post + `) + await transaction.commit() + } catch (error) { + // eslint-disable-next-line no-console + console.log(error) + await transaction.rollback() + // eslint-disable-next-line no-console + console.log('rolled back') + throw new Error(error) + } finally { + await session.close() + } +} + +export async function down(_next) { + const driver = getDriver() + const session = driver.session() + const transaction = session.beginTransaction() + + try { + // Implement your migration here. + await transaction.run(` + MATCH (u:User)-[obs:OBSERVES]->(p:Post)<-[:COMMENTS]-(:Comment)<-[:WROTE]-(u) + WHERE NOT (u)-[:WROTE]->(post) + DELETE obs + RETURN p + `) + await transaction.commit() + } catch (error) { + // eslint-disable-next-line no-console + console.log(error) + await transaction.rollback() + // eslint-disable-next-line no-console + console.log('rolled back') + throw new Error(error) + } finally { + await session.close() + } +} diff --git a/backend/src/db/migrations/20250405030454-email-notification-settings.ts b/backend/src/db/migrations/20250405030454-email-notification-settings.ts new file mode 100644 index 000000000..eaa9a7a6e --- /dev/null +++ b/backend/src/db/migrations/20250405030454-email-notification-settings.ts @@ -0,0 +1,68 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + +import { getDriver } from '@db/neo4j' + +export const description = + 'Transforms the `sendNotificationEmails` property on User to a multi value system' + +export async function up(_next) { + const driver = getDriver() + const session = driver.session() + const transaction = session.beginTransaction() + + try { + // Implement your migration here. + await transaction.run(` + MATCH (user:User) + SET user.emailNotificationsCommentOnObservedPost = user.sendNotificationEmails + SET user.emailNotificationsMention = user.sendNotificationEmails + SET user.emailNotificationsChatMessage = user.sendNotificationEmails + SET user.emailNotificationsGroupMemberJoined = user.sendNotificationEmails + SET user.emailNotificationsGroupMemberLeft = user.sendNotificationEmails + SET user.emailNotificationsGroupMemberRemoved = user.sendNotificationEmails + SET user.emailNotificationsGroupMemberRoleChanged = user.sendNotificationEmails + REMOVE user.sendNotificationEmails + `) + await transaction.commit() + } catch (error) { + // eslint-disable-next-line no-console + console.log(error) + await transaction.rollback() + // eslint-disable-next-line no-console + console.log('rolled back') + throw new Error(error) + } finally { + await session.close() + } +} + +export async function down(_next) { + const driver = getDriver() + const session = driver.session() + const transaction = session.beginTransaction() + + try { + // Implement your migration here. + await transaction.run(` + MATCH (user:User) + SET user.sendNotificationEmails = user.emailNotificationsMention + REMOVE user.emailNotificationsCommentOnObservedPost + REMOVE user.emailNotificationsMention + REMOVE user.emailNotificationsChatMessage + REMOVE user.emailNotificationsGroupMemberJoined + REMOVE user.emailNotificationsGroupMemberLeft + REMOVE user.emailNotificationsGroupMemberRemoved + REMOVE user.emailNotificationsGroupMemberRoleChanged + `) + await transaction.commit() + } catch (error) { + // eslint-disable-next-line no-console + console.log(error) + await transaction.rollback() + // eslint-disable-next-line no-console + console.log('rolled back') + throw new Error(error) + } finally { + await session.close() + } +} diff --git a/backend/src/db/migrations/20250414220436-delete-old-badges.ts b/backend/src/db/migrations/20250414220436-delete-old-badges.ts new file mode 100644 index 000000000..d03e14619 --- /dev/null +++ b/backend/src/db/migrations/20250414220436-delete-old-badges.ts @@ -0,0 +1,51 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + +import { getDriver } from '@db/neo4j' + +export const description = '' + +export async function up(_next) { + const driver = getDriver() + const session = driver.session() + const transaction = session.beginTransaction() + + try { + // Implement your migration here. + await transaction.run(` + MATCH (badge:Badge) + DETACH DELETE badge + `) + await transaction.commit() + } catch (error) { + // eslint-disable-next-line no-console + console.log(error) + await transaction.rollback() + // eslint-disable-next-line no-console + console.log('rolled back') + throw new Error(error) + } finally { + await session.close() + } +} + +export async function down(_next) { + const driver = getDriver() + const session = driver.session() + const transaction = session.beginTransaction() + + try { + // cannot be rolled back + // Implement your migration here. + // await transaction.run(``) + // await transaction.commit() + } catch (error) { + // eslint-disable-next-line no-console + console.log(error) + await transaction.rollback() + // eslint-disable-next-line no-console + console.log('rolled back') + throw new Error(error) + } finally { + await session.close() + } +} diff --git a/backend/src/db/migrations/20250502230521-migrate-to-s3.ts b/backend/src/db/migrations/20250502230521-migrate-to-s3.ts new file mode 100644 index 000000000..243b7f4e9 --- /dev/null +++ b/backend/src/db/migrations/20250502230521-migrate-to-s3.ts @@ -0,0 +1,91 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + +import { open } from 'node:fs/promises' +import path from 'node:path' + +import { S3Client, ObjectCannedACL } from '@aws-sdk/client-s3' +import { Upload } from '@aws-sdk/lib-storage' +import { lookup } from 'mime-types' + +import CONFIG from '@config/index' +import { getDriver } from '@db/neo4j' + +export const description = + 'Upload all image files to a S3 compatible object storage in order to reduce load on our backend.' + +export async function up(_next) { + const { AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_ENDPOINT, AWS_BUCKET } = CONFIG + if (!(AWS_ACCESS_KEY_ID && AWS_SECRET_ACCESS_KEY && AWS_ENDPOINT && AWS_BUCKET)) { + throw new Error('No S3 configuration given, cannot upload image files') + } + + const driver = getDriver() + const session = driver.session() + const transaction = session.beginTransaction() + + const s3 = new S3Client({ + credentials: { + accessKeyId: AWS_ACCESS_KEY_ID, + secretAccessKey: AWS_SECRET_ACCESS_KEY, + }, + endpoint: AWS_ENDPOINT, + forcePathStyle: true, + }) + try { + const { records } = await transaction.run('MATCH (image:Image) RETURN image.url as url') + let urls: string[] = records.map((r) => r.get('url') as string) + urls = urls.filter((url) => url.startsWith('/uploads')) + // eslint-disable-next-line no-console + console.log('URLS uploaded:') + await Promise.all( + urls + .map((url) => async () => { + const { pathname } = new URL(url, 'http://example.org') + // TODO: find a better way to do this - this is quite a hack + const fileLocation = + CONFIG.NODE_ENV === 'production' + ? path.join(__dirname, `../../../../public/${pathname}`) // we're in the /build folder + : path.join(__dirname, `../../../public/${pathname}`) + const s3Location = `original${pathname}` + const mimeType = lookup(fileLocation) + // eslint-disable-next-line security/detect-non-literal-fs-filename + const fileHandle = await open(fileLocation) + const params = { + Bucket: AWS_BUCKET, + Key: s3Location, + ACL: ObjectCannedACL.public_read, + ContentType: mimeType || 'image/jpeg', + Body: fileHandle.createReadStream(), + } + const command = new Upload({ client: s3, params }) + + const data = await command.done() + const { Location: spacesUrl } = data + + const updatedRecord = await transaction.run( + 'MATCH (image:Image {url: $url}) SET image.url = $spacesUrl RETURN image.url as url', + { url, spacesUrl }, + ) + const [updatedUrl] = updatedRecord.records.map((record) => record.get('url') as string) + // eslint-disable-next-line no-console + console.log(updatedUrl) + return updatedUrl + }) + .map((p) => p()), + ) + await transaction.commit() + } catch (error) { + // eslint-disable-next-line no-console + console.log(error) + await transaction.rollback() + // eslint-disable-next-line no-console + console.log('rolled back') + throw new Error(error) + } finally { + await session.close() + } +} + +export function down(_next) { + throw new Error('This migration is irreversible: The backend does not have disk access anymore.') +} diff --git a/backend/src/db/migrations/20250530140506-post-sort-date.ts b/backend/src/db/migrations/20250530140506-post-sort-date.ts new file mode 100644 index 000000000..a0806070d --- /dev/null +++ b/backend/src/db/migrations/20250530140506-post-sort-date.ts @@ -0,0 +1,53 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + +import { getDriver } from '@db/neo4j' + +export const description = '' + +export async function up(_next) { + const driver = getDriver() + const session = driver.session() + const transaction = session.beginTransaction() + + try { + // Implement your migration here. + await transaction.run(` + MATCH (p:Post) + SET p.sortDate = p.createdAt + `) + await transaction.commit() + } catch (error) { + // eslint-disable-next-line no-console + console.log(error) + await transaction.rollback() + // eslint-disable-next-line no-console + console.log('rolled back') + throw new Error(error) + } finally { + await session.close() + } +} + +export async function down(_next) { + const driver = getDriver() + const session = driver.session() + const transaction = session.beginTransaction() + + try { + // Implement your migration here. + await transaction.run(` + MATCH (p:Post) + REMOVE p.sortDate + `) + await transaction.commit() + } catch (error) { + // eslint-disable-next-line no-console + console.log(error) + await transaction.rollback() + // eslint-disable-next-line no-console + console.log('rolled back') + throw new Error(error) + } finally { + await session.close() + } +} diff --git a/backend/src/models/Badge.ts b/backend/src/db/models/Badge.ts similarity index 62% rename from backend/src/models/Badge.ts rename to backend/src/db/models/Badge.ts index 9c4831041..e8d61cb42 100644 --- a/backend/src/models/Badge.ts +++ b/backend/src/db/models/Badge.ts @@ -1,7 +1,7 @@ export default { id: { type: 'string', primary: true, lowercase: true }, - status: { type: 'string', valid: ['permanent', 'temporary'] }, - type: { type: 'string', valid: ['role', 'crowdfunding'] }, + type: { type: 'string', valid: ['verification', 'trophy'] }, icon: { type: 'string', required: true }, + description: { type: 'string', required: true }, createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, } diff --git a/backend/src/models/Category.ts b/backend/src/db/models/Category.ts similarity index 100% rename from backend/src/models/Category.ts rename to backend/src/db/models/Category.ts diff --git a/backend/src/models/Comment.ts b/backend/src/db/models/Comment.ts similarity index 100% rename from backend/src/models/Comment.ts rename to backend/src/db/models/Comment.ts diff --git a/backend/src/models/Donations.ts b/backend/src/db/models/Donations.ts similarity index 100% rename from backend/src/models/Donations.ts rename to backend/src/db/models/Donations.ts diff --git a/backend/src/models/EmailAddress.ts b/backend/src/db/models/EmailAddress.ts similarity index 100% rename from backend/src/models/EmailAddress.ts rename to backend/src/db/models/EmailAddress.ts diff --git a/backend/src/models/Group.ts b/backend/src/db/models/Group.ts similarity index 100% rename from backend/src/models/Group.ts rename to backend/src/db/models/Group.ts diff --git a/backend/src/models/Image.ts b/backend/src/db/models/Image.ts similarity index 100% rename from backend/src/models/Image.ts rename to backend/src/db/models/Image.ts diff --git a/backend/src/models/InviteCode.ts b/backend/src/db/models/InviteCode.ts similarity index 78% rename from backend/src/models/InviteCode.ts rename to backend/src/db/models/InviteCode.ts index 7204f1b38..0617529ac 100644 --- a/backend/src/models/InviteCode.ts +++ b/backend/src/db/models/InviteCode.ts @@ -14,4 +14,10 @@ export default { target: 'User', direction: 'in', }, + invitesTo: { + type: 'relationship', + relationship: 'INVITES_TO', + target: 'Group', + direction: 'out', + }, } diff --git a/backend/src/models/Location.ts b/backend/src/db/models/Location.ts similarity index 100% rename from backend/src/models/Location.ts rename to backend/src/db/models/Location.ts diff --git a/backend/src/models/Migration.ts b/backend/src/db/models/Migration.ts similarity index 100% rename from backend/src/models/Migration.ts rename to backend/src/db/models/Migration.ts diff --git a/backend/src/models/Post.ts b/backend/src/db/models/Post.ts similarity index 81% rename from backend/src/models/Post.ts rename to backend/src/db/models/Post.ts index e206ea1f5..6697faa30 100644 --- a/backend/src/models/Post.ts +++ b/backend/src/db/models/Post.ts @@ -45,6 +45,7 @@ export default { required: true, default: () => new Date().toISOString(), }, + sortDate: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, language: { type: 'string', allow: [null] }, comments: { type: 'relationship', @@ -58,4 +59,15 @@ export default { }, pinned: { type: 'boolean', default: null, valid: [null, true] }, postType: { type: 'string', default: 'Article', valid: ['Article', 'Event'] }, + observes: { + type: 'relationship', + relationship: 'OBSERVES', + target: 'User', + direction: 'in', + properties: { + createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + updatedAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + active: { type: 'boolean', default: true }, + }, + }, } diff --git a/backend/src/models/Report.ts b/backend/src/db/models/Report.ts similarity index 100% rename from backend/src/models/Report.ts rename to backend/src/db/models/Report.ts diff --git a/backend/src/models/SocialMedia.ts b/backend/src/db/models/SocialMedia.ts similarity index 100% rename from backend/src/models/SocialMedia.ts rename to backend/src/db/models/SocialMedia.ts diff --git a/backend/src/models/Tag.ts b/backend/src/db/models/Tag.ts similarity index 100% rename from backend/src/models/Tag.ts rename to backend/src/db/models/Tag.ts diff --git a/backend/src/models/UnverifiedEmailAddress.ts b/backend/src/db/models/UnverifiedEmailAddress.ts similarity index 100% rename from backend/src/models/UnverifiedEmailAddress.ts rename to backend/src/db/models/UnverifiedEmailAddress.ts diff --git a/backend/src/models/User.spec.ts b/backend/src/db/models/User.spec.ts similarity index 90% rename from backend/src/models/User.spec.ts rename to backend/src/db/models/User.spec.ts index 17f2fe0a9..cea2d4db0 100644 --- a/backend/src/models/User.spec.ts +++ b/backend/src/db/models/User.spec.ts @@ -1,5 +1,8 @@ -import { cleanDatabase } from '../db/factories' -import { getNeode, getDriver } from '../db/neo4j' +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ + +import { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' const driver = getDriver() const neode = getNeode() @@ -10,7 +13,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) // TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 diff --git a/backend/src/models/User.ts b/backend/src/db/models/User.ts similarity index 74% rename from backend/src/models/User.ts rename to backend/src/db/models/User.ts index 9b828e27e..77a37c3c1 100644 --- a/backend/src/models/User.ts +++ b/backend/src/db/models/User.ts @@ -52,6 +52,24 @@ export default { target: 'Badge', direction: 'in', }, + selected: { + type: 'relationship', + relationship: 'SELECTED', + target: 'Badge', + direction: 'out', + properties: { + slot: { + type: 'int', + required: true, + }, + }, + }, + verifies: { + type: 'relationship', + relationship: 'VERIFIES', + target: 'Badge', + direction: 'in', + }, invitedBy: { type: 'relationship', relationship: 'INVITED', target: 'User', direction: 'in' }, lastActiveAt: { type: 'string', isoDate: true }, lastOnlineStatus: { type: 'string' }, @@ -155,12 +173,58 @@ export default { type: 'boolean', default: false, }, - sendNotificationEmails: { + + // emailNotifications + emailNotificationsCommentOnObservedPost: { type: 'boolean', default: true, }, + emailNotificationsMention: { + type: 'boolean', + default: true, + }, + emailNotificationsChatMessage: { + type: 'boolean', + default: true, + }, + emailNotificationsGroupMemberJoined: { + type: 'boolean', + default: true, + }, + emailNotificationsGroupMemberLeft: { + type: 'boolean', + default: true, + }, + emailNotificationsGroupMemberRemoved: { + type: 'boolean', + default: true, + }, + emailNotificationsGroupMemberRoleChanged: { + type: 'boolean', + default: true, + }, + emailNotificationsFollowingUsers: { + type: 'boolean', + default: true, + }, + emailNotificationsPostInGroup: { + type: 'boolean', + default: true, + }, + locale: { type: 'string', allow: [null], }, + observes: { + type: 'relationship', + relationship: 'OBSERVES', + target: 'Post', + direction: 'out', + properties: { + createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + updatedAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + active: { type: 'boolean', default: true }, + }, + }, } diff --git a/backend/src/models/index.ts b/backend/src/db/models/index.ts similarity index 85% rename from backend/src/models/index.ts rename to backend/src/db/models/index.ts index f7d338684..6bbdab338 100644 --- a/backend/src/models/index.ts +++ b/backend/src/db/models/index.ts @@ -1,5 +1,11 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable n/no-missing-require */ +/* eslint-disable n/global-require */ // NOTE: We cannot use `fs` here to clean up the code. Cypress breaks on any npm // module that is not browser-compatible. Node's `fs` module is server-side only +// eslint-disable-next-line @typescript-eslint/no-explicit-any declare let Cypress: any | undefined export default { Image: typeof Cypress !== 'undefined' ? require('./Image') : require('./Image').default, diff --git a/backend/src/db/neo4j.ts b/backend/src/db/neo4j.ts index 78b52237e..ecaef68b5 100644 --- a/backend/src/db/neo4j.ts +++ b/backend/src/db/neo4j.ts @@ -1,9 +1,11 @@ -import neo4j from 'neo4j-driver' -import CONFIG from './../config' +/* eslint-disable import/no-named-as-default-member */ +import neo4j, { Driver } from 'neo4j-driver' import Neode from 'neode' -import models from '../models' -let driver +import CONFIG from '@config/index' +import models from '@db/models/index' + +let driver: Driver const defaultOptions = { uri: CONFIG.NEO4J_URI, username: CONFIG.NEO4J_USERNAME, @@ -18,7 +20,7 @@ export function getDriver(options = {}) { return driver } -let neodeInstance +let neodeInstance: Neode export function getNeode(options = {}) { if (!neodeInstance) { const { uri, username, password } = { ...defaultOptions, ...options } diff --git a/backend/src/db/reset-with-migrations.ts b/backend/src/db/reset-with-migrations.ts new file mode 100644 index 000000000..78db831ce --- /dev/null +++ b/backend/src/db/reset-with-migrations.ts @@ -0,0 +1,22 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-floating-promises */ +/* eslint-disable n/no-process-exit */ +import CONFIG from '@config/index' + +import { cleanDatabase } from './factories' + +if (CONFIG.PRODUCTION && !CONFIG.PRODUCTION_DB_CLEAN_ALLOW) { + throw new Error(`You cannot clean the database in a non-staging and real production environment!`) +} + +;(async function () { + try { + await cleanDatabase({ withMigrations: true }) + console.log('Successfully deleted all nodes and relations including!') // eslint-disable-line no-console + process.exit(0) + // eslint-disable-next-line no-catch-all/no-catch-all + } catch (err) { + console.log(`Error occurred deleting the nodes and relations (reset the db)\n\n${err}`) // eslint-disable-line no-console + process.exit(1) + } +})() diff --git a/backend/src/db/clean.ts b/backend/src/db/reset.ts similarity index 62% rename from backend/src/db/clean.ts rename to backend/src/db/reset.ts index eac26036c..a381799c6 100644 --- a/backend/src/db/clean.ts +++ b/backend/src/db/reset.ts @@ -1,5 +1,9 @@ -import CONFIG from '../config' -import { cleanDatabase } from '../db/factories' +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-floating-promises */ +/* eslint-disable n/no-process-exit */ +import CONFIG from '@config/index' + +import { cleanDatabase } from './factories' if (CONFIG.PRODUCTION && !CONFIG.PRODUCTION_DB_CLEAN_ALLOW) { throw new Error(`You cannot clean the database in a non-staging and real production environment!`) @@ -10,6 +14,7 @@ if (CONFIG.PRODUCTION && !CONFIG.PRODUCTION_DB_CLEAN_ALLOW) { await cleanDatabase() console.log('Successfully deleted all nodes and relations!') // eslint-disable-line no-console process.exit(0) + // eslint-disable-next-line no-catch-all/no-catch-all } catch (err) { console.log(`Error occurred deleting the nodes and relations (reset the db)\n\n${err}`) // eslint-disable-line no-console process.exit(1) diff --git a/backend/src/db/seed.ts b/backend/src/db/seed.ts index bff236f64..e7f5b23c5 100644 --- a/backend/src/db/seed.ts +++ b/backend/src/db/seed.ts @@ -1,28 +1,35 @@ -import sample from 'lodash/sample' -import { createTestClient } from 'apollo-server-testing' -import CONFIG from '../config' -import createServer from '../server' +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-floating-promises */ +/* eslint-disable n/no-process-exit */ import { faker } from '@faker-js/faker' -import Factory from '../db/factories' -import { getNeode, getDriver } from '../db/neo4j' -import { - createGroupMutation, - joinGroupMutation, - changeGroupMemberRoleMutation, -} from '../graphql/groups' -import { createPostMutation } from '../graphql/posts' -import { createRoomMutation } from '../graphql/rooms' -import { createMessageMutation } from '../graphql/messages' -import { createCommentMutation } from '../graphql/comments' -import { categories } from '../constants/categories' +import { createTestClient } from 'apollo-server-testing' +import sample from 'lodash/sample' + +import CONFIG from '@config/index' +import { categories } from '@constants/categories' +import { changeGroupMemberRoleMutation } from '@graphql/queries/changeGroupMemberRoleMutation' +import { createCommentMutation } from '@graphql/queries/createCommentMutation' +import { createGroupMutation } from '@graphql/queries/createGroupMutation' +import { createMessageMutation } from '@graphql/queries/createMessageMutation' +import { createPostMutation } from '@graphql/queries/createPostMutation' +import { createRoomMutation } from '@graphql/queries/createRoomMutation' +import { joinGroupMutation } from '@graphql/queries/joinGroupMutation' +import createServer from '@src/server' + +import Factory from './factories' +import { getNeode, getDriver } from './neo4j' +import { trophies, verification } from './seed/badges' if (CONFIG.PRODUCTION && !CONFIG.PRODUCTION_DB_CLEAN_ALLOW) { throw new Error(`You cannot seed the database in a non-staging and real production environment!`) } +CONFIG.SEND_MAIL = true + const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] -/* eslint-disable no-multi-spaces */ ;(async function () { let authenticatedUser = null const driver = getDriver() @@ -121,32 +128,28 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] await Hamburg.relateTo(Germany, 'isIn') await Paris.relateTo(France, 'isIn') - // badges - const racoon = await Factory.build('badge', { - id: 'indiegogo_en_racoon', - icon: '/img/badges/indiegogo_en_racoon.svg', - }) - const rabbit = await Factory.build('badge', { - id: 'indiegogo_en_rabbit', - icon: '/img/badges/indiegogo_en_rabbit.svg', - }) - const wolf = await Factory.build('badge', { - id: 'indiegogo_en_wolf', - icon: '/img/badges/indiegogo_en_wolf.svg', - }) - const bear = await Factory.build('badge', { - id: 'indiegogo_en_bear', - icon: '/img/badges/indiegogo_en_bear.svg', - }) - const turtle = await Factory.build('badge', { - id: 'indiegogo_en_turtle', - icon: '/img/badges/indiegogo_en_turtle.svg', - }) - const rhino = await Factory.build('badge', { - id: 'indiegogo_en_rhino', - icon: '/img/badges/indiegogo_en_rhino.svg', - }) + const { + trophyAirship, + trophyBee, + trophyStarter, + trophyFlower, + trophyPanda, + trophyTiger, + trophyAlienship, + trophyBalloon, + trophyMagicrainbow, + trophySuperfounder, + trophyBigballoon, + trophyLifetree, + trophyRacoon, + trophyRhino, + trophyWolf, + trophyTurtle, + trophyBear, + trophyRabbit, + } = await trophies() + const { verificationAdmin, verificationModerator, verificationDeveloper } = await verification() // users const peterLustig = await Factory.build( 'user', @@ -240,14 +243,50 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] await jennyRostock.relateTo(Paris, 'isIn') await huey.relateTo(Paris, 'isIn') - await peterLustig.relateTo(racoon, 'rewarded') - await peterLustig.relateTo(rhino, 'rewarded') - await peterLustig.relateTo(wolf, 'rewarded') - await bobDerBaumeister.relateTo(racoon, 'rewarded') - await bobDerBaumeister.relateTo(turtle, 'rewarded') - await jennyRostock.relateTo(bear, 'rewarded') - await dagobert.relateTo(rabbit, 'rewarded') + // badges + await peterLustig.relateTo(trophyRacoon, 'rewarded') + await peterLustig.relateTo(trophyRhino, 'rewarded') + await peterLustig.relateTo(trophyWolf, 'rewarded') + await peterLustig.relateTo(trophyAirship, 'rewarded') + await peterLustig.relateTo(verificationAdmin, 'verifies') + await peterLustig.relateTo(trophyRacoon, 'selected', { slot: 0 }) + await peterLustig.relateTo(trophyRhino, 'selected', { slot: 1 }) + await peterLustig.relateTo(trophyAirship, 'selected', { slot: 5 }) + await bobDerBaumeister.relateTo(trophyRacoon, 'rewarded') + await bobDerBaumeister.relateTo(trophyTurtle, 'rewarded') + await bobDerBaumeister.relateTo(trophyBee, 'rewarded') + await bobDerBaumeister.relateTo(verificationModerator, 'verifies') + await bobDerBaumeister.relateTo(trophyRacoon, 'selected', { slot: 1 }) + await bobDerBaumeister.relateTo(trophyTurtle, 'selected', { slot: 2 }) + + await jennyRostock.relateTo(trophyBear, 'rewarded') + await jennyRostock.relateTo(trophyStarter, 'rewarded') + await jennyRostock.relateTo(trophyFlower, 'rewarded') + await jennyRostock.relateTo(trophyBear, 'selected', { slot: 0 }) + await jennyRostock.relateTo(trophyStarter, 'selected', { slot: 1 }) + await jennyRostock.relateTo(trophyFlower, 'selected', { slot: 2 }) + + await huey.relateTo(trophyPanda, 'rewarded') + await huey.relateTo(trophyTiger, 'rewarded') + await huey.relateTo(trophyAlienship, 'rewarded') + await huey.relateTo(trophyBalloon, 'rewarded') + await huey.relateTo(trophyMagicrainbow, 'rewarded') + await huey.relateTo(trophySuperfounder, 'rewarded') + await huey.relateTo(verificationDeveloper, 'verifies') + await huey.relateTo(trophyPanda, 'selected', { slot: 0 }) + await huey.relateTo(trophyTiger, 'selected', { slot: 1 }) + await huey.relateTo(trophyAlienship, 'selected', { slot: 2 }) + + await dewey.relateTo(trophyBigballoon, 'rewarded') + await dewey.relateTo(trophyLifetree, 'rewarded') + await dewey.relateTo(trophyBigballoon, 'selected', { slot: 7 }) + await dewey.relateTo(trophyLifetree, 'selected', { slot: 8 }) + + await louie.relateTo(trophyRabbit, 'rewarded') + await louie.relateTo(trophyRabbit, 'selected', { slot: 4 }) + + // Friends await peterLustig.relateTo(bobDerBaumeister, 'friends') await peterLustig.relateTo(jennyRostock, 'friends') await bobDerBaumeister.relateTo(jennyRostock, 'friends') @@ -269,12 +308,11 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] await dagobert.relateTo(louie, 'blocked') // categories - let i = 0 for (const category of categories) { await Factory.build('category', { - id: `cat${i++}`, - slug: category.name, - naem: category.name, + id: category.id, + slug: category.slug, + name: category.name, icon: category.icon, }) } @@ -632,9 +670,9 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] categoryIds: ['cat16'], author: peterLustig, image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'food', width: 300, height: 169 }), + width: 300, + height: 169, sensitive: true, - aspectRatio: 300 / 169, }), }, ) @@ -648,8 +686,8 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] categoryIds: ['cat1'], author: bobDerBaumeister, image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'technics', width: 300, height: 1500 }), - aspectRatio: 300 / 1500, + width: 300, + height: 1500, }), }, ) @@ -696,8 +734,8 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] categoryIds: ['cat6'], author: peterLustig, image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'city', width: 300, height: 857 }), - aspectRatio: 300 / 857, + width: 300, + height: 857, }), }, ) @@ -735,8 +773,8 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] categoryIds: ['cat11'], author: louie, image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'people', width: 300, height: 901 }), - aspectRatio: 300 / 901, + width: 300, + height: 901, }), }, ) @@ -761,8 +799,8 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] categoryIds: ['cat14'], author: jennyRostock, image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'abstract', width: 300, height: 200 }), - aspectRatio: 300 / 450, + width: 300, + height: 200, }), }, ) @@ -821,7 +859,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] mutation: createPostMutation(), variables: { id: 'p8', - image: faker.image.urlLoremFlickr({ category: 'nature' }), title: `Quantum Flow Theory explains Quantum Gravity`, content: hashtagAndMention1, categoryIds: ['cat8'], @@ -875,6 +912,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] authenticatedUser = null + // eslint-disable-next-line @typescript-eslint/no-explicit-any const comments: any[] = [] comments.push( await Factory.build( @@ -1049,6 +1087,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] await huey.relateTo(p9, 'shouted') await louie.relateTo(p10, 'shouted') + // eslint-disable-next-line @typescript-eslint/no-explicit-any const reports: any[] = [] reports.push( await Factory.build('report'), @@ -1156,6 +1195,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] closed: true, }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any const additionalUsers: any[] = [] for (let i = 0; i < 30; i++) { const user = await Factory.build('user') @@ -1177,9 +1217,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] { categoryIds: ['cat1'], author: jennyRostock, - image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'abstract' }), - }), }, ) } @@ -1228,9 +1265,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] { categoryIds: ['cat1'], author: peterLustig, - image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'city' }), - }), }, ) } @@ -1279,9 +1313,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] { categoryIds: ['cat1'], author: dewey, - image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'food' }), - }), }, ) } @@ -1330,9 +1361,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] { categoryIds: ['cat1'], author: louie, - image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'technics' }), - }), }, ) } @@ -1381,9 +1409,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] { categoryIds: ['cat1'], author: bobDerBaumeister, - image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'people' }), - }), }, ) } @@ -1432,9 +1457,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] { categoryIds: ['cat1'], author: huey, - image: Factory.build('image', { - url: faker.image.urlLoremFlickr({ category: 'nature' }), - }), }, ) } @@ -1563,12 +1585,12 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] /* eslint-disable-next-line no-console */ console.log('Seeded Data...') await driver.close() - await neode.close() + neode.close() process.exit(0) + // eslint-disable-next-line no-catch-all/no-catch-all } catch (err) { /* eslint-disable-next-line no-console */ console.error(err) process.exit(1) } })() -/* eslint-enable no-multi-spaces */ diff --git a/backend/src/db/seed/badges.ts b/backend/src/db/seed/badges.ts new file mode 100644 index 000000000..dc044419c --- /dev/null +++ b/backend/src/db/seed/badges.ts @@ -0,0 +1,188 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import Factory from '@db/factories' + +export const trophies = async () => { + return { + // Blue Animals + trophyBear: await Factory.build('badge', { + id: 'trophy_bear', + type: 'trophy', + description: 'Has earned a Bear', + icon: '/img/badges/trophy_blue_bear.svg', + }), + trophyPanda: await Factory.build('badge', { + id: 'trophy_panda', + type: 'trophy', + description: 'Has earned a Panda', + icon: '/img/badges/trophy_blue_panda.svg', + }), + trophyRabbit: await Factory.build('badge', { + id: 'trophy_rabbit', + type: 'trophy', + description: 'Has earned a Rabbit', + icon: '/img/badges/trophy_blue_rabbit.svg', + }), + trophyRacoon: await Factory.build('badge', { + id: 'trophy_racoon', + type: 'trophy', + description: 'Has earned a Racoon', + icon: '/img/badges/trophy_blue_racoon.svg', + }), + trophyRhino: await Factory.build('badge', { + id: 'trophy_rhino', + type: 'trophy', + description: 'Has earned a Rhino', + icon: '/img/badges/trophy_blue_rhino.svg', + }), + trophyTiger: await Factory.build('badge', { + id: 'trophy_tiger', + type: 'trophy', + description: 'Has earned a Tiger', + icon: '/img/badges/trophy_blue_tiger.svg', + }), + trophyTurtle: await Factory.build('badge', { + id: 'trophy_turtle', + type: 'trophy', + description: 'Has earned a Turtle', + icon: '/img/badges/trophy_blue_turtle.svg', + }), + trophyWhale: await Factory.build('badge', { + id: 'trophy_whale', + type: 'trophy', + description: 'Has earned a Whale', + icon: '/img/badges/trophy_blue_whale.svg', + }), + trophyWolf: await Factory.build('badge', { + id: 'trophy_wolf', + type: 'trophy', + description: 'Has earned a Wolf', + icon: '/img/badges/trophy_blue_wolf.svg', + }), + // Green Transports + trophyAirship: await Factory.build('badge', { + id: 'trophy_airship', + type: 'trophy', + description: 'Has earned an Airship', + icon: '/img/badges/trophy_green_airship.svg', + }), + trophyAlienship: await Factory.build('badge', { + id: 'trophy_alienship', + type: 'trophy', + description: 'Has earned an Alienship', + icon: '/img/badges/trophy_green_alienship.svg', + }), + trophyBalloon: await Factory.build('badge', { + id: 'trophy_balloon', + type: 'trophy', + description: 'Has earned a Balloon', + icon: '/img/badges/trophy_green_balloon.svg', + }), + trophyBigballoon: await Factory.build('badge', { + id: 'trophy_bigballoon', + type: 'trophy', + description: 'Has earned a Big Balloon', + icon: '/img/badges/trophy_green_bigballoon.svg', + }), + trophyCrane: await Factory.build('badge', { + id: 'trophy_crane', + type: 'trophy', + description: 'Has earned a Crane', + icon: '/img/badges/trophy_green_crane.svg', + }), + trophyGlider: await Factory.build('badge', { + id: 'trophy_glider', + type: 'trophy', + description: 'Has earned a Glider', + icon: '/img/badges/trophy_green_glider.svg', + }), + trophyHelicopter: await Factory.build('badge', { + id: 'trophy_helicopter', + type: 'trophy', + description: 'Has earned a Helicopter', + icon: '/img/badges/trophy_green_helicopter.svg', + }), + // Green Animals + trophyBee: await Factory.build('badge', { + id: 'trophy_bee', + type: 'trophy', + description: 'Has earned a Bee', + icon: '/img/badges/trophy_green_bee.svg', + }), + trophyButterfly: await Factory.build('badge', { + id: 'trophy_butterfly', + type: 'trophy', + description: 'Has earned a Butterfly', + icon: '/img/badges/trophy_green_butterfly.svg', + }), + // Green Plants + trophyFlower: await Factory.build('badge', { + id: 'trophy_flower', + type: 'trophy', + description: 'Has earned a Flower', + icon: '/img/badges/trophy_green_flower.svg', + }), + trophyLifetree: await Factory.build('badge', { + id: 'trophy_lifetree', + type: 'trophy', + description: 'Has earned the tree of life', + icon: '/img/badges/trophy_green_lifetree.svg', + }), + // Green Misc + trophyDoublerainbow: await Factory.build('badge', { + id: 'trophy_doublerainbow', + type: 'trophy', + description: 'Has earned the Double Rainbow', + icon: '/img/badges/trophy_green_doublerainbow.svg', + }), + trophyEndrainbow: await Factory.build('badge', { + id: 'trophy_endrainbow', + type: 'trophy', + description: 'Has earned the End of the Rainbow', + icon: '/img/badges/trophy_green_endrainbow.svg', + }), + trophyMagicrainbow: await Factory.build('badge', { + id: 'trophy_magicrainbow', + type: 'trophy', + description: 'Has earned the Magic Rainbow', + icon: '/img/badges/trophy_green_magicrainbow.svg', + }), + trophyStarter: await Factory.build('badge', { + id: 'trophy_starter', + type: 'trophy', + description: 'Has earned the Starter Badge', + icon: '/img/badges/trophy_green_starter.svg', + }), + trophySuperfounder: await Factory.build('badge', { + id: 'trophy_superfounder', + type: 'trophy', + description: 'Has earned the Super Founder Badge', + icon: '/img/badges/trophy_green_superfounder.svg', + }), + } +} + +export const verification = async () => { + return { + // Red Role + verificationModerator: await Factory.build('badge', { + id: 'verification_moderator', + type: 'verification', + description: 'Is a Moderator', + icon: '/img/badges/verification_red_moderator.svg', + }), + verificationAdmin: await Factory.build('badge', { + id: 'verification_admin', + type: 'verification', + description: 'Is an Administrator', + icon: '/img/badges/verification_red_admin.svg', + }), + verificationDeveloper: await Factory.build('badge', { + id: 'verification_developer', + type: 'verification', + description: 'Is a Developer', + icon: '/img/badges/verification_red_developer.svg', + }), + } +} diff --git a/backend/src/db/types/User.ts b/backend/src/db/types/User.ts new file mode 100644 index 000000000..5f621868b --- /dev/null +++ b/backend/src/db/types/User.ts @@ -0,0 +1,32 @@ +import { Integer, Node } from 'neo4j-driver' + +export interface UserDbProperties { + allowEmbedIframes: boolean + awaySince?: string + createdAt: string + deleted: boolean + disabled: boolean + emailNotificationsChatMessage?: boolean + emailNotificationsCommentOnObservedPost?: boolean + emailNotificationsFollowingUsers?: boolean + emailNotificationsGroupMemberJoined?: boolean + emailNotificationsGroupMemberLeft?: boolean + emailNotificationsGroupMemberRemoved?: boolean + emailNotificationsGroupMemberRoleChanged?: boolean + emailNotificationsMention?: boolean + emailNotificationsPostInGroup?: boolean + encryptedPassword: string + id: string + lastActiveAt?: string + lastOnlineStatus?: string + locale: string + name: string + role: string + showShoutsPublicly: boolean + slug: string + termsAndConditionsAgreedAt: string + termsAndConditionsAgreedVersion: string + updatedAt: string +} + +export type User = Node diff --git a/backend/src/emails/__snapshots__/sendChatMessageMail.spec.ts.snap b/backend/src/emails/__snapshots__/sendChatMessageMail.spec.ts.snap new file mode 100644 index 000000000..86e2c22ec --- /dev/null +++ b/backend/src/emails/__snapshots__/sendChatMessageMail.spec.ts.snap @@ -0,0 +1,267 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`sendChatMessageMail English chat_message template 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Hello chatReceiver,

+
+
+

you have received a new chat message from chatSender. +

Show Chat +
+

See you soon on ocelot.social!

+

– The ocelot.social Team


+

PS: If you don't want to receive e-mails anymore, change your notification settings!

+
+
+
+

If you have questions or problems, feel free to contact our support: devops@ocelot.social

+
+ +
+ +", + "subject": "ocelot.social – Notification: New chat message", + "text": "HELLO CHATRECEIVER, + +you have received a new chat message from chatSender +[http://webapp:3000/profile/chatSender/chatsender]. + +Show Chat [http://webapp:3000/chat] + +See you soon on ocelot.social [https://ocelot.social]! + +– The ocelot.social Team + + +PS: If you don't want to receive e-mails anymore, change your notification +settings [http://webapp:3000/settings/notifications]! + +If you have questions or problems, feel free to contact our support: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendChatMessageMail German chat_message template 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Hallo chatReceiver,

+
+
+

du hast eine neue Chat-Nachricht von chatSender erhalten. +

Chat anzeigen +
+

Bis bald bei ocelot.social!

+

– Dein ocelot.social Team


+

PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine Benachrichtigungseinstellungen!

+
+
+
+

Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: devops@ocelot.social

+
+ +
+ +", + "subject": "ocelot.social – Benachrichtigung: Neue Chat Nachricht", + "text": "HALLO CHATRECEIVER, + +du hast eine neue Chat-Nachricht von chatSender +[http://webapp:3000/profile/chatSender/chatsender] erhalten. + +Chat anzeigen [http://webapp:3000/chat] + +Bis bald bei ocelot.social [https://ocelot.social]! + +– Dein ocelot.social Team + + +PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine +Benachrichtigungseinstellungen [http://webapp:3000/settings/notifications]! + +Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; diff --git a/backend/src/emails/__snapshots__/sendEmailVerification.spec.ts.snap b/backend/src/emails/__snapshots__/sendEmailVerification.spec.ts.snap new file mode 100644 index 000000000..63ae9c98f --- /dev/null +++ b/backend/src/emails/__snapshots__/sendEmailVerification.spec.ts.snap @@ -0,0 +1,273 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`sendEmailVerification English renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Hello User,

+
+
+

So, you want to change your e-mail? No problem! Just click the button below to verify your new address:

Verify e-mail address +

If you don't want to change your e-mail address feel free to ignore this message.

+

If the above button doesn't work, you can also copy the following code into your browser window: 123456

+
+

See you soon on ocelot.social!

+

– The ocelot.social Team

+
+
+
+

If you have questions or problems, feel free to contact our support: devops@ocelot.social

+
+ +
+ +", + "subject": "New E-Mail Address ocelot.social", + "text": "HELLO USER, + +So, you want to change your e-mail? No problem! Just click the button below to +verify your new address: + +Verify e-mail address +[http://webapp:3000/settings/my-email-address/verify?email=user%40example.org&nonce=123456] + +If you don't want to change your e-mail address feel free to ignore this +message. + +If the above button doesn't work, you can also copy the following code into your +browser window: 123456 + +See you soon on ocelot.social [https://ocelot.social]! + +– The ocelot.social Team + +If you have questions or problems, feel free to contact our support: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendEmailVerification German renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Hallo User,

+
+
+

Du möchtest also deine E-Mail ändern? Kein Problem! Mit Klick auf diesen Button kannst du deine neue E-Mail Adresse bestätigen:

E-Mail Adresse bestätigen +

Falls du deine E-Mail Adresse doch nicht ändern möchtest, kannst du diese Nachricht einfach ignorieren.

+

Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in dein Browserfenster kopieren: 123456

+
+

Bis bald bei ocelot.social!

+

– Dein ocelot.social Team

+
+
+
+

Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: devops@ocelot.social

+
+ +
+ +", + "subject": "Neue E-Mail Addresse ocelot.social", + "text": "HALLO USER, + +Du möchtest also deine E-Mail ändern? Kein Problem! Mit Klick auf diesen Button +kannst du deine neue E-Mail Adresse bestätigen: + +E-Mail Adresse bestätigen +[http://webapp:3000/settings/my-email-address/verify?email=user%40example.org&nonce=123456] + +Falls du deine E-Mail Adresse doch nicht ändern möchtest, kannst du diese +Nachricht einfach ignorieren. + +Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in +dein Browserfenster kopieren: 123456 + +Bis bald bei ocelot.social [https://ocelot.social]! + +– Dein ocelot.social Team + +Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; diff --git a/backend/src/emails/__snapshots__/sendNotificationMail.spec.ts.snap b/backend/src/emails/__snapshots__/sendNotificationMail.spec.ts.snap new file mode 100644 index 000000000..3770d9bdb --- /dev/null +++ b/backend/src/emails/__snapshots__/sendNotificationMail.spec.ts.snap @@ -0,0 +1,2389 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`sendNotificationMail English changed_group_member_role template 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Hello Jenny Rostock,

+
+
+

your role in the group “The Group” has been changed. Click on the button to view this group:

View group +
+

See you soon on ocelot.social!

+

– The ocelot.social Team


+

PS: If you don't want to receive e-mails anymore, change your notification settings!

+
+
+
+

If you have questions or problems, feel free to contact our support: devops@ocelot.social

+
+ +
+ +", + "subject": "ocelot.social – Notification: Role in group changed", + "text": "HELLO JENNY ROSTOCK, + +your role in the group “The Group” has been changed. Click on the button to view +this group: + +View group [http://webapp:3000/groups/g1/the-group] + +See you soon on ocelot.social [https://ocelot.social]! + +– The ocelot.social Team + + +PS: If you don't want to receive e-mails anymore, change your notification +settings [http://webapp:3000/settings/notifications]! + +If you have questions or problems, feel free to contact our support: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendNotificationMail English commented_on_post template 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Hello Jenny Rostock,

+
+
+

Peter Lustig commented on a post that you are observing with the title “New Post”. Click on the button to view this comment: +

View comment +
+

See you soon on ocelot.social!

+

– The ocelot.social Team


+

PS: If you don't want to receive e-mails anymore, change your notification settings!

+
+
+
+

If you have questions or problems, feel free to contact our support: devops@ocelot.social

+
+ +
+ +", + "subject": "ocelot.social – Notification: New comment on post", + "text": "HELLO JENNY ROSTOCK, + +Peter Lustig [http://webapp:3000/profile/u2/peter-lustig] commented on a post +that you are observing with the title “New Post”. Click on the button to view +this comment: + +View comment [http://webapp:3000/post/p1/new-post#commentId-c1] + +See you soon on ocelot.social [https://ocelot.social]! + +– The ocelot.social Team + + +PS: If you don't want to receive e-mails anymore, change your notification +settings [http://webapp:3000/settings/notifications]! + +If you have questions or problems, feel free to contact our support: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendNotificationMail English followed_user_posted template 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Hello Jenny Rostock,

+
+
+

Peter Lustig, a user you are following, wrote a new post with the title “New Post”. Click on the button to view this post: +

View post +
+

See you soon on ocelot.social!

+

– The ocelot.social Team


+

PS: If you don't want to receive e-mails anymore, change your notification settings!

+
+
+
+

If you have questions or problems, feel free to contact our support: devops@ocelot.social

+
+ +
+ +", + "subject": "ocelot.social – Notification: New post by followd user", + "text": "HELLO JENNY ROSTOCK, + +Peter Lustig [http://webapp:3000/user/u2/peter-lustig], a user you are +following, wrote a new post with the title “New Post”. Click on the button to +view this post: + +View post [http://webapp:3000/post/p1/new-post] + +See you soon on ocelot.social [https://ocelot.social]! + +– The ocelot.social Team + + +PS: If you don't want to receive e-mails anymore, change your notification +settings [http://webapp:3000/settings/notifications]! + +If you have questions or problems, feel free to contact our support: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendNotificationMail English mentioned_in_comment template 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Hello Jenny Rostock,

+
+
+

Peter Lustig mentioned you in a comment to the post with the title “New Post”. Click on the button to view this comment: +

View comment +
+

See you soon on ocelot.social!

+

– The ocelot.social Team


+

PS: If you don't want to receive e-mails anymore, change your notification settings!

+
+
+
+

If you have questions or problems, feel free to contact our support: devops@ocelot.social

+
+ +
+ +", + "subject": "ocelot.social – Notification: Mentioned in comment", + "text": "HELLO JENNY ROSTOCK, + +Peter Lustig [http://webapp:3000/profile/u2/peter-lustig] mentioned you in a +comment to the post with the title “New Post”. Click on the button to view this +comment: + +View comment [http://webapp:3000/post/p1/new-post#commentId-c1] + +See you soon on ocelot.social [https://ocelot.social]! + +– The ocelot.social Team + + +PS: If you don't want to receive e-mails anymore, change your notification +settings [http://webapp:3000/settings/notifications]! + +If you have questions or problems, feel free to contact our support: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendNotificationMail English mentioned_in_post template 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Hello Jenny Rostock,

+
+
+

Peter Lustig mentioned you in a post with the title “New Post”. Click on the button to view this post: +

View post +
+

See you soon on ocelot.social!

+

– The ocelot.social Team


+

PS: If you don't want to receive e-mails anymore, change your notification settings!

+
+
+
+

If you have questions or problems, feel free to contact our support: devops@ocelot.social

+
+ +
+ +", + "subject": "ocelot.social – Notification: Mentioned in post", + "text": "HELLO JENNY ROSTOCK, + +Peter Lustig [http://webapp:3000/user/u2/peter-lustig] mentioned you in a post +with the title “New Post”. Click on the button to view this post: + +View post [http://webapp:3000/post/p1/new-post] + +See you soon on ocelot.social [https://ocelot.social]! + +– The ocelot.social Team + + +PS: If you don't want to receive e-mails anymore, change your notification +settings [http://webapp:3000/settings/notifications]! + +If you have questions or problems, feel free to contact our support: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendNotificationMail English post_in_group template 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Hello Jenny Rostock,

+
+
+

someone wrote a new post with the title “New Post” in one of your groups. Click on the button to view this post:

View post +
+

See you soon on ocelot.social!

+

– The ocelot.social Team


+

PS: If you don't want to receive e-mails anymore, change your notification settings!

+
+
+
+

If you have questions or problems, feel free to contact our support: devops@ocelot.social

+
+ +
+ +", + "subject": "ocelot.social – Notification: New post in group", + "text": "HELLO JENNY ROSTOCK, + +someone wrote a new post with the title “New Post” in one of your groups. Click +on the button to view this post: + +View post [http://webapp:3000/post/p1/new-post] + +See you soon on ocelot.social [https://ocelot.social]! + +– The ocelot.social Team + + +PS: If you don't want to receive e-mails anymore, change your notification +settings [http://webapp:3000/settings/notifications]! + +If you have questions or problems, feel free to contact our support: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendNotificationMail English removed_user_from_group template 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Hello Jenny Rostock,

+
+
+

you have been removed from the group “The Group”.

+
+

See you soon on ocelot.social!

+

– The ocelot.social Team


+

PS: If you don't want to receive e-mails anymore, change your notification settings!

+
+
+
+

If you have questions or problems, feel free to contact our support: devops@ocelot.social

+
+ +
+ +", + "subject": "ocelot.social – Notification: Removed from group", + "text": "HELLO JENNY ROSTOCK, + +you have been removed from the group “The Group”. + +See you soon on ocelot.social [https://ocelot.social]! + +– The ocelot.social Team + + +PS: If you don't want to receive e-mails anymore, change your notification +settings [http://webapp:3000/settings/notifications]! + +If you have questions or problems, feel free to contact our support: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendNotificationMail English user_joined_group template 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Hello Jenny Rostock,

+
+
+

Peter Lustig joined the group “The Group”. Click on the button to view this group: +

View group +
+

See you soon on ocelot.social!

+

– The ocelot.social Team


+

PS: If you don't want to receive e-mails anymore, change your notification settings!

+
+
+
+

If you have questions or problems, feel free to contact our support: devops@ocelot.social

+
+ +
+ +", + "subject": "ocelot.social – Notification: User joined group", + "text": "HELLO JENNY ROSTOCK, + +Peter Lustig [http://webapp:3000/profile/u2/peter-lustig] joined the group “The +Group”. Click on the button to view this group: + +View group [http://webapp:3000/groups/g1/the-group] + +See you soon on ocelot.social [https://ocelot.social]! + +– The ocelot.social Team + + +PS: If you don't want to receive e-mails anymore, change your notification +settings [http://webapp:3000/settings/notifications]! + +If you have questions or problems, feel free to contact our support: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendNotificationMail English user_left_group template 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Hello Jenny Rostock,

+
+
+

Peter Lustig left the group “The Group”. Click on the button to view this group: +

View group +
+

See you soon on ocelot.social!

+

– The ocelot.social Team


+

PS: If you don't want to receive e-mails anymore, change your notification settings!

+
+
+
+

If you have questions or problems, feel free to contact our support: devops@ocelot.social

+
+ +
+ +", + "subject": "ocelot.social – Notification: User left group", + "text": "HELLO JENNY ROSTOCK, + +Peter Lustig [http://webapp:3000/profile/u2/peter-lustig] left the group “The +Group”. Click on the button to view this group: + +View group [http://webapp:3000/groups/g1/the-group] + +See you soon on ocelot.social [https://ocelot.social]! + +– The ocelot.social Team + + +PS: If you don't want to receive e-mails anymore, change your notification +settings [http://webapp:3000/settings/notifications]! + +If you have questions or problems, feel free to contact our support: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendNotificationMail German changed_group_member_role template 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Hallo Jenny Rostock,

+
+
+

deine Rolle in der Gruppe „The Group“ wurde geändert. Klicke auf den Knopf, um diese Gruppe zu sehen:

Gruppe ansehen +
+

Bis bald bei ocelot.social!

+

– Dein ocelot.social Team


+

PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine Benachrichtigungseinstellungen!

+
+
+
+

Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: devops@ocelot.social

+
+ +
+ +", + "subject": "ocelot.social – Benachrichtigung: Rolle in Gruppe geändert", + "text": "HALLO JENNY ROSTOCK, + +deine Rolle in der Gruppe „The Group“ wurde geändert. Klicke auf den Knopf, um +diese Gruppe zu sehen: + +Gruppe ansehen [http://webapp:3000/groups/g1/the-group] + +Bis bald bei ocelot.social [https://ocelot.social]! + +– Dein ocelot.social Team + + +PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine +Benachrichtigungseinstellungen [http://webapp:3000/settings/notifications]! + +Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendNotificationMail German commented_on_post template 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Hallo Jenny Rostock,

+
+
+

Peter Lustig hat einen Beitrag den du beobachtest mit dem Titel „New Post“ kommentiert. Klicke auf den Knopf, um diesen Kommentar zu sehen: +

Kommentar ansehen +
+

Bis bald bei ocelot.social!

+

– Dein ocelot.social Team


+

PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine Benachrichtigungseinstellungen!

+
+
+
+

Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: devops@ocelot.social

+
+ +
+ +", + "subject": "ocelot.social – Benachrichtigung: Neuer Kommentar zu Beitrag", + "text": "HALLO JENNY ROSTOCK, + +Peter Lustig [http://webapp:3000/profile/u2/peter-lustig] hat einen Beitrag den +du beobachtest mit dem Titel „New Post“ kommentiert. Klicke auf den Knopf, um +diesen Kommentar zu sehen: + +Kommentar ansehen [http://webapp:3000/post/p1/new-post#commentId-c1] + +Bis bald bei ocelot.social [https://ocelot.social]! + +– Dein ocelot.social Team + + +PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine +Benachrichtigungseinstellungen [http://webapp:3000/settings/notifications]! + +Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendNotificationMail German followed_user_posted template 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Hallo Jenny Rostock,

+
+
+

Peter Lustig, ein Nutzer dem du folgst, hat einen neuen Beitrag mit dem Titel „New Post“ geschrieben. Klicke auf den Knopf, um diesen Beitrag zu sehen: +

Beitrag ansehen +
+

Bis bald bei ocelot.social!

+

– Dein ocelot.social Team


+

PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine Benachrichtigungseinstellungen!

+
+
+
+

Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: devops@ocelot.social

+
+ +
+ +", + "subject": "ocelot.social – Benachrichtigung: Neuer Beitrag von gefolgtem Nutzer", + "text": "HALLO JENNY ROSTOCK, + +Peter Lustig [http://webapp:3000/user/u2/peter-lustig], ein Nutzer dem du +folgst, hat einen neuen Beitrag mit dem Titel „New Post“ geschrieben. Klicke auf +den Knopf, um diesen Beitrag zu sehen: + +Beitrag ansehen [http://webapp:3000/post/p1/new-post] + +Bis bald bei ocelot.social [https://ocelot.social]! + +– Dein ocelot.social Team + + +PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine +Benachrichtigungseinstellungen [http://webapp:3000/settings/notifications]! + +Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendNotificationMail German mentioned_in_comment template 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Hallo Jenny Rostock,

+
+
+

Peter Lustig hat dich in einem Kommentar zu dem Beitrag mit dem Titel „New Post“ erwähnt. Klicke auf den Knopf, um den Kommentar zu sehen: +

Kommentar ansehen +
+

Bis bald bei ocelot.social!

+

– Dein ocelot.social Team


+

PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine Benachrichtigungseinstellungen!

+
+
+
+

Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: devops@ocelot.social

+
+ +
+ +", + "subject": "ocelot.social – Benachrichtigung: Erwähnung in Kommentar", + "text": "HALLO JENNY ROSTOCK, + +Peter Lustig [http://webapp:3000/profile/u2/peter-lustig] hat dich in einem +Kommentar zu dem Beitrag mit dem Titel „New Post“ erwähnt. Klicke auf den Knopf, +um den Kommentar zu sehen: + +Kommentar ansehen [http://webapp:3000/post/p1/new-post#commentId-c1] + +Bis bald bei ocelot.social [https://ocelot.social]! + +– Dein ocelot.social Team + + +PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine +Benachrichtigungseinstellungen [http://webapp:3000/settings/notifications]! + +Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendNotificationMail German mentioned_in_post template 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Hallo Jenny Rostock,

+
+
+

Peter Lustig hat dich in einem Beitrag mit dem Titel „New Post“ erwähnt. Klicke auf den Knopf, um den Beitrag zu sehen: +

Beitrag ansehen +
+

Bis bald bei ocelot.social!

+

– Dein ocelot.social Team


+

PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine Benachrichtigungseinstellungen!

+
+
+
+

Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: devops@ocelot.social

+
+ +
+ +", + "subject": "ocelot.social – Benachrichtigung: Erwähnung in Beitrag", + "text": "HALLO JENNY ROSTOCK, + +Peter Lustig [http://webapp:3000/user/u2/peter-lustig] hat dich in einem Beitrag +mit dem Titel „New Post“ erwähnt. Klicke auf den Knopf, um den Beitrag zu sehen: + +Beitrag ansehen [http://webapp:3000/post/p1/new-post] + +Bis bald bei ocelot.social [https://ocelot.social]! + +– Dein ocelot.social Team + + +PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine +Benachrichtigungseinstellungen [http://webapp:3000/settings/notifications]! + +Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendNotificationMail German post_in_group template 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Hallo Jenny Rostock,

+
+
+

jemand hat einen neuen Beitrag mit dem Titel „New Post“ in einer deiner Gruppen geschrieben. Klicke auf den Knopf, um diesen Beitrag zu sehen:

Beitrag ansehen +
+

Bis bald bei ocelot.social!

+

– Dein ocelot.social Team


+

PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine Benachrichtigungseinstellungen!

+
+
+
+

Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: devops@ocelot.social

+
+ +
+ +", + "subject": "ocelot.social – Benachrichtigung: Neuer Beitrag in Gruppe", + "text": "HALLO JENNY ROSTOCK, + +jemand hat einen neuen Beitrag mit dem Titel „New Post“ in einer deiner Gruppen +geschrieben. Klicke auf den Knopf, um diesen Beitrag zu sehen: + +Beitrag ansehen [http://webapp:3000/post/p1/new-post] + +Bis bald bei ocelot.social [https://ocelot.social]! + +– Dein ocelot.social Team + + +PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine +Benachrichtigungseinstellungen [http://webapp:3000/settings/notifications]! + +Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendNotificationMail German removed_user_from_group template 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Hallo Jenny Rostock,

+
+
+

du wurdest aus der Gruppe „The Group“ entfernt.

+
+

Bis bald bei ocelot.social!

+

– Dein ocelot.social Team


+

PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine Benachrichtigungseinstellungen!

+
+
+
+

Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: devops@ocelot.social

+
+ +
+ +", + "subject": "ocelot.social – Benachrichtigung: Aus Gruppe entfernt", + "text": "HALLO JENNY ROSTOCK, + +du wurdest aus der Gruppe „The Group“ entfernt. + +Bis bald bei ocelot.social [https://ocelot.social]! + +– Dein ocelot.social Team + + +PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine +Benachrichtigungseinstellungen [http://webapp:3000/settings/notifications]! + +Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendNotificationMail German user_joined_group template 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Hallo Jenny Rostock,

+
+
+

Peter Lustig ist der Gruppe „The Group“ beigetreten. Klicke auf den Knopf, um diese Gruppe zu sehen: +

Gruppe ansehen +
+

Bis bald bei ocelot.social!

+

– Dein ocelot.social Team


+

PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine Benachrichtigungseinstellungen!

+
+
+
+

Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: devops@ocelot.social

+
+ +
+ +", + "subject": "ocelot.social – Benachrichtigung: Nutzer tritt Gruppe bei", + "text": "HALLO JENNY ROSTOCK, + +Peter Lustig [http://webapp:3000/profile/u2/peter-lustig] ist der Gruppe „The +Group“ beigetreten. Klicke auf den Knopf, um diese Gruppe zu sehen: + +Gruppe ansehen [http://webapp:3000/groups/g1/the-group] + +Bis bald bei ocelot.social [https://ocelot.social]! + +– Dein ocelot.social Team + + +PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine +Benachrichtigungseinstellungen [http://webapp:3000/settings/notifications]! + +Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendNotificationMail German user_left_group template 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Hallo Jenny Rostock,

+
+
+

Peter Lustig hat die Gruppe „The Group“ verlassen. Klicke auf den Knopf, um diese Gruppe zu sehen: +

Gruppe ansehen +
+

Bis bald bei ocelot.social!

+

– Dein ocelot.social Team


+

PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine Benachrichtigungseinstellungen!

+
+
+
+

Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: devops@ocelot.social

+
+ +
+ +", + "subject": "ocelot.social – Benachrichtigung: Nutzer verlässt Gruppe", + "text": "HALLO JENNY ROSTOCK, + +Peter Lustig [http://webapp:3000/profile/u2/peter-lustig] hat die Gruppe „The +Group“ verlassen. Klicke auf den Knopf, um diese Gruppe zu sehen: + +Gruppe ansehen [http://webapp:3000/groups/g1/the-group] + +Bis bald bei ocelot.social [https://ocelot.social]! + +– Dein ocelot.social Team + + +PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine +Benachrichtigungseinstellungen [http://webapp:3000/settings/notifications]! + +Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; diff --git a/backend/src/emails/__snapshots__/sendRegistrationMail.spec.ts.snap b/backend/src/emails/__snapshots__/sendRegistrationMail.spec.ts.snap new file mode 100644 index 000000000..562e3b16c --- /dev/null +++ b/backend/src/emails/__snapshots__/sendRegistrationMail.spec.ts.snap @@ -0,0 +1,583 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`sendRegistrationMail with invite code English renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Welcome to ocelot.social!

+
+
+

Thank you for joining our cause – it's awesome to have you on board. There's just one tiny step missing before we can start shaping the world together … Please confirm your e-mail address by clicking the button below:

Confirm your e-mail address +

If the above button doesn't work, you can also copy the following code into your browser window: 123456

+

However, this only works if you have registered through our website.

+

If you didn't sign up for ocelot.social we recommend you to check it out! It's a social network from people for people who want to connect and change the world together. +

+

PS: If you ignore this e-mail we will not create an account for you. ;)

+
+

See you soon on ocelot.social!

+

– The ocelot.social Team

+
+
+
+

If you have questions or problems, feel free to contact our support: devops@ocelot.social

+
+ +
+ +", + "subject": "Welcome to ocelot.social", + "text": "WELCOME TO OCELOT.SOCIAL! + +Thank you for joining our cause – it's awesome to have you on board. There's +just one tiny step missing before we can start shaping the world together … +Please confirm your e-mail address by clicking the button below: + +Confirm your e-mail address +[http://webapp:3000/registration?email=user%40example.org&nonce=123456&inviteCode=welcome&method=invite-code] + +If the above button doesn't work, you can also copy the following code into your +browser window: 123456 + +However, this only works if you have registered through our website. + +If you didn't sign up for ocelot.social we recommend you to check it out! It's a +social network from people for people who want to connect and change the world +together. + +PS: If you ignore this e-mail we will not create an account for you. ;) + +See you soon on ocelot.social [https://ocelot.social]! + +– The ocelot.social Team + +If you have questions or problems, feel free to contact our support: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendRegistrationMail with invite code German renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Willkommen bei ocelot.social!

+
+
+

Danke, dass du dich angemeldet hast – wir freuen uns, dich dabei zu haben. Jetzt fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können … Bitte bestätige deine E-Mail Adresse:

Bestätige deine E-Mail Adresse +

Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in dein Browserfenster kopieren: 123456

+

Das funktioniert allerdings nur, wenn du dich über unsere Website registriert hast.

+

Falls du dich nicht selbst bei ocelot.social angemeldet hast, schau doch mal vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen. +

+

PS: Wenn du keinen Account bei uns möchtest, kannst du diese E-Mail einfach ignorieren. ;)

+
+

Bis bald bei ocelot.social!

+

– Dein ocelot.social Team

+
+
+
+

Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: devops@ocelot.social

+
+ +
+ +", + "subject": "Willkommen bei ocelot.social", + "text": "WILLKOMMEN BEI OCELOT.SOCIAL! + +Danke, dass du dich angemeldet hast – wir freuen uns, dich dabei zu haben. Jetzt +fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können +… Bitte bestätige deine E-Mail Adresse: + +Bestätige deine E-Mail Adresse +[http://webapp:3000/registration?email=user%40example.org&nonce=123456&inviteCode=welcome&method=invite-code] + +Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in +dein Browserfenster kopieren: 123456 + +Das funktioniert allerdings nur, wenn du dich über unsere Website registriert +hast. + +Falls du dich nicht selbst bei ocelot.social angemeldet hast, schau doch mal +vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen. + +PS: Wenn du keinen Account bei uns möchtest, kannst du diese E-Mail einfach +ignorieren. ;) + +Bis bald bei ocelot.social [https://ocelot.social]! + +– Dein ocelot.social Team + +Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendRegistrationMail without invite code English renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Welcome to ocelot.social!

+
+
+

Thank you for joining our cause – it's awesome to have you on board. There's just one tiny step missing before we can start shaping the world together … Please confirm your e-mail address by clicking the button below:

Confirm your e-mail address +

If the above button doesn't work, you can also copy the following code into your browser window: 123456

+

However, this only works if you have registered through our website.

+

If you didn't sign up for ocelot.social we recommend you to check it out! It's a social network from people for people who want to connect and change the world together. +

+

PS: If you ignore this e-mail we will not create an account for you. ;)

+
+

See you soon on ocelot.social!

+

– The ocelot.social Team

+
+
+
+

If you have questions or problems, feel free to contact our support: devops@ocelot.social

+
+ +
+ +", + "subject": "Welcome to ocelot.social", + "text": "WELCOME TO OCELOT.SOCIAL! + +Thank you for joining our cause – it's awesome to have you on board. There's +just one tiny step missing before we can start shaping the world together … +Please confirm your e-mail address by clicking the button below: + +Confirm your e-mail address +[http://webapp:3000/registration?email=user%40example.org&nonce=123456&method=invite-mail] + +If the above button doesn't work, you can also copy the following code into your +browser window: 123456 + +However, this only works if you have registered through our website. + +If you didn't sign up for ocelot.social we recommend you to check it out! It's a +social network from people for people who want to connect and change the world +together. + +PS: If you ignore this e-mail we will not create an account for you. ;) + +See you soon on ocelot.social [https://ocelot.social]! + +– The ocelot.social Team + +If you have questions or problems, feel free to contact our support: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendRegistrationMail without invite code German renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Willkommen bei ocelot.social!

+
+
+

Danke, dass du dich angemeldet hast – wir freuen uns, dich dabei zu haben. Jetzt fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können … Bitte bestätige deine E-Mail Adresse:

Bestätige deine E-Mail Adresse +

Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in dein Browserfenster kopieren: 123456

+

Das funktioniert allerdings nur, wenn du dich über unsere Website registriert hast.

+

Falls du dich nicht selbst bei ocelot.social angemeldet hast, schau doch mal vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen. +

+

PS: Wenn du keinen Account bei uns möchtest, kannst du diese E-Mail einfach ignorieren. ;)

+
+

Bis bald bei ocelot.social!

+

– Dein ocelot.social Team

+
+
+
+

Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: devops@ocelot.social

+
+ +
+ +", + "subject": "Willkommen bei ocelot.social", + "text": "WILLKOMMEN BEI OCELOT.SOCIAL! + +Danke, dass du dich angemeldet hast – wir freuen uns, dich dabei zu haben. Jetzt +fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können +… Bitte bestätige deine E-Mail Adresse: + +Bestätige deine E-Mail Adresse +[http://webapp:3000/registration?email=user%40example.org&nonce=123456&method=invite-mail] + +Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in +dein Browserfenster kopieren: 123456 + +Das funktioniert allerdings nur, wenn du dich über unsere Website registriert +hast. + +Falls du dich nicht selbst bei ocelot.social angemeldet hast, schau doch mal +vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen. + +PS: Wenn du keinen Account bei uns möchtest, kannst du diese E-Mail einfach +ignorieren. ;) + +Bis bald bei ocelot.social [https://ocelot.social]! + +– Dein ocelot.social Team + +Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; diff --git a/backend/src/emails/__snapshots__/sendResetPasswordMail.spec.ts.snap b/backend/src/emails/__snapshots__/sendResetPasswordMail.spec.ts.snap new file mode 100644 index 000000000..d7f10b33d --- /dev/null +++ b/backend/src/emails/__snapshots__/sendResetPasswordMail.spec.ts.snap @@ -0,0 +1,272 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`sendResetPasswordMail English renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Hello Jenny Rostock,

+
+
+

So, you forgot your password? No problem! Just click the button below to reset it within the next 24 hours:

Confirm your e-mail address +

If you didn't request a new password feel free to ignore this e-mail.

+

If the above button doesn't work you can also copy the following code into your browser window: 123456

+
+

See you soon on ocelot.social!

+

– The ocelot.social Team

+
+
+
+

If you have questions or problems, feel free to contact our support: devops@ocelot.social

+
+ +
+ +", + "subject": "Reset Password ocelot.social", + "text": "HELLO JENNY ROSTOCK, + +So, you forgot your password? No problem! Just click the button below to reset +it within the next 24 hours: + +Confirm your e-mail address +[http://webapp:3000/password-reset/change-password?email=user%40example.org&nonce=123456] + +If you didn't request a new password feel free to ignore this e-mail. + +If the above button doesn't work you can also copy the following code into your +browser window: 123456 + +See you soon on ocelot.social [https://ocelot.social]! + +– The ocelot.social Team + +If you have questions or problems, feel free to contact our support: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendResetPasswordMail German renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Hallo Jenny Rostock,

+
+
+

Du hast also dein Passwort vergessen? Kein Problem! Mit Klick auf diesen Button kannst du innerhalb der nächsten 24 Stunden dein Passwort zurücksetzen:

Bestätige deine E-Mail Adresse +

Falls du kein neues Passwort angefordert hast, kannst du diese E-Mail einfach ignorieren.

+

Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in dein Browserfenster kopieren: 123456

+
+

Bis bald bei ocelot.social!

+

– Dein ocelot.social Team

+
+
+
+

Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: devops@ocelot.social

+
+ +
+ +", + "subject": "Neues Passwort ocelot.social", + "text": "HALLO JENNY ROSTOCK, + +Du hast also dein Passwort vergessen? Kein Problem! Mit Klick auf diesen Button +kannst du innerhalb der nächsten 24 Stunden dein Passwort zurücksetzen: + +Bestätige deine E-Mail Adresse +[http://webapp:3000/password-reset/change-password?email=user%40example.org&nonce=123456] + +Falls du kein neues Passwort angefordert hast, kannst du diese E-Mail einfach +ignorieren. + +Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in +dein Browserfenster kopieren: 123456 + +Bis bald bei ocelot.social [https://ocelot.social]! + +– Dein ocelot.social Team + +Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; diff --git a/backend/src/emails/__snapshots__/sendWrongEmail.spec.ts.snap b/backend/src/emails/__snapshots__/sendWrongEmail.spec.ts.snap new file mode 100644 index 000000000..6c2300274 --- /dev/null +++ b/backend/src/emails/__snapshots__/sendWrongEmail.spec.ts.snap @@ -0,0 +1,267 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`sendWrongEmail English renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Welcome to ocelot.social!

+
+
+

You requested a password reset but unfortunately we couldn't find an account associated with your e-mail address. Did you maybe use another one when you signed up?

Try a different e-mail +

If you don't have an account at ocelot.social yet or if you didn't want to reset your password, please ignore this e-mail. +

+
+

See you soon on ocelot.social!

+

– The ocelot.social Team

+
+
+
+

If you have questions or problems, feel free to contact our support: devops@ocelot.social

+
+ +
+ +", + "subject": "Wrong E-mail? ocelot.social", + "text": "WELCOME TO OCELOT.SOCIAL! + +You requested a password reset but unfortunately we couldn't find an account +associated with your e-mail address. Did you maybe use another one when you +signed up? + +Try a different e-mail [http://webapp:3000/password-reset/request] + +If you don't have an account at ocelot.social yet or if you didn't want to reset +your password, please ignore this e-mail. + +See you soon on ocelot.social [https://ocelot.social]! + +– The ocelot.social Team + +If you have questions or problems, feel free to contact our support: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendWrongEmail German renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Willkommen bei ocelot.social!

+
+
+

Du hast bei uns ein neues Passwort angefordert – leider haben wir aber keinen Account mit deiner E-Mailadresse gefunden. Kann es sein, dass du mit einer anderen Adresse bei uns angemeldet bist?

Versuch' es mit einer anderen E-Mail +

Wenn du noch keinen Account bei ocelot.social hast oder dein Password gar nicht ändern willst, kannst du diese E-Mail einfach ignorieren! +

+
+

Bis bald bei ocelot.social!

+

– Dein ocelot.social Team

+
+
+
+

Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: devops@ocelot.social

+
+ +
+ +", + "subject": "Falsche Mailaddresse? ocelot.social", + "text": "WILLKOMMEN BEI OCELOT.SOCIAL! + +Du hast bei uns ein neues Passwort angefordert – leider haben wir aber keinen +Account mit deiner E-Mailadresse gefunden. Kann es sein, dass du mit einer +anderen Adresse bei uns angemeldet bist? + +Versuch' es mit einer anderen E-Mail [http://webapp:3000/password-reset/request] + +Wenn du noch keinen Account bei ocelot.social hast oder dein Password gar nicht +ändern willst, kannst du diese E-Mail einfach ignorieren! + +Bis bald bei ocelot.social [https://ocelot.social]! + +– Dein ocelot.social Team + +Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: +devops@ocelot.social [devops@ocelot.social] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; diff --git a/backend/src/emails/__snapshots__/supportLine.spec.ts.snap b/backend/src/emails/__snapshots__/supportLine.spec.ts.snap new file mode 100644 index 000000000..78dab2d41 --- /dev/null +++ b/backend/src/emails/__snapshots__/supportLine.spec.ts.snap @@ -0,0 +1,531 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`sendResetPasswordMail with support English renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Hello Jenny Rostock,

+
+
+

So, you forgot your password? No problem! Just click the button below to reset it within the next 24 hours:

Confirm your e-mail address +

If you didn't request a new password feel free to ignore this e-mail.

+

If the above button doesn't work you can also copy the following code into your browser window: 123456

+
+

See you soon on ocelot.social!

+

– The ocelot.social Team

+
+
+
+

If you have questions or problems, feel free to contact our support: support@example.org

+
+ +
+ +", + "subject": "Reset Password ocelot.social", + "text": "HELLO JENNY ROSTOCK, + +So, you forgot your password? No problem! Just click the button below to reset +it within the next 24 hours: + +Confirm your e-mail address +[http://webapp:3000/password-reset/change-password?email=user%40example.org&nonce=123456] + +If you didn't request a new password feel free to ignore this e-mail. + +If the above button doesn't work you can also copy the following code into your +browser window: 123456 + +See you soon on ocelot.social [https://ocelot.social]! + +– The ocelot.social Team + +If you have questions or problems, feel free to contact our support: +support@example.org [support@example.org] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendResetPasswordMail with support German renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Hallo Jenny Rostock,

+
+
+

Du hast also dein Passwort vergessen? Kein Problem! Mit Klick auf diesen Button kannst du innerhalb der nächsten 24 Stunden dein Passwort zurücksetzen:

Bestätige deine E-Mail Adresse +

Falls du kein neues Passwort angefordert hast, kannst du diese E-Mail einfach ignorieren.

+

Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in dein Browserfenster kopieren: 123456

+
+

Bis bald bei ocelot.social!

+

– Dein ocelot.social Team

+
+
+
+

Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: support@example.org

+
+ +
+ +", + "subject": "Neues Passwort ocelot.social", + "text": "HALLO JENNY ROSTOCK, + +Du hast also dein Passwort vergessen? Kein Problem! Mit Klick auf diesen Button +kannst du innerhalb der nächsten 24 Stunden dein Passwort zurücksetzen: + +Bestätige deine E-Mail Adresse +[http://webapp:3000/password-reset/change-password?email=user%40example.org&nonce=123456] + +Falls du kein neues Passwort angefordert hast, kannst du diese E-Mail einfach +ignorieren. + +Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in +dein Browserfenster kopieren: 123456 + +Bis bald bei ocelot.social [https://ocelot.social]! + +– Dein ocelot.social Team + +Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: +support@example.org [support@example.org] + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendResetPasswordMail without support English renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Hello Jenny Rostock,

+
+
+

So, you forgot your password? No problem! Just click the button below to reset it within the next 24 hours:

Confirm your e-mail address +

If you didn't request a new password feel free to ignore this e-mail.

+

If the above button doesn't work you can also copy the following code into your browser window: 123456

+
+

See you soon on ocelot.social!

+

– The ocelot.social Team

+
+
+ +
+ +", + "subject": "Reset Password ocelot.social", + "text": "HELLO JENNY ROSTOCK, + +So, you forgot your password? No problem! Just click the button below to reset +it within the next 24 hours: + +Confirm your e-mail address +[http://webapp:3000/password-reset/change-password?email=user%40example.org&nonce=123456] + +If you didn't request a new password feel free to ignore this e-mail. + +If the above button doesn't work you can also copy the following code into your +browser window: 123456 + +See you soon on ocelot.social [https://ocelot.social]! + +– The ocelot.social Team + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; + +exports[`sendResetPasswordMail without support German renders correctly 1`] = ` +{ + "attachments": [], + "from": "ocelot.social ", + "html": " + + + + + + + + +
+
+
+
+
+

Hallo Jenny Rostock,

+
+
+

Du hast also dein Passwort vergessen? Kein Problem! Mit Klick auf diesen Button kannst du innerhalb der nächsten 24 Stunden dein Passwort zurücksetzen:

Bestätige deine E-Mail Adresse +

Falls du kein neues Passwort angefordert hast, kannst du diese E-Mail einfach ignorieren.

+

Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in dein Browserfenster kopieren: 123456

+
+

Bis bald bei ocelot.social!

+

– Dein ocelot.social Team

+
+
+ +
+ +", + "subject": "Neues Passwort ocelot.social", + "text": "HALLO JENNY ROSTOCK, + +Du hast also dein Passwort vergessen? Kein Problem! Mit Klick auf diesen Button +kannst du innerhalb der nächsten 24 Stunden dein Passwort zurücksetzen: + +Bestätige deine E-Mail Adresse +[http://webapp:3000/password-reset/change-password?email=user%40example.org&nonce=123456] + +Falls du kein neues Passwort angefordert hast, kannst du diese E-Mail einfach +ignorieren. + +Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in +dein Browserfenster kopieren: 123456 + +Bis bald bei ocelot.social [https://ocelot.social]! + +– Dein ocelot.social Team + + +ocelot.social Community [https://ocelot.social]", + "to": "user@example.org", +} +`; diff --git a/backend/src/emails/locales/de.json b/backend/src/emails/locales/de.json new file mode 100644 index 000000000..205a529b7 --- /dev/null +++ b/backend/src/emails/locales/de.json @@ -0,0 +1,72 @@ +{ + "notification": "Benachrichtigung", + "subjects": { + "changedGroupMemberRole": "Rolle in Gruppe geändert", + "chatMessage": "Neue Chat Nachricht", + "commentedOnPost": "Neuer Kommentar zu Beitrag", + "followedUserPosted": "Neuer Beitrag von gefolgtem Nutzer", + "mentionedInComment": "Erwähnung in Kommentar", + "mentionedInPost": "Erwähnung in Beitrag", + "newEmail": "Neue E-Mail Addresse", + "removedUserFromGroup": "Aus Gruppe entfernt", + "postInGroup": "Neuer Beitrag in Gruppe", + "resetPassword": "Neues Passwort", + "userJoinedGroup": "Nutzer tritt Gruppe bei", + "userLeftGroup": "Nutzer verlässt Gruppe", + "wrongEmail": "Falsche Mailaddresse?" + }, + "registration": { + "introduction": "Danke, dass du dich angemeldet hast – wir freuen uns, dich dabei zu haben. Jetzt fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können … Bitte bestätige deine E-Mail Adresse:", + "codeHint": "Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in dein Browserfenster kopieren: ", + "codeHintException": "Das funktioniert allerdings nur, wenn du dich über unsere Website registriert hast.", + "notYouStart": "Falls du dich nicht selbst bei ", + "notYouEnd": " angemeldet hast, schau doch mal vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen.", + "ps": "PS: Wenn du keinen Account bei uns möchtest, kannst du diese E-Mail einfach ignorieren. ;)" + }, + "emailVerification": { + "codeHint": "Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in dein Browserfenster kopieren: ", + "introduction": "Du möchtest also deine E-Mail ändern? Kein Problem! Mit Klick auf diesen Button kannst du deine neue E-Mail Adresse bestätigen:", + "doNotChange": "Falls du deine E-Mail Adresse doch nicht ändern möchtest, kannst du diese Nachricht einfach ignorieren. " + }, + "support": "Wenn du Fragen oder Probleme hast, kannst du dich gerne an den Support wenden: ", + "buttons": { + "confirmEmail": "Bestätige deine E-Mail Adresse", + "resetPassword": "Passwort zurücksetzen", + "tryAgain": "Versuch' es mit einer anderen E-Mail", + "verifyEmail": "E-Mail Adresse bestätigen", + "viewChat": "Chat anzeigen", + "viewComment": "Kommentar ansehen", + "viewGroup": "Gruppe ansehen", + "viewPost": "Beitrag ansehen" + }, + "general": { + "greeting": "Hallo", + "seeYou": "Bis bald bei ", + "yourTeam": "– Dein {team} Team", + "settingsHint": "PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine ", + "settingsName": "Benachrichtigungseinstellungen", + "welcome": "Willkommen bei" + }, + "resetPassword": { + "codeHint": "Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in dein Browserfenster kopieren: ", + "ignore": "Falls du kein neues Passwort angefordert hast, kannst du diese E-Mail einfach ignorieren.", + "introduction": "Du hast also dein Passwort vergessen? Kein Problem! Mit Klick auf diesen Button kannst du innerhalb der nächsten 24 Stunden dein Passwort zurücksetzen:" + }, + "wrongEmail": { + "codeHint": "Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in dein Browserfenster kopieren: ", + "ignoreEnd": " hast oder dein Password gar nicht ändern willst, kannst du diese E-Mail einfach ignorieren!", + "ignoreStart": "Wenn du noch keinen Account bei ", + "introduction": "Du hast bei uns ein neues Passwort angefordert – leider haben wir aber keinen Account mit deiner E-Mailadresse gefunden. Kann es sein, dass du mit einer anderen Adresse bei uns angemeldet bist?" + }, + "changedGroupMemberRole": "deine Rolle in der Gruppe „{groupName}“ wurde geändert. Klicke auf den Knopf, um diese Gruppe zu sehen:", + "chatMessageStart": "du hast eine neue Chat-Nachricht von ", + "chatMessageEnd": " erhalten.", + "commentedOnPost": " hat einen Beitrag den du beobachtest mit dem Titel „{postTitle}“ kommentiert. Klicke auf den Knopf, um diesen Kommentar zu sehen:", + "followedUserPosted": ", ein Nutzer dem du folgst, hat einen neuen Beitrag mit dem Titel „{postTitle}“ geschrieben. Klicke auf den Knopf, um diesen Beitrag zu sehen:", + "mentionedInComment": " hat dich in einem Kommentar zu dem Beitrag mit dem Titel „{postTitle}“ erwähnt. Klicke auf den Knopf, um den Kommentar zu sehen:", + "mentionedInPost": " hat dich in einem Beitrag mit dem Titel „{postTitle}“ erwähnt. Klicke auf den Knopf, um den Beitrag zu sehen:", + "postInGroup": "jemand hat einen neuen Beitrag mit dem Titel „{postTitle}“ in einer deiner Gruppen geschrieben. Klicke auf den Knopf, um diesen Beitrag zu sehen:", + "removedUserFromGroup": "du wurdest aus der Gruppe „{groupName}“ entfernt.", + "userJoinedGroup": " ist der Gruppe „{groupName}“ beigetreten. Klicke auf den Knopf, um diese Gruppe zu sehen:", + "userLeftGroup": " hat die Gruppe „{groupName}“ verlassen. Klicke auf den Knopf, um diese Gruppe zu sehen:" +} diff --git a/backend/src/emails/locales/en.json b/backend/src/emails/locales/en.json new file mode 100644 index 000000000..97a45d3c7 --- /dev/null +++ b/backend/src/emails/locales/en.json @@ -0,0 +1,71 @@ +{ + "notification": "Notification", + "subjects": { + "changedGroupMemberRole": "Role in group changed", + "chatMessage": "New chat message", + "commentedOnPost": "New comment on post", + "followedUserPosted": "New post by followd user", + "mentionedInComment": "Mentioned in comment", + "mentionedInPost": "Mentioned in post", + "newEmail": "New E-Mail Address", + "removedUserFromGroup": "Removed from group", + "postInGroup": "New post in group", + "resetPassword": "Reset Password", + "userJoinedGroup": "User joined group", + "userLeftGroup": "User left group", + "wrongEmail": "Wrong E-mail?" + }, + "registration": { + "introduction": "Thank you for joining our cause – it's awesome to have you on board. There's just one tiny step missing before we can start shaping the world together … Please confirm your e-mail address by clicking the button below:", + "codeHint": "If the above button doesn't work, you can also copy the following code into your browser window: ", + "codeHintException": "However, this only works if you have registered through our website.", + "notYouStart": "If you didn't sign up for ", + "notYouEnd": " we recommend you to check it out! It's a social network from people for people who want to connect and change the world together.", + "ps": "PS: If you ignore this e-mail we will not create an account for you. ;)" + }, + "emailVerification": { + "codeHint": "If the above button doesn't work, you can also copy the following code into your browser window: ", + "introduction": "So, you want to change your e-mail? No problem! Just click the button below to verify your new address:", + "doNotChange": "If you don't want to change your e-mail address feel free to ignore this message. " + }, + "support": "If you have questions or problems, feel free to contact our support: ", + "buttons": { + "confirmEmail": "Confirm your e-mail address", + "resetPassword": "Reset password", + "tryAgain": "Try a different e-mail", + "verifyEmail": "Verify e-mail address", + "viewChat": "Show Chat", + "viewComment": "View comment", + "viewGroup": "View group", + "viewPost": "View post" + }, + "general": { + "greeting": "Hello", + "seeYou": "See you soon on ", + "yourTeam": "– The {team} Team", + "settingsHint": "PS: If you don't want to receive e-mails anymore, change your ", + "settingsName": "notification settings", + "welcome": "Welcome to" + }, + "resetPassword": { + "codeHint": "If the above button doesn't work you can also copy the following code into your browser window: ", + "ignore": "If you didn't request a new password feel free to ignore this e-mail.", + "introduction": "So, you forgot your password? No problem! Just click the button below to reset it within the next 24 hours:" + }, + "wrongEmail": { + "ignoreEnd": " yet or if you didn't want to reset your password, please ignore this e-mail.", + "ignoreStart": "If you don't have an account at ", + "introduction": "You requested a password reset but unfortunately we couldn't find an account associated with your e-mail address. Did you maybe use another one when you signed up?" + }, + "changedGroupMemberRole": "your role in the group “{groupName}” has been changed. Click on the button to view this group:", + "chatMessageStart": "you have received a new chat message from ", + "chatMessageEnd": ".", + "commentedOnPost": " commented on a post that you are observing with the title “{postTitle}”. Click on the button to view this comment:", + "followedUserPosted": ", a user you are following, wrote a new post with the title “{postTitle}”. Click on the button to view this post:", + "mentionedInComment": " mentioned you in a comment to the post with the title “{postTitle}”. Click on the button to view this comment:", + "mentionedInPost": " mentioned you in a post with the title “{postTitle}”. Click on the button to view this post:", + "removedUserFromGroup": "you have been removed from the group “{groupName}”.", + "postInGroup": "someone wrote a new post with the title “{postTitle}” in one of your groups. Click on the button to view this post:", + "userJoinedGroup": " joined the group “{groupName}”. Click on the button to view this group:", + "userLeftGroup": " left the group “{groupName}”. Click on the button to view this group:" +} diff --git a/backend/src/emails/sendChatMessageMail.spec.ts b/backend/src/emails/sendChatMessageMail.spec.ts new file mode 100644 index 000000000..45835bbc3 --- /dev/null +++ b/backend/src/emails/sendChatMessageMail.spec.ts @@ -0,0 +1,87 @@ +import { sendChatMessageMail } from './sendEmail' + +const senderUser = { + allowEmbedIframes: false, + createdAt: '2025-04-30T00:16:49.610Z', + deleted: false, + disabled: false, + emailNotificationsChatMessage: true, + emailNotificationsCommentOnObservedPost: true, + emailNotificationsFollowingUsers: true, + emailNotificationsGroupMemberJoined: true, + emailNotificationsGroupMemberLeft: true, + emailNotificationsGroupMemberRemoved: true, + emailNotificationsGroupMemberRoleChanged: true, + emailNotificationsMention: true, + emailNotificationsPostInGroup: true, + encryptedPassword: '$2b$10$n.WujXapJrvn498lS97MD.gn8QwjWI9xlf8ckEYYtMTOPadMidcbG', + id: 'chatSender', + locale: 'en', + name: 'chatSender', + role: 'user', + showShoutsPublicly: false, + slug: 'chatsender', + termsAndConditionsAgreedAt: '2019-08-01T10:47:19.212Z', + termsAndConditionsAgreedVersion: '0.0.1', + updatedAt: '2025-04-30T00:16:49.610Z', +} + +const recipientUser = { + allowEmbedIframes: false, + createdAt: '2025-04-30T00:16:49.716Z', + deleted: false, + disabled: false, + emailNotificationsChatMessage: true, + emailNotificationsCommentOnObservedPost: true, + emailNotificationsFollowingUsers: true, + emailNotificationsGroupMemberJoined: true, + emailNotificationsGroupMemberLeft: true, + emailNotificationsGroupMemberRemoved: true, + emailNotificationsGroupMemberRoleChanged: true, + emailNotificationsMention: true, + emailNotificationsPostInGroup: true, + encryptedPassword: '$2b$10$KOrCHvEB5CM7D.P3VcX2z.pSSBZKZhPqHW/QKym6V1S6fiG..xtBq', + id: 'chatReceiver', + locale: 'en', + name: 'chatReceiver', + role: 'user', + showShoutsPublicly: false, + slug: 'chatreceiver', + termsAndConditionsAgreedAt: '2019-08-01T10:47:19.212Z', + termsAndConditionsAgreedVersion: '0.0.1', + updatedAt: '2025-04-30T00:16:49.716Z', +} + +describe('sendChatMessageMail', () => { + describe('English', () => { + beforeEach(() => { + recipientUser.locale = 'en' + }) + + it('chat_message template', async () => { + await expect( + sendChatMessageMail({ + email: 'user@example.org', + senderUser, + recipientUser, + }), + ).resolves.toMatchSnapshot() + }) + }) + + describe('German', () => { + beforeEach(() => { + recipientUser.locale = 'de' + }) + + it('chat_message template', async () => { + await expect( + sendChatMessageMail({ + email: 'user@example.org', + senderUser, + recipientUser, + }), + ).resolves.toMatchSnapshot() + }) + }) +}) diff --git a/backend/src/emails/sendEmail.ts b/backend/src/emails/sendEmail.ts new file mode 100644 index 000000000..61a5ba951 --- /dev/null +++ b/backend/src/emails/sendEmail.ts @@ -0,0 +1,322 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +import path from 'node:path' + +import Email from 'email-templates' +import { createTransport } from 'nodemailer' + +// import type Email as EmailType from '@types/email-templates' + +import CONFIG, { nodemailerTransportOptions } from '@config/index' +import logosWebapp from '@config/logosBranded' +import metadata from '@config/metadata' +import { UserDbProperties } from '@db/types/User' + +const welcomeImageUrl = new URL(logosWebapp.LOGO_WELCOME_PATH, CONFIG.CLIENT_URI) +const settingsUrl = new URL('/settings/notifications', CONFIG.CLIENT_URI) + +export const defaultParams = { + welcomeImageUrl, + APPLICATION_NAME: CONFIG.APPLICATION_NAME, + ORGANIZATION_NAME: metadata.ORGANIZATION_NAME, + ORGANIZATION_URL: CONFIG.ORGANIZATION_URL, + SUPPORT_EMAIL: CONFIG.SUPPORT_EMAIL, + supportUrl: CONFIG.SUPPORT_URL, + settingsUrl, + renderSettingsUrl: true, +} + +const from = `${CONFIG.APPLICATION_NAME} <${CONFIG.EMAIL_DEFAULT_SENDER}>` + +const transport = createTransport(nodemailerTransportOptions) + +const email = new Email({ + message: { + from, + }, + transport, + i18n: { + locales: ['en', 'de'], + defaultLocale: CONFIG.LANGUAGE_DEFAULT, + retryInDefaultLocale: false, + directory: path.join(__dirname, 'locales'), + updateFiles: false, + objectNotation: true, + mustacheConfig: { + tags: ['{', '}'], + disable: false, + }, + }, + send: CONFIG.SEND_MAIL, + preview: false, + // This is very useful to see the emails sent by the unit tests + /* + preview: { + open: { + app: 'brave-browser', + }, + }, + */ +}) + +interface OriginalMessage { + to: string + from: string + attachments: string[] + subject: string + html: string + text: string +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const sendNotificationMail = async (notification: any): Promise => { + const locale = notification?.to?.locale + const to = notification?.email + const name = notification?.to?.name + const template = notification?.reason + + try { + const { originalMessage } = await email.send({ + template: path.join(__dirname, 'templates', template), + message: { + to, + }, + locals: { + ...defaultParams, + locale, + name, + postTitle: + notification?.from?.__typename === 'Comment' + ? notification?.from?.post?.title + : notification?.from?.title, + postUrl: new URL( + notification?.from?.__typename === 'Comment' + ? `/post/${notification?.from?.post?.id}/${notification?.from?.post?.slug}` + : `/post/${notification?.from?.id}/${notification?.from?.slug}`, + CONFIG.CLIENT_URI, + ), + postAuthorName: + notification?.from?.__typename === 'Comment' + ? undefined + : notification?.from?.author?.name, + postAuthorUrl: + notification?.from?.__typename === 'Comment' + ? undefined + : new URL( + `user/${notification?.from?.author?.id}/${notification?.from?.author?.slug}`, + CONFIG.CLIENT_URI, + ), + commenterName: + notification?.from?.__typename === 'Comment' + ? notification?.from?.author?.name + : undefined, + commenterUrl: + notification?.from?.__typename === 'Comment' + ? new URL( + `/profile/${notification?.from?.author?.id}/${notification?.from?.author?.slug}`, + CONFIG.CLIENT_URI, + ) + : undefined, + commentUrl: + notification?.from?.__typename === 'Comment' + ? new URL( + `/post/${notification?.from?.post?.id}/${notification?.from?.post?.slug}#commentId-${notification?.from?.id}`, + CONFIG.CLIENT_URI, + ) + : undefined, + // chattingUser: 'SR-71', + // chatUrl: new URL('/chat', CONFIG.CLIENT_URI), + groupUrl: + notification?.from?.__typename === 'Group' + ? new URL( + `/groups/${notification?.from?.id}/${notification?.from?.slug}`, + CONFIG.CLIENT_URI, + ) + : undefined, + groupName: + notification?.from?.__typename === 'Group' ? notification?.from?.name : undefined, + groupRelatedUserName: + notification?.from?.__typename === 'Group' ? notification?.relatedUser?.name : undefined, + groupRelatedUserUrl: + notification?.from?.__typename === 'Group' + ? new URL( + `/profile/${notification?.relatedUser?.id}/${notification?.relatedUser?.slug}`, + CONFIG.CLIENT_URI, + ) + : undefined, + }, + }) + return originalMessage as OriginalMessage + } catch (error) { + throw new Error(error) + } +} + +export interface ChatMessageEmailInput { + senderUser: UserDbProperties + recipientUser: UserDbProperties + email: string +} + +export const sendChatMessageMail = async ( + data: ChatMessageEmailInput, +): Promise => { + const { senderUser, recipientUser } = data + const to = data.email + try { + const { originalMessage } = await email.send({ + template: path.join(__dirname, 'templates', 'chat_message'), + message: { + to, + }, + locals: { + ...defaultParams, + locale: recipientUser.locale, + name: recipientUser.name, + chattingUser: senderUser.name, + chattingUserUrl: new URL(`/profile/${senderUser.id}/${senderUser.slug}`, CONFIG.CLIENT_URI), + chatUrl: new URL('/chat', CONFIG.CLIENT_URI), + }, + }) + return originalMessage as OriginalMessage + } catch (error) { + throw new Error(error) + } +} + +interface VerifyMailInput { + email: string + nonce: string + locale: string +} + +interface RegistrationMailInput extends VerifyMailInput { + inviteCode?: string +} + +export const sendRegistrationMail = async ( + data: RegistrationMailInput, +): Promise => { + const { nonce, locale, inviteCode } = data + const to = data.email + const actionUrl = new URL('/registration', CONFIG.CLIENT_URI) + actionUrl.searchParams.set('email', to) + actionUrl.searchParams.set('nonce', nonce) + if (inviteCode) { + actionUrl.searchParams.set('inviteCode', inviteCode) + actionUrl.searchParams.set('method', 'invite-code') + } else { + actionUrl.searchParams.set('method', 'invite-mail') + } + + try { + const { originalMessage } = await email.send({ + template: path.join(__dirname, 'templates', 'registration'), + message: { + to, + }, + locals: { + ...defaultParams, + locale, + actionUrl, + nonce, + renderSettingsUrl: false, + }, + }) + return originalMessage as OriginalMessage + } catch (error) { + throw new Error(error) + } +} + +interface EmailVerificationInput extends VerifyMailInput { + name: string +} + +export const sendEmailVerification = async ( + data: EmailVerificationInput, +): Promise => { + const { nonce, locale, name } = data + const to = data.email + const actionUrl = new URL('/settings/my-email-address/verify', CONFIG.CLIENT_URI) + actionUrl.searchParams.set('email', to) + actionUrl.searchParams.set('nonce', nonce) + + try { + const { originalMessage } = await email.send({ + template: path.join(__dirname, 'templates', 'emailVerification'), + message: { + to, + }, + locals: { + ...defaultParams, + locale, + actionUrl, + nonce, + name, + renderSettingsUrl: false, + }, + }) + return originalMessage as OriginalMessage + } catch (error) { + throw new Error(error) + } +} + +export const sendResetPasswordMail = async ( + data: EmailVerificationInput, +): Promise => { + const { nonce, locale, name } = data + const to = data.email + const actionUrl = new URL('/password-reset/change-password', CONFIG.CLIENT_URI) + actionUrl.searchParams.set('email', to) + actionUrl.searchParams.set('nonce', nonce) + try { + const { originalMessage } = await email.send({ + template: path.join(__dirname, 'templates', 'resetPassword'), + message: { + to, + }, + locals: { + ...defaultParams, + locale, + actionUrl, + nonce, + name, + renderSettingsUrl: false, + }, + }) + return originalMessage as OriginalMessage + } catch (error) { + throw new Error(error) + } +} + +export const sendWrongEmail = async (data: { + locale: string + email: string +}): Promise => { + const { locale } = data + const to = data.email + const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI) + try { + const { originalMessage } = await email.send({ + template: path.join(__dirname, 'templates', 'wrongEmail'), + message: { + to, + }, + locals: { + ...defaultParams, + locale, + actionUrl, + renderSettingsUrl: false, + }, + }) + return originalMessage as OriginalMessage + } catch (error) { + throw new Error(error) + } +} diff --git a/backend/src/emails/sendEmailVerification.spec.ts b/backend/src/emails/sendEmailVerification.spec.ts new file mode 100644 index 000000000..0863dd9db --- /dev/null +++ b/backend/src/emails/sendEmailVerification.spec.ts @@ -0,0 +1,35 @@ +import { sendEmailVerification } from './sendEmail' + +describe('sendEmailVerification', () => { + const data: { + email: string + nonce: string + locale: string + name: string + } = { + email: 'user@example.org', + nonce: '123456', + locale: 'en', + name: 'User', + } + + describe('English', () => { + beforeEach(() => { + data.locale = 'en' + }) + + it('renders correctly', async () => { + await expect(sendEmailVerification(data)).resolves.toMatchSnapshot() + }) + }) + + describe('German', () => { + beforeEach(() => { + data.locale = 'de' + }) + + it('renders correctly', async () => { + await expect(sendEmailVerification(data)).resolves.toMatchSnapshot() + }) + }) +}) diff --git a/backend/src/emails/sendNotificationMail.spec.ts b/backend/src/emails/sendNotificationMail.spec.ts new file mode 100644 index 000000000..fee641e2e --- /dev/null +++ b/backend/src/emails/sendNotificationMail.spec.ts @@ -0,0 +1,475 @@ +import { sendNotificationMail } from './sendEmail' + +describe('sendNotificationMail', () => { + let locale = 'en' + + describe('English', () => { + beforeEach(() => { + locale = 'en' + }) + + it('followed_user_posted template', async () => { + await expect( + sendNotificationMail({ + reason: 'followed_user_posted', + email: 'user@example.org', + to: { + name: 'Jenny Rostock', + id: 'u1', + slug: 'jenny-rostock', + locale, + }, + from: { + id: 'p1', + slug: 'new-post', + title: 'New Post', + author: { + id: 'u2', + name: 'Peter Lustig', + slug: 'peter-lustig', + }, + }, + }), + ).resolves.toMatchSnapshot() + }) + + it('post_in_group template', async () => { + await expect( + sendNotificationMail({ + reason: 'post_in_group', + email: 'user@example.org', + to: { + name: 'Jenny Rostock', + id: 'u1', + slug: 'jenny-rostock', + locale, + }, + from: { + id: 'p1', + slug: 'new-post', + title: 'New Post', + author: { + id: 'u2', + name: 'Peter Lustig', + slug: 'peter-lustig', + }, + }, + }), + ).resolves.toMatchSnapshot() + }) + + it('mentioned_in_post template', async () => { + await expect( + sendNotificationMail({ + reason: 'mentioned_in_post', + email: 'user@example.org', + to: { + name: 'Jenny Rostock', + id: 'u1', + slug: 'jenny-rostock', + locale, + }, + from: { + id: 'p1', + slug: 'new-post', + title: 'New Post', + author: { + id: 'u2', + name: 'Peter Lustig', + slug: 'peter-lustig', + }, + }, + }), + ).resolves.toMatchSnapshot() + }) + + it('commented_on_post template', async () => { + await expect( + sendNotificationMail({ + reason: 'commented_on_post', + email: 'user@example.org', + to: { + name: 'Jenny Rostock', + id: 'u1', + slug: 'jenny-rostock', + locale, + }, + from: { + __typename: 'Comment', + id: 'c1', + slug: 'new-comment', + author: { + id: 'u2', + name: 'Peter Lustig', + slug: 'peter-lustig', + }, + post: { + id: 'p1', + slug: 'new-post', + title: 'New Post', + }, + }, + }), + ).resolves.toMatchSnapshot() + }) + + it('mentioned_in_comment template', async () => { + await expect( + sendNotificationMail({ + reason: 'mentioned_in_comment', + email: 'user@example.org', + to: { + name: 'Jenny Rostock', + id: 'u1', + slug: 'jenny-rostock', + locale, + }, + from: { + __typename: 'Comment', + id: 'c1', + slug: 'new-comment', + author: { + id: 'u2', + name: 'Peter Lustig', + slug: 'peter-lustig', + }, + post: { + id: 'p1', + slug: 'new-post', + title: 'New Post', + }, + }, + }), + ).resolves.toMatchSnapshot() + }) + + it('changed_group_member_role template', async () => { + await expect( + sendNotificationMail({ + reason: 'changed_group_member_role', + email: 'user@example.org', + to: { + name: 'Jenny Rostock', + id: 'u1', + slug: 'jenny-rostock', + locale, + }, + from: { + __typename: 'Group', + id: 'g1', + slug: 'the-group', + name: 'The Group', + }, + }), + ).resolves.toMatchSnapshot() + }) + + it('user_joined_group template', async () => { + await expect( + sendNotificationMail({ + reason: 'user_joined_group', + email: 'user@example.org', + to: { + name: 'Jenny Rostock', + id: 'u1', + slug: 'jenny-rostock', + locale, + }, + from: { + __typename: 'Group', + id: 'g1', + slug: 'the-group', + name: 'The Group', + }, + relatedUser: { + id: 'u2', + name: 'Peter Lustig', + slug: 'peter-lustig', + }, + }), + ).resolves.toMatchSnapshot() + }) + + it('user_left_group template', async () => { + await expect( + sendNotificationMail({ + reason: 'user_left_group', + email: 'user@example.org', + to: { + name: 'Jenny Rostock', + id: 'u1', + slug: 'jenny-rostock', + locale, + }, + from: { + __typename: 'Group', + id: 'g1', + slug: 'the-group', + name: 'The Group', + }, + relatedUser: { + id: 'u2', + name: 'Peter Lustig', + slug: 'peter-lustig', + }, + }), + ).resolves.toMatchSnapshot() + }) + + it('removed_user_from_group template', async () => { + await expect( + sendNotificationMail({ + reason: 'removed_user_from_group', + email: 'user@example.org', + to: { + name: 'Jenny Rostock', + id: 'u1', + slug: 'jenny-rostock', + locale, + }, + from: { + __typename: 'Group', + id: 'g1', + slug: 'the-group', + name: 'The Group', + }, + }), + ).resolves.toMatchSnapshot() + }) + }) + + describe('German', () => { + beforeEach(() => { + locale = 'de' + }) + + it('followed_user_posted template', async () => { + await expect( + sendNotificationMail({ + reason: 'followed_user_posted', + email: 'user@example.org', + to: { + name: 'Jenny Rostock', + id: 'u1', + slug: 'jenny-rostock', + locale, + }, + from: { + id: 'p1', + slug: 'new-post', + title: 'New Post', + author: { + id: 'u2', + name: 'Peter Lustig', + slug: 'peter-lustig', + }, + }, + }), + ).resolves.toMatchSnapshot() + }) + + it('post_in_group template', async () => { + await expect( + sendNotificationMail({ + reason: 'post_in_group', + email: 'user@example.org', + to: { + name: 'Jenny Rostock', + id: 'u1', + slug: 'jenny-rostock', + locale, + }, + from: { + id: 'p1', + slug: 'new-post', + title: 'New Post', + author: { + id: 'u2', + name: 'Peter Lustig', + slug: 'peter-lustig', + }, + }, + }), + ).resolves.toMatchSnapshot() + }) + + it('mentioned_in_post template', async () => { + await expect( + sendNotificationMail({ + reason: 'mentioned_in_post', + email: 'user@example.org', + to: { + name: 'Jenny Rostock', + id: 'u1', + slug: 'jenny-rostock', + locale, + }, + from: { + id: 'p1', + slug: 'new-post', + title: 'New Post', + author: { + id: 'u2', + name: 'Peter Lustig', + slug: 'peter-lustig', + }, + }, + }), + ).resolves.toMatchSnapshot() + }) + + it('commented_on_post template', async () => { + await expect( + sendNotificationMail({ + reason: 'commented_on_post', + email: 'user@example.org', + to: { + name: 'Jenny Rostock', + id: 'u1', + slug: 'jenny-rostock', + locale, + }, + from: { + __typename: 'Comment', + id: 'c1', + slug: 'new-comment', + author: { + id: 'u2', + name: 'Peter Lustig', + slug: 'peter-lustig', + }, + post: { + id: 'p1', + slug: 'new-post', + title: 'New Post', + }, + }, + }), + ).resolves.toMatchSnapshot() + }) + + it('mentioned_in_comment template', async () => { + await expect( + sendNotificationMail({ + reason: 'mentioned_in_comment', + email: 'user@example.org', + to: { + name: 'Jenny Rostock', + id: 'u1', + slug: 'jenny-rostock', + locale, + }, + from: { + __typename: 'Comment', + id: 'c1', + slug: 'new-comment', + author: { + id: 'u2', + name: 'Peter Lustig', + slug: 'peter-lustig', + }, + post: { + id: 'p1', + slug: 'new-post', + title: 'New Post', + }, + }, + }), + ).resolves.toMatchSnapshot() + }) + + it('changed_group_member_role template', async () => { + await expect( + sendNotificationMail({ + reason: 'changed_group_member_role', + email: 'user@example.org', + to: { + name: 'Jenny Rostock', + id: 'u1', + slug: 'jenny-rostock', + locale, + }, + from: { + __typename: 'Group', + id: 'g1', + slug: 'the-group', + name: 'The Group', + }, + }), + ).resolves.toMatchSnapshot() + }) + + it('user_joined_group template', async () => { + await expect( + sendNotificationMail({ + reason: 'user_joined_group', + email: 'user@example.org', + to: { + name: 'Jenny Rostock', + id: 'u1', + slug: 'jenny-rostock', + locale, + }, + from: { + __typename: 'Group', + id: 'g1', + slug: 'the-group', + name: 'The Group', + }, + relatedUser: { + id: 'u2', + name: 'Peter Lustig', + slug: 'peter-lustig', + }, + }), + ).resolves.toMatchSnapshot() + }) + + it('user_left_group template', async () => { + await expect( + sendNotificationMail({ + reason: 'user_left_group', + email: 'user@example.org', + to: { + name: 'Jenny Rostock', + id: 'u1', + slug: 'jenny-rostock', + locale, + }, + from: { + __typename: 'Group', + id: 'g1', + slug: 'the-group', + name: 'The Group', + }, + relatedUser: { + id: 'u2', + name: 'Peter Lustig', + slug: 'peter-lustig', + }, + }), + ).resolves.toMatchSnapshot() + }) + + it('removed_user_from_group template', async () => { + await expect( + sendNotificationMail({ + reason: 'removed_user_from_group', + email: 'user@example.org', + to: { + name: 'Jenny Rostock', + id: 'u1', + slug: 'jenny-rostock', + locale, + }, + from: { + __typename: 'Group', + id: 'g1', + slug: 'the-group', + name: 'The Group', + }, + }), + ).resolves.toMatchSnapshot() + }) + }) +}) diff --git a/backend/src/emails/sendRegistrationMail.spec.ts b/backend/src/emails/sendRegistrationMail.spec.ts new file mode 100644 index 000000000..ea66771c2 --- /dev/null +++ b/backend/src/emails/sendRegistrationMail.spec.ts @@ -0,0 +1,63 @@ +import { sendRegistrationMail } from './sendEmail' + +describe('sendRegistrationMail', () => { + const data: { + email: string + nonce: string + locale: string + inviteCode?: string + } = { + email: 'user@example.org', + nonce: '123456', + locale: 'en', + inviteCode: 'welcome', + } + + describe('with invite code', () => { + describe('English', () => { + beforeEach(() => { + data.locale = 'en' + data.inviteCode = 'welcome' + }) + + it('renders correctly', async () => { + await expect(sendRegistrationMail(data)).resolves.toMatchSnapshot() + }) + }) + + describe('German', () => { + beforeEach(() => { + data.locale = 'de' + data.inviteCode = 'welcome' + }) + + it('renders correctly', async () => { + await expect(sendRegistrationMail(data)).resolves.toMatchSnapshot() + }) + }) + }) + + describe('without invite code', () => { + describe('English', () => { + beforeEach(() => { + data.locale = 'en' + delete data.inviteCode + }) + + it('renders correctly', async () => { + await expect(sendRegistrationMail(data)).resolves.toMatchSnapshot() + }) + }) + + describe('German', () => { + beforeEach(() => { + data.locale = 'de' + delete data.inviteCode + }) + + it('renders correctly', async () => { + await expect(sendRegistrationMail(data)).resolves.toMatchSnapshot() + }) + }) + }) +}) diff --git a/backend/src/emails/sendResetPasswordMail.spec.ts b/backend/src/emails/sendResetPasswordMail.spec.ts new file mode 100644 index 000000000..e37af2e7b --- /dev/null +++ b/backend/src/emails/sendResetPasswordMail.spec.ts @@ -0,0 +1,35 @@ +import { sendResetPasswordMail } from './sendEmail' + +describe('sendResetPasswordMail', () => { + const data: { + email: string + nonce: string + locale: string + name: string + } = { + email: 'user@example.org', + nonce: '123456', + locale: 'en', + name: 'Jenny Rostock', + } + + describe('English', () => { + beforeEach(() => { + data.locale = 'en' + }) + + it('renders correctly', async () => { + await expect(sendResetPasswordMail(data)).resolves.toMatchSnapshot() + }) + }) + + describe('German', () => { + beforeEach(() => { + data.locale = 'de' + }) + + it('renders correctly', async () => { + await expect(sendResetPasswordMail(data)).resolves.toMatchSnapshot() + }) + }) +}) diff --git a/backend/src/emails/sendWrongEmail.spec.ts b/backend/src/emails/sendWrongEmail.spec.ts new file mode 100644 index 000000000..854d935f9 --- /dev/null +++ b/backend/src/emails/sendWrongEmail.spec.ts @@ -0,0 +1,31 @@ +import { sendWrongEmail } from './sendEmail' + +describe('sendWrongEmail', () => { + const data: { + email: string + locale: string + } = { + email: 'user@example.org', + locale: 'en', + } + + describe('English', () => { + beforeEach(() => { + data.locale = 'en' + }) + + it('renders correctly', async () => { + await expect(sendWrongEmail(data)).resolves.toMatchSnapshot() + }) + }) + + describe('German', () => { + beforeEach(() => { + data.locale = 'de' + }) + + it('renders correctly', async () => { + await expect(sendWrongEmail(data)).resolves.toMatchSnapshot() + }) + }) +}) diff --git a/backend/src/emails/supportLine.spec.ts b/backend/src/emails/supportLine.spec.ts new file mode 100644 index 000000000..c890be794 --- /dev/null +++ b/backend/src/emails/supportLine.spec.ts @@ -0,0 +1,67 @@ +import { sendResetPasswordMail, defaultParams } from './sendEmail' + +describe('sendResetPasswordMail', () => { + const data: { + email: string + nonce: string + locale: string + name: string + } = { + email: 'user@example.org', + nonce: '123456', + locale: 'en', + name: 'Jenny Rostock', + } + + describe('with support', () => { + beforeEach(() => { + defaultParams.SUPPORT_EMAIL = 'support@example.org' + }) + + describe('English', () => { + beforeEach(() => { + data.locale = 'en' + }) + + it('renders correctly', async () => { + await expect(sendResetPasswordMail(data)).resolves.toMatchSnapshot() + }) + }) + + describe('German', () => { + beforeEach(() => { + data.locale = 'de' + }) + + it('renders correctly', async () => { + await expect(sendResetPasswordMail(data)).resolves.toMatchSnapshot() + }) + }) + }) + + describe('without support', () => { + beforeEach(() => { + delete defaultParams.SUPPORT_EMAIL + }) + + describe('English', () => { + beforeEach(() => { + data.locale = 'en' + }) + + it('renders correctly', async () => { + await expect(sendResetPasswordMail(data)).resolves.toMatchSnapshot() + }) + }) + + describe('German', () => { + beforeEach(() => { + data.locale = 'de' + }) + + it('renders correctly', async () => { + await expect(sendResetPasswordMail(data)).resolves.toMatchSnapshot() + }) + }) + }) +}) diff --git a/backend/src/emails/templates/changed_group_member_role/html.pug b/backend/src/emails/templates/changed_group_member_role/html.pug new file mode 100644 index 000000000..acb50546d --- /dev/null +++ b/backend/src/emails/templates/changed_group_member_role/html.pug @@ -0,0 +1,7 @@ +extend ../layout.pug + +block content + .content + - var groupUrl = groupUrl + p= t('changedGroupMemberRole', { groupName }) + a.button(href=groupUrl)= t('buttons.viewGroup') diff --git a/backend/src/emails/templates/changed_group_member_role/subject.pug b/backend/src/emails/templates/changed_group_member_role/subject.pug new file mode 100644 index 000000000..2cd0d345e --- /dev/null +++ b/backend/src/emails/templates/changed_group_member_role/subject.pug @@ -0,0 +1 @@ += `${APPLICATION_NAME} – ${t('notification')}: ${t('subjects.changedGroupMemberRole')}` \ No newline at end of file diff --git a/backend/src/emails/templates/chat_message/html.pug b/backend/src/emails/templates/chat_message/html.pug new file mode 100644 index 000000000..d49581d7d --- /dev/null +++ b/backend/src/emails/templates/chat_message/html.pug @@ -0,0 +1,8 @@ +extend ../layout.pug + +block content + .content + p= t('chatMessageStart') + a.user(href=chattingUserUrl)= chattingUser + = t('chatMessageEnd') + a.button(href=chatUrl)= t('buttons.viewChat') diff --git a/backend/src/emails/templates/chat_message/subject.pug b/backend/src/emails/templates/chat_message/subject.pug new file mode 100644 index 000000000..73206e2d6 --- /dev/null +++ b/backend/src/emails/templates/chat_message/subject.pug @@ -0,0 +1 @@ += `${APPLICATION_NAME} – ${t('notification')}: ${t('subjects.chatMessage')}` \ No newline at end of file diff --git a/backend/src/emails/templates/commented_on_post/html.pug b/backend/src/emails/templates/commented_on_post/html.pug new file mode 100644 index 000000000..b139e0f9d --- /dev/null +++ b/backend/src/emails/templates/commented_on_post/html.pug @@ -0,0 +1,8 @@ +extend ../layout.pug + +block content + .content + p + a.user(href=commenterUrl)= commenterName + = t('commentedOnPost', { postTitle}) + a.button(href=commentUrl)= t('buttons.viewComment') diff --git a/backend/src/emails/templates/commented_on_post/subject.pug b/backend/src/emails/templates/commented_on_post/subject.pug new file mode 100644 index 000000000..6a3d4da35 --- /dev/null +++ b/backend/src/emails/templates/commented_on_post/subject.pug @@ -0,0 +1 @@ += `${APPLICATION_NAME} – ${t('notification')}: ${t('subjects.commentedOnPost')}` \ No newline at end of file diff --git a/backend/src/emails/templates/emailVerification/html.pug b/backend/src/emails/templates/emailVerification/html.pug new file mode 100644 index 000000000..7483106e4 --- /dev/null +++ b/backend/src/emails/templates/emailVerification/html.pug @@ -0,0 +1,10 @@ +extend ../layout.pug + +block content + .content + p= t('emailVerification.introduction') + a.button(href=actionUrl)= t('buttons.verifyEmail') + p= t('emailVerification.doNotChange') + + p= t('emailVerification.codeHint') + span= nonce diff --git a/backend/src/emails/templates/emailVerification/subject.pug b/backend/src/emails/templates/emailVerification/subject.pug new file mode 100644 index 000000000..5fc98a7b9 --- /dev/null +++ b/backend/src/emails/templates/emailVerification/subject.pug @@ -0,0 +1 @@ += `${t('subjects.newEmail')} ${APPLICATION_NAME}` \ No newline at end of file diff --git a/backend/src/emails/templates/followed_user_posted/html.pug b/backend/src/emails/templates/followed_user_posted/html.pug new file mode 100644 index 000000000..1b2a0114f --- /dev/null +++ b/backend/src/emails/templates/followed_user_posted/html.pug @@ -0,0 +1,8 @@ +extend ../layout.pug + +block content + .content + p + a.user(href=postAuthorUrl)= postAuthorName + = t('followedUserPosted', { postTitle }) + a.button(href=postUrl)= t('buttons.viewPost') diff --git a/backend/src/emails/templates/followed_user_posted/subject.pug b/backend/src/emails/templates/followed_user_posted/subject.pug new file mode 100644 index 000000000..0da84b83c --- /dev/null +++ b/backend/src/emails/templates/followed_user_posted/subject.pug @@ -0,0 +1 @@ += `${APPLICATION_NAME} – ${t('notification')}: ${t('subjects.followedUserPosted')}` \ No newline at end of file diff --git a/backend/src/emails/templates/includes/footer.pug b/backend/src/emails/templates/includes/footer.pug new file mode 100644 index 000000000..a8deeac84 --- /dev/null +++ b/backend/src/emails/templates/includes/footer.pug @@ -0,0 +1,5 @@ +footer + .footer + - var organizationUrl = ORGANIZATION_URL + - var organizationName = ORGANIZATION_NAME + a(href=organizationUrl)= organizationName \ No newline at end of file diff --git a/backend/src/emails/templates/includes/greeting.pug b/backend/src/emails/templates/includes/greeting.pug new file mode 100644 index 000000000..6b682fc2d --- /dev/null +++ b/backend/src/emails/templates/includes/greeting.pug @@ -0,0 +1,17 @@ +//- This sets the greeting at the end of every e-mail +.text-block + - var organizationUrl = ORGANIZATION_URL + - var team = APPLICATION_NAME + - var settingsUrl = settingsUrl + - var renderSettingsUrl = renderSettingsUrl + p= t('general.seeYou') + a.organization(href=organizationUrl)= team + | ! + p= t('general.yourTeam', { team }) + + if renderSettingsUrl + br + p= t('general.settingsHint') + a.settings(href=settingsUrl)= t('general.settingsName') + | ! + diff --git a/backend/src/emails/templates/includes/header.pug b/backend/src/emails/templates/includes/header.pug new file mode 100644 index 000000000..09b2b07b7 --- /dev/null +++ b/backend/src/emails/templates/includes/header.pug @@ -0,0 +1,9 @@ +header + .head + - var img = welcomeImageUrl + img.head-logo( + alt="Welcome Image" + loading="lazy" + src=img + ) + diff --git a/backend/src/emails/templates/includes/salutation.pug b/backend/src/emails/templates/includes/salutation.pug new file mode 100644 index 000000000..faca3bb64 --- /dev/null +++ b/backend/src/emails/templates/includes/salutation.pug @@ -0,0 +1 @@ +h2= `${t('general.greeting')} ${name},` diff --git a/backend/src/emails/templates/includes/support.pug b/backend/src/emails/templates/includes/support.pug new file mode 100644 index 000000000..49c73e844 --- /dev/null +++ b/backend/src/emails/templates/includes/support.pug @@ -0,0 +1,2 @@ +p= t('support') + a(href='mailto:' + supportEmail)= supportEmail \ No newline at end of file diff --git a/backend/src/emails/templates/includes/webflow.css b/backend/src/emails/templates/includes/webflow.css new file mode 100644 index 000000000..1dc1f0b24 --- /dev/null +++ b/backend/src/emails/templates/includes/webflow.css @@ -0,0 +1,69 @@ +body{ + display: block; + font-family: Lato, sans-serif; + font-size: 17px; + text-align: left; + text-align: -webkit-left; + justify-content: center; + padding: 15px; + margin: 0px; +} + +h2 { + margin-top: 25px; + font-size: 25px; + font-weight: normal; + line-height: 22px; + color: #333333; +} + +.container { + max-width: 680px; + margin: 0 auto; + display: block; +} + +.head-logo { + width: 60%; + height: auto; + display: block; + margin-left: auto; + margin-right: auto; +} + +a { + color: #17b53e; +} + +a.button { + background: #17b53e; + font-family: Lato, sans-serif; + font-size: 16px; + line-height: 15px; + text-decoration: none; + text-align:center; + padding: 13px 17px; + color: #ffffff; + display: table; + margin-left: auto; + margin-right: auto; + border-radius: 4px; +} + +span { + color: #17b53e; +} + +.text-block { + margin-top: 20px; + color: #000000; +} + +footer { + padding: 20px; + font-family: Lato, sans-serif; + font-size: 12px; + line-height: 15px; + text-align: center; + color: #888888; +} diff --git a/backend/src/emails/templates/includes/welcome.pug b/backend/src/emails/templates/includes/welcome.pug new file mode 100644 index 000000000..f4ec6f8bd --- /dev/null +++ b/backend/src/emails/templates/includes/welcome.pug @@ -0,0 +1 @@ +h2= `${t('general.welcome')} ${APPLICATION_NAME}!` \ No newline at end of file diff --git a/backend/src/emails/templates/layout.pug b/backend/src/emails/templates/layout.pug new file mode 100644 index 000000000..4614f97be --- /dev/null +++ b/backend/src/emails/templates/layout.pug @@ -0,0 +1,35 @@ +doctype html +html(lang=locale) + head + meta( + content="multipart/html; charset=UTF-8" + http-equiv="content-type" + ) + meta( + name="viewport" + content="width=device-width, initial-scale=1" + ) + style. + .wf-force-outline-none[tabindex="-1"]:focus{outline:none;} + style + include includes/webflow.css + + - var name = name + body + div.container + include includes/header.pug + if name + include includes/salutation.pug + else + include includes/welcome.pug + + .wrapper + block content + include includes/greeting.pug + + - var supportEmail = SUPPORT_EMAIL + if supportEmail + .support + include includes/support.pug + + include includes/footer.pug diff --git a/backend/src/emails/templates/mentioned_in_comment/html.pug b/backend/src/emails/templates/mentioned_in_comment/html.pug new file mode 100644 index 000000000..a7b9be1de --- /dev/null +++ b/backend/src/emails/templates/mentioned_in_comment/html.pug @@ -0,0 +1,8 @@ +extend ../layout.pug + +block content + .content + p + a.user(href=commenterUrl)= commenterName + = t('mentionedInComment', { postTitle}) + a.button(href=commentUrl)= t('buttons.viewComment') diff --git a/backend/src/emails/templates/mentioned_in_comment/subject.pug b/backend/src/emails/templates/mentioned_in_comment/subject.pug new file mode 100644 index 000000000..70d094e59 --- /dev/null +++ b/backend/src/emails/templates/mentioned_in_comment/subject.pug @@ -0,0 +1 @@ += `${APPLICATION_NAME} – ${t('notification')}: ${t('subjects.mentionedInComment')}` \ No newline at end of file diff --git a/backend/src/emails/templates/mentioned_in_post/html.pug b/backend/src/emails/templates/mentioned_in_post/html.pug new file mode 100644 index 000000000..5a31c7258 --- /dev/null +++ b/backend/src/emails/templates/mentioned_in_post/html.pug @@ -0,0 +1,8 @@ +extend ../layout.pug + +block content + .content + p + a.user(href=postAuthorUrl)= postAuthorName + = t('mentionedInPost', { postTitle }) + a.button(href=postUrl)= t('buttons.viewPost') diff --git a/backend/src/emails/templates/mentioned_in_post/subject.pug b/backend/src/emails/templates/mentioned_in_post/subject.pug new file mode 100644 index 000000000..c318630a3 --- /dev/null +++ b/backend/src/emails/templates/mentioned_in_post/subject.pug @@ -0,0 +1 @@ += `${APPLICATION_NAME} – ${t('notification')}: ${t('subjects.mentionedInPost')}` \ No newline at end of file diff --git a/backend/src/emails/templates/post_in_group/html.pug b/backend/src/emails/templates/post_in_group/html.pug new file mode 100644 index 000000000..bc69ed2e9 --- /dev/null +++ b/backend/src/emails/templates/post_in_group/html.pug @@ -0,0 +1,7 @@ +extend ../layout.pug + +block content + .content + - var postUrl = postUrl + p= t('postInGroup', { postTitle}) + a.button(href=postUrl)= t('buttons.viewPost') diff --git a/backend/src/emails/templates/post_in_group/subject.pug b/backend/src/emails/templates/post_in_group/subject.pug new file mode 100644 index 000000000..1f989190d --- /dev/null +++ b/backend/src/emails/templates/post_in_group/subject.pug @@ -0,0 +1 @@ += `${APPLICATION_NAME} – ${t('notification')}: ${t('subjects.postInGroup')}` \ No newline at end of file diff --git a/backend/src/emails/templates/registration/html.pug b/backend/src/emails/templates/registration/html.pug new file mode 100644 index 000000000..b50aaca31 --- /dev/null +++ b/backend/src/emails/templates/registration/html.pug @@ -0,0 +1,15 @@ +extend ../layout.pug + +block content + .content + p= t('registration.introduction') + a.button(href=actionUrl)= t('buttons.confirmEmail') + p= t('registration.codeHint') + span= nonce + p= t('registration.codeHintException') + + p= t('registration.notYouStart') + a(href=ORGANIZATION_LINK)= APPLICATION_NAME + = t('registration.notYouEnd') + + p= t('registration.ps') \ No newline at end of file diff --git a/backend/src/emails/templates/registration/subject.pug b/backend/src/emails/templates/registration/subject.pug new file mode 100644 index 000000000..7e9dbec7f --- /dev/null +++ b/backend/src/emails/templates/registration/subject.pug @@ -0,0 +1 @@ += `${t('general.welcome')} ${APPLICATION_NAME}` \ No newline at end of file diff --git a/backend/src/emails/templates/removed_user_from_group/html.pug b/backend/src/emails/templates/removed_user_from_group/html.pug new file mode 100644 index 000000000..cb991540e --- /dev/null +++ b/backend/src/emails/templates/removed_user_from_group/html.pug @@ -0,0 +1,5 @@ +extend ../layout.pug + +block content + .content + p= t('removedUserFromGroup', { groupName }) diff --git a/backend/src/emails/templates/removed_user_from_group/subject.pug b/backend/src/emails/templates/removed_user_from_group/subject.pug new file mode 100644 index 000000000..c70855f62 --- /dev/null +++ b/backend/src/emails/templates/removed_user_from_group/subject.pug @@ -0,0 +1 @@ += `${APPLICATION_NAME} – ${t('notification')}: ${t('subjects.removedUserFromGroup')}` \ No newline at end of file diff --git a/backend/src/emails/templates/resetPassword/html.pug b/backend/src/emails/templates/resetPassword/html.pug new file mode 100644 index 000000000..f10ee01c2 --- /dev/null +++ b/backend/src/emails/templates/resetPassword/html.pug @@ -0,0 +1,9 @@ +extend ../layout.pug + +block content + .content + p= t('resetPassword.introduction') + a.button(href=actionUrl)= t('buttons.confirmEmail') + p= t('resetPassword.ignore') + p= t('resetPassword.codeHint') + span= nonce diff --git a/backend/src/emails/templates/resetPassword/subject.pug b/backend/src/emails/templates/resetPassword/subject.pug new file mode 100644 index 000000000..047af2052 --- /dev/null +++ b/backend/src/emails/templates/resetPassword/subject.pug @@ -0,0 +1 @@ += `${t('subjects.resetPassword')} ${APPLICATION_NAME}` \ No newline at end of file diff --git a/backend/src/emails/templates/user_joined_group/html.pug b/backend/src/emails/templates/user_joined_group/html.pug new file mode 100644 index 000000000..00bc116a8 --- /dev/null +++ b/backend/src/emails/templates/user_joined_group/html.pug @@ -0,0 +1,8 @@ +extend ../layout.pug + +block content + .content + p + a.user(href=groupRelatedUserUrl)= groupRelatedUserName + = t('userJoinedGroup', { groupName }) + a.button(href=groupUrl)= t('buttons.viewGroup') diff --git a/backend/src/emails/templates/user_joined_group/subject.pug b/backend/src/emails/templates/user_joined_group/subject.pug new file mode 100644 index 000000000..4e2cae4a1 --- /dev/null +++ b/backend/src/emails/templates/user_joined_group/subject.pug @@ -0,0 +1 @@ += `${APPLICATION_NAME} – ${t('notification')}: ${t('subjects.userJoinedGroup')}` \ No newline at end of file diff --git a/backend/src/emails/templates/user_left_group/html.pug b/backend/src/emails/templates/user_left_group/html.pug new file mode 100644 index 000000000..73374e464 --- /dev/null +++ b/backend/src/emails/templates/user_left_group/html.pug @@ -0,0 +1,8 @@ +extend ../layout.pug + +block content + .content + p + a.user(href=groupRelatedUserUrl)= groupRelatedUserName + = t('userLeftGroup', { groupName }) + a.button(href=groupUrl)= t('buttons.viewGroup') diff --git a/backend/src/emails/templates/user_left_group/subject.pug b/backend/src/emails/templates/user_left_group/subject.pug new file mode 100644 index 000000000..52aa6f1a6 --- /dev/null +++ b/backend/src/emails/templates/user_left_group/subject.pug @@ -0,0 +1 @@ += `${APPLICATION_NAME} – ${t('notification')}: ${t('subjects.userLeftGroup')}` \ No newline at end of file diff --git a/backend/src/emails/templates/wrongEmail/html.pug b/backend/src/emails/templates/wrongEmail/html.pug new file mode 100644 index 000000000..79f97833f --- /dev/null +++ b/backend/src/emails/templates/wrongEmail/html.pug @@ -0,0 +1,10 @@ +extend ../layout.pug + +block content + .content + p= t('wrongEmail.introduction') + a.button(href=actionUrl)= t('buttons.tryAgain') + + p= t('wrongEmail.ignoreStart') + a(href=ORGANIZATION_LINK)= APPLICATION_NAME + = t('wrongEmail.ignoreEnd') diff --git a/backend/src/emails/templates/wrongEmail/subject.pug b/backend/src/emails/templates/wrongEmail/subject.pug new file mode 100644 index 000000000..b6bc2d01c --- /dev/null +++ b/backend/src/emails/templates/wrongEmail/subject.pug @@ -0,0 +1 @@ += `${t('subjects.wrongEmail')} ${APPLICATION_NAME}` \ No newline at end of file diff --git a/backend/src/graphql/groups.ts b/backend/src/graphql/groups.ts deleted file mode 100644 index a7cfc3351..000000000 --- a/backend/src/graphql/groups.ts +++ /dev/null @@ -1,216 +0,0 @@ -import gql from 'graphql-tag' - -// ------ mutations - -export const createGroupMutation = () => { - return gql` - mutation ( - $id: ID - $name: String! - $slug: String - $about: String - $description: String! - $groupType: GroupType! - $actionRadius: GroupActionRadius! - $categoryIds: [ID] - $locationName: String # empty string '' sets it to null - ) { - CreateGroup( - id: $id - name: $name - slug: $slug - about: $about - description: $description - groupType: $groupType - actionRadius: $actionRadius - categoryIds: $categoryIds - locationName: $locationName - ) { - id - name - slug - createdAt - updatedAt - disabled - deleted - about - description - descriptionExcerpt - groupType - actionRadius - categories { - id - slug - name - icon - } - locationName - location { - name - nameDE - nameEN - } - myRole - } - } - ` -} - -export const updateGroupMutation = () => { - return gql` - mutation ( - $id: ID! - $name: String - $slug: String - $about: String - $description: String - $actionRadius: GroupActionRadius - $categoryIds: [ID] - $avatar: ImageInput - $locationName: String # empty string '' sets it to null - ) { - UpdateGroup( - id: $id - name: $name - slug: $slug - about: $about - description: $description - actionRadius: $actionRadius - categoryIds: $categoryIds - avatar: $avatar - locationName: $locationName - ) { - id - name - slug - createdAt - updatedAt - disabled - deleted - about - description - descriptionExcerpt - groupType - actionRadius - categories { - id - slug - name - icon - } - # avatar # test this as result - locationName - location { - name - nameDE - nameEN - } - myRole - } - } - ` -} - -export const joinGroupMutation = () => { - return gql` - mutation ($groupId: ID!, $userId: ID!) { - JoinGroup(groupId: $groupId, userId: $userId) { - id - name - slug - myRoleInGroup - } - } - ` -} - -export const leaveGroupMutation = () => { - return gql` - mutation ($groupId: ID!, $userId: ID!) { - LeaveGroup(groupId: $groupId, userId: $userId) { - id - name - slug - myRoleInGroup - } - } - ` -} - -export const changeGroupMemberRoleMutation = () => { - return gql` - mutation ($groupId: ID!, $userId: ID!, $roleInGroup: GroupMemberRole!) { - ChangeGroupMemberRole(groupId: $groupId, userId: $userId, roleInGroup: $roleInGroup) { - id - name - slug - myRoleInGroup - } - } - ` -} - -export const removeUserFromGroupMutation = () => { - return gql` - mutation ($groupId: ID!, $userId: ID!) { - RemoveUserFromGroup(groupId: $groupId, userId: $userId) { - id - name - slug - myRoleInGroup - } - } - ` -} - -// ------ queries - -export const groupQuery = () => { - return gql` - query ($isMember: Boolean, $id: ID, $slug: String) { - Group(isMember: $isMember, id: $id, slug: $slug) { - id - name - slug - createdAt - updatedAt - disabled - deleted - about - description - descriptionExcerpt - groupType - actionRadius - categories { - id - slug - name - icon - } - avatar { - url - } - locationName - location { - name - nameDE - nameEN - } - myRole - } - } - ` -} - -export const groupMembersQuery = () => { - return gql` - query ($id: ID!) { - GroupMembers(id: $id) { - id - name - slug - myRoleInGroup - } - } - ` -} diff --git a/backend/src/graphql/messages.ts b/backend/src/graphql/messages.ts deleted file mode 100644 index 2842c7230..000000000 --- a/backend/src/graphql/messages.ts +++ /dev/null @@ -1,50 +0,0 @@ -import gql from 'graphql-tag' - -export const createMessageMutation = () => { - return gql` - mutation ($roomId: ID!, $content: String!) { - CreateMessage(roomId: $roomId, content: $content) { - id - content - senderId - username - avatar - date - saved - distributed - seen - } - } - ` -} - -export const messageQuery = () => { - return gql` - query ($roomId: ID!, $first: Int, $offset: Int) { - Message(roomId: $roomId, first: $first, offset: $offset, orderBy: indexId_desc) { - _id - id - indexId - content - senderId - author { - id - } - username - avatar - date - saved - distributed - seen - } - } - ` -} - -export const markMessagesAsSeen = () => { - return gql` - mutation ($messageIds: [String!]) { - MarkMessagesAsSeen(messageIds: $messageIds) - } - ` -} diff --git a/backend/src/graphql/notifications.ts b/backend/src/graphql/notifications.ts deleted file mode 100644 index 233077372..000000000 --- a/backend/src/graphql/notifications.ts +++ /dev/null @@ -1,65 +0,0 @@ -import gql from 'graphql-tag' - -// ------ mutations - -export const markAsReadMutation = () => { - return gql` - mutation ($id: ID!) { - markAsRead(id: $id) { - from { - __typename - ... on Post { - content - } - ... on Comment { - content - } - } - read - createdAt - } - } - ` -} - -export const markAllAsReadMutation = () => { - return gql` - mutation { - markAllAsRead { - from { - __typename - ... on Post { - content - } - ... on Comment { - content - } - } - read - createdAt - } - } - ` -} - -// ------ queries - -export const notificationQuery = () => { - return gql` - query ($read: Boolean, $orderBy: NotificationOrdering) { - notifications(read: $read, orderBy: $orderBy) { - from { - __typename - ... on Post { - content - } - ... on Comment { - content - } - } - read - createdAt - } - } - ` -} diff --git a/backend/src/graphql/posts.ts b/backend/src/graphql/posts.ts deleted file mode 100644 index dcd75a4ff..000000000 --- a/backend/src/graphql/posts.ts +++ /dev/null @@ -1,113 +0,0 @@ -import gql from 'graphql-tag' - -// ------ mutations - -export const createPostMutation = () => { - return gql` - mutation ( - $id: ID - $title: String! - $slug: String - $content: String! - $categoryIds: [ID] - $groupId: ID - $postType: PostType - $eventInput: _EventInput - ) { - CreatePost( - id: $id - title: $title - slug: $slug - content: $content - categoryIds: $categoryIds - groupId: $groupId - postType: $postType - eventInput: $eventInput - ) { - id - slug - title - content - disabled - deleted - postType - author { - name - } - categories { - id - } - eventStart - eventEnd - eventLocationName - eventVenue - eventIsOnline - eventLocation { - lng - lat - } - isObservedByMe - observingUsersCount - } - } - ` -} - -// ------ queries - -export const postQuery = () => { - return gql` - query Post($id: ID!) { - Post(id: $id) { - id - title - content - } - } - ` -} - -export const filterPosts = () => { - return gql` - query Post($filter: _PostFilter, $first: Int, $offset: Int, $orderBy: [_PostOrdering]) { - Post(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) { - id - title - content - eventStart - } - } - ` -} - -export const profilePagePosts = () => { - return gql` - query profilePagePosts( - $filter: _PostFilter - $first: Int - $offset: Int - $orderBy: [_PostOrdering] - ) { - profilePagePosts(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) { - id - title - content - } - } - ` -} - -export const searchPosts = () => { - return gql` - query ($query: String!, $firstPosts: Int, $postsOffset: Int) { - searchPosts(query: $query, firstPosts: $firstPosts, postsOffset: $postsOffset) { - postCount - posts { - id - title - content - } - } - } - ` -} diff --git a/backend/src/graphql/queries/Group.ts b/backend/src/graphql/queries/Group.ts new file mode 100644 index 000000000..b6009ddc1 --- /dev/null +++ b/backend/src/graphql/queries/Group.ts @@ -0,0 +1,40 @@ +import gql from 'graphql-tag' + +export const Group = gql` + query Group($isMember: Boolean, $id: ID, $slug: String) { + Group(isMember: $isMember, id: $id, slug: $slug) { + id + name + slug + createdAt + updatedAt + disabled + deleted + about + description + descriptionExcerpt + groupType + actionRadius + categories { + id + slug + name + icon + } + avatar { + url + } + locationName + location { + name + nameDE + nameEN + } + myRole + inviteCodes { + code + redeemedByCount + } + } + } +` diff --git a/backend/src/graphql/queries/GroupMembers.ts b/backend/src/graphql/queries/GroupMembers.ts new file mode 100644 index 000000000..5950952cb --- /dev/null +++ b/backend/src/graphql/queries/GroupMembers.ts @@ -0,0 +1,12 @@ +import gql from 'graphql-tag' + +export const GroupMembers = gql` + query GroupMembers($id: ID!) { + GroupMembers(id: $id) { + id + name + slug + myRoleInGroup + } + } +` diff --git a/backend/src/graphql/queries/Post.ts b/backend/src/graphql/queries/Post.ts new file mode 100644 index 000000000..f737bac86 --- /dev/null +++ b/backend/src/graphql/queries/Post.ts @@ -0,0 +1,12 @@ +import gql from 'graphql-tag' + +export const Post = gql` + query ($orderBy: [_PostOrdering]) { + Post(orderBy: $orderBy) { + id + pinned + createdAt + pinnedAt + } + } +` diff --git a/backend/src/graphql/queries/changeGroupMemberRoleMutation.ts b/backend/src/graphql/queries/changeGroupMemberRoleMutation.ts new file mode 100644 index 000000000..a01c19cfb --- /dev/null +++ b/backend/src/graphql/queries/changeGroupMemberRoleMutation.ts @@ -0,0 +1,14 @@ +import gql from 'graphql-tag' + +export const changeGroupMemberRoleMutation = () => { + return gql` + mutation ($groupId: ID!, $userId: ID!, $roleInGroup: GroupMemberRole!) { + ChangeGroupMemberRole(groupId: $groupId, userId: $userId, roleInGroup: $roleInGroup) { + id + name + slug + myRoleInGroup + } + } + ` +} diff --git a/backend/src/graphql/comments.ts b/backend/src/graphql/queries/createCommentMutation.ts similarity index 76% rename from backend/src/graphql/comments.ts rename to backend/src/graphql/queries/createCommentMutation.ts index b408c5e95..c3824e1d0 100644 --- a/backend/src/graphql/comments.ts +++ b/backend/src/graphql/queries/createCommentMutation.ts @@ -1,7 +1,5 @@ import gql from 'graphql-tag' -// ------ mutations - export const createCommentMutation = gql` mutation ($id: ID, $postId: ID!, $content: String!) { CreateComment(id: $id, postId: $postId, content: $content) { @@ -9,7 +7,3 @@ export const createCommentMutation = gql` } } ` - -// ------ queries - -// fill queries in here diff --git a/backend/src/graphql/queries/createGroupMutation.ts b/backend/src/graphql/queries/createGroupMutation.ts new file mode 100644 index 000000000..20cd93323 --- /dev/null +++ b/backend/src/graphql/queries/createGroupMutation.ts @@ -0,0 +1,55 @@ +import gql from 'graphql-tag' + +export const createGroupMutation = () => { + return gql` + mutation ( + $id: ID + $name: String! + $slug: String + $about: String + $description: String! + $groupType: GroupType! + $actionRadius: GroupActionRadius! + $categoryIds: [ID] + $locationName: String # empty string '' sets it to null + ) { + CreateGroup( + id: $id + name: $name + slug: $slug + about: $about + description: $description + groupType: $groupType + actionRadius: $actionRadius + categoryIds: $categoryIds + locationName: $locationName + ) { + id + name + slug + createdAt + updatedAt + disabled + deleted + about + description + descriptionExcerpt + groupType + actionRadius + categories { + id + slug + name + icon + } + locationName + location { + name + nameDE + nameEN + } + myRole + } + } + ` +} diff --git a/backend/src/graphql/queries/createMessageMutation.ts b/backend/src/graphql/queries/createMessageMutation.ts new file mode 100644 index 000000000..e8c6fc7b8 --- /dev/null +++ b/backend/src/graphql/queries/createMessageMutation.ts @@ -0,0 +1,19 @@ +import gql from 'graphql-tag' + +export const createMessageMutation = () => { + return gql` + mutation ($roomId: ID!, $content: String!) { + CreateMessage(roomId: $roomId, content: $content) { + id + content + senderId + username + avatar + date + saved + distributed + seen + } + } + ` +} diff --git a/backend/src/graphql/queries/createPostMutation.ts b/backend/src/graphql/queries/createPostMutation.ts new file mode 100644 index 000000000..f0a01b303 --- /dev/null +++ b/backend/src/graphql/queries/createPostMutation.ts @@ -0,0 +1,52 @@ +import gql from 'graphql-tag' + +export const createPostMutation = () => { + return gql` + mutation ( + $id: ID + $title: String! + $slug: String + $content: String! + $categoryIds: [ID] + $groupId: ID + $postType: PostType + $eventInput: _EventInput + ) { + CreatePost( + id: $id + title: $title + slug: $slug + content: $content + categoryIds: $categoryIds + groupId: $groupId + postType: $postType + eventInput: $eventInput + ) { + id + slug + title + content + disabled + deleted + postType + author { + name + } + categories { + id + } + eventStart + eventEnd + eventLocationName + eventVenue + eventIsOnline + eventLocation { + lng + lat + } + isObservedByMe + observingUsersCount + } + } + ` +} diff --git a/backend/src/graphql/queries/createRoomMutation.ts b/backend/src/graphql/queries/createRoomMutation.ts new file mode 100644 index 000000000..3a791d294 --- /dev/null +++ b/backend/src/graphql/queries/createRoomMutation.ts @@ -0,0 +1,24 @@ +import gql from 'graphql-tag' + +export const createRoomMutation = () => { + return gql` + mutation ($userId: ID!) { + CreateRoom(userId: $userId) { + id + roomId + roomName + lastMessageAt + unreadCount + #avatar + users { + _id + id + name + avatar { + url + } + } + } + } + ` +} diff --git a/backend/src/graphql/queries/currentUser.ts b/backend/src/graphql/queries/currentUser.ts new file mode 100644 index 000000000..753fe5288 --- /dev/null +++ b/backend/src/graphql/queries/currentUser.ts @@ -0,0 +1,15 @@ +import gql from 'graphql-tag' + +export const currentUser = gql` + query currentUser { + currentUser { + following { + name + } + inviteCodes { + code + redeemedByCount + } + } + } +` diff --git a/backend/src/graphql/queries/filterPosts.ts b/backend/src/graphql/queries/filterPosts.ts new file mode 100644 index 000000000..7e6d5059f --- /dev/null +++ b/backend/src/graphql/queries/filterPosts.ts @@ -0,0 +1,14 @@ +import gql from 'graphql-tag' + +export const filterPosts = () => { + return gql` + query Post($filter: _PostFilter, $first: Int, $offset: Int, $orderBy: [_PostOrdering]) { + Post(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) { + id + title + content + eventStart + } + } + ` +} diff --git a/backend/src/graphql/queries/generateGroupInviteCode.ts b/backend/src/graphql/queries/generateGroupInviteCode.ts new file mode 100644 index 000000000..5633b41b7 --- /dev/null +++ b/backend/src/graphql/queries/generateGroupInviteCode.ts @@ -0,0 +1,36 @@ +import gql from 'graphql-tag' + +export const generateGroupInviteCode = gql` + mutation generateGroupInviteCode($groupId: ID!, $expiresAt: String, $comment: String) { + generateGroupInviteCode(groupId: $groupId, expiresAt: $expiresAt, comment: $comment) { + code + createdAt + generatedBy { + id + name + avatar { + url + } + } + redeemedBy { + id + name + avatar { + url + } + } + expiresAt + comment + invitedTo { + id + groupType + name + about + avatar { + url + } + } + isValid + } + } +` diff --git a/backend/src/graphql/queries/generatePersonalInviteCode.ts b/backend/src/graphql/queries/generatePersonalInviteCode.ts new file mode 100644 index 000000000..429b25549 --- /dev/null +++ b/backend/src/graphql/queries/generatePersonalInviteCode.ts @@ -0,0 +1,36 @@ +import gql from 'graphql-tag' + +export const generatePersonalInviteCode = gql` + mutation generatePersonalInviteCode($expiresAt: String, $comment: String) { + generatePersonalInviteCode(expiresAt: $expiresAt, comment: $comment) { + code + createdAt + generatedBy { + id + name + avatar { + url + } + } + redeemedBy { + id + name + avatar { + url + } + } + expiresAt + comment + invitedTo { + id + groupType + name + about + avatar { + url + } + } + isValid + } + } +` diff --git a/backend/src/graphql/queries/invalidateInviteCode.ts b/backend/src/graphql/queries/invalidateInviteCode.ts new file mode 100644 index 000000000..1b8581be3 --- /dev/null +++ b/backend/src/graphql/queries/invalidateInviteCode.ts @@ -0,0 +1,36 @@ +import gql from 'graphql-tag' + +export const invalidateInviteCode = gql` + mutation invalidateInviteCode($code: String!) { + invalidateInviteCode(code: $code) { + code + createdAt + generatedBy { + id + name + avatar { + url + } + } + redeemedBy { + id + name + avatar { + url + } + } + expiresAt + comment + invitedTo { + id + groupType + name + about + avatar { + url + } + } + isValid + } + } +` diff --git a/backend/src/graphql/queries/joinGroupMutation.ts b/backend/src/graphql/queries/joinGroupMutation.ts new file mode 100644 index 000000000..ce627b1ef --- /dev/null +++ b/backend/src/graphql/queries/joinGroupMutation.ts @@ -0,0 +1,14 @@ +import gql from 'graphql-tag' + +export const joinGroupMutation = () => { + return gql` + mutation ($groupId: ID!, $userId: ID!) { + JoinGroup(groupId: $groupId, userId: $userId) { + id + name + slug + myRoleInGroup + } + } + ` +} diff --git a/backend/src/graphql/queries/leaveGroupMutation.ts b/backend/src/graphql/queries/leaveGroupMutation.ts new file mode 100644 index 000000000..470bd6a7a --- /dev/null +++ b/backend/src/graphql/queries/leaveGroupMutation.ts @@ -0,0 +1,14 @@ +import gql from 'graphql-tag' + +export const leaveGroupMutation = () => { + return gql` + mutation ($groupId: ID!, $userId: ID!) { + LeaveGroup(groupId: $groupId, userId: $userId) { + id + name + slug + myRoleInGroup + } + } + ` +} diff --git a/backend/src/graphql/userManagement.ts b/backend/src/graphql/queries/loginMutation.ts similarity index 72% rename from backend/src/graphql/userManagement.ts rename to backend/src/graphql/queries/loginMutation.ts index 3cb8a05f8..8c7b36f12 100644 --- a/backend/src/graphql/userManagement.ts +++ b/backend/src/graphql/queries/loginMutation.ts @@ -1,13 +1,7 @@ import gql from 'graphql-tag' -// ------ mutations - export const loginMutation = gql` mutation ($email: String!, $password: String!) { login(email: $email, password: $password) } ` - -// ------ queries - -// fill queries in here diff --git a/backend/src/graphql/queries/markAllAsReadMutation.ts b/backend/src/graphql/queries/markAllAsReadMutation.ts new file mode 100644 index 000000000..d1f19e369 --- /dev/null +++ b/backend/src/graphql/queries/markAllAsReadMutation.ts @@ -0,0 +1,21 @@ +import gql from 'graphql-tag' + +export const markAllAsReadMutation = () => { + return gql` + mutation { + markAllAsRead { + from { + __typename + ... on Post { + content + } + ... on Comment { + content + } + } + read + createdAt + } + } + ` +} diff --git a/backend/src/graphql/queries/markAsReadMutation.ts b/backend/src/graphql/queries/markAsReadMutation.ts new file mode 100644 index 000000000..fd855665a --- /dev/null +++ b/backend/src/graphql/queries/markAsReadMutation.ts @@ -0,0 +1,21 @@ +import gql from 'graphql-tag' + +export const markAsReadMutation = () => { + return gql` + mutation ($id: ID!) { + markAsRead(id: $id) { + from { + __typename + ... on Post { + content + } + ... on Comment { + content + } + } + read + createdAt + } + } + ` +} diff --git a/backend/src/graphql/queries/markMessagesAsSeen.ts b/backend/src/graphql/queries/markMessagesAsSeen.ts new file mode 100644 index 000000000..9081c5def --- /dev/null +++ b/backend/src/graphql/queries/markMessagesAsSeen.ts @@ -0,0 +1,9 @@ +import gql from 'graphql-tag' + +export const markMessagesAsSeen = () => { + return gql` + mutation ($messageIds: [String!]) { + MarkMessagesAsSeen(messageIds: $messageIds) + } + ` +} diff --git a/backend/src/graphql/queries/messageQuery.ts b/backend/src/graphql/queries/messageQuery.ts new file mode 100644 index 000000000..791851121 --- /dev/null +++ b/backend/src/graphql/queries/messageQuery.ts @@ -0,0 +1,24 @@ +import gql from 'graphql-tag' + +export const messageQuery = () => { + return gql` + query ($roomId: ID!, $first: Int, $offset: Int) { + Message(roomId: $roomId, first: $first, offset: $offset, orderBy: indexId_desc) { + _id + id + indexId + content + senderId + author { + id + } + username + avatar + date + saved + distributed + seen + } + } + ` +} diff --git a/backend/src/graphql/queries/notificationQuery.ts b/backend/src/graphql/queries/notificationQuery.ts new file mode 100644 index 000000000..965fb9ce9 --- /dev/null +++ b/backend/src/graphql/queries/notificationQuery.ts @@ -0,0 +1,21 @@ +import gql from 'graphql-tag' + +export const notificationQuery = () => { + return gql` + query ($read: Boolean, $orderBy: NotificationOrdering) { + notifications(read: $read, orderBy: $orderBy) { + from { + __typename + ... on Post { + content + } + ... on Comment { + content + } + } + read + createdAt + } + } + ` +} diff --git a/backend/src/graphql/queries/postQuery.ts b/backend/src/graphql/queries/postQuery.ts new file mode 100644 index 000000000..ff8faf311 --- /dev/null +++ b/backend/src/graphql/queries/postQuery.ts @@ -0,0 +1,13 @@ +import gql from 'graphql-tag' + +export const postQuery = () => { + return gql` + query Post($id: ID!) { + Post(id: $id) { + id + title + content + } + } + ` +} diff --git a/backend/src/graphql/queries/profilePagePosts.ts b/backend/src/graphql/queries/profilePagePosts.ts new file mode 100644 index 000000000..5d713a23c --- /dev/null +++ b/backend/src/graphql/queries/profilePagePosts.ts @@ -0,0 +1,18 @@ +import gql from 'graphql-tag' + +export const profilePagePosts = () => { + return gql` + query profilePagePosts( + $filter: _PostFilter + $first: Int + $offset: Int + $orderBy: [_PostOrdering] + ) { + profilePagePosts(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) { + id + title + content + } + } + ` +} diff --git a/backend/src/graphql/queries/pushPost.ts b/backend/src/graphql/queries/pushPost.ts new file mode 100644 index 000000000..56568188a --- /dev/null +++ b/backend/src/graphql/queries/pushPost.ts @@ -0,0 +1,9 @@ +import gql from 'graphql-tag' + +export const pushPost = gql` + mutation pushPost($id: ID!) { + pushPost(id: $id) { + id + } + } +` diff --git a/backend/src/graphql/queries/redeemInviteCode.ts b/backend/src/graphql/queries/redeemInviteCode.ts new file mode 100644 index 000000000..0852c564a --- /dev/null +++ b/backend/src/graphql/queries/redeemInviteCode.ts @@ -0,0 +1,7 @@ +import gql from 'graphql-tag' + +export const redeemInviteCode = gql` + mutation redeemInviteCode($code: String!) { + redeemInviteCode(code: $code) + } +` diff --git a/backend/src/graphql/queries/removeUserFromGroupMutation.ts b/backend/src/graphql/queries/removeUserFromGroupMutation.ts new file mode 100644 index 000000000..bdb9792d9 --- /dev/null +++ b/backend/src/graphql/queries/removeUserFromGroupMutation.ts @@ -0,0 +1,14 @@ +import gql from 'graphql-tag' + +export const removeUserFromGroupMutation = () => { + return gql` + mutation ($groupId: ID!, $userId: ID!) { + RemoveUserFromGroup(groupId: $groupId, userId: $userId) { + id + name + slug + myRoleInGroup + } + } + ` +} diff --git a/backend/src/graphql/queries/rewardTrophyBadge.ts b/backend/src/graphql/queries/rewardTrophyBadge.ts new file mode 100644 index 000000000..dda869d7f --- /dev/null +++ b/backend/src/graphql/queries/rewardTrophyBadge.ts @@ -0,0 +1,20 @@ +import gql from 'graphql-tag' + +export const rewardTrophyBadge = gql` + mutation rewardTrophyBadge($badgeId: ID!, $userId: ID!) { + rewardTrophyBadge(badgeId: $badgeId, userId: $userId) { + id + badgeVerification { + id + isDefault + } + badgeTrophiesCount + badgeTrophies { + id + } + badgeTrophiesSelected { + id + } + } + } +` diff --git a/backend/src/graphql/rooms.ts b/backend/src/graphql/queries/roomQuery.ts similarity index 58% rename from backend/src/graphql/rooms.ts rename to backend/src/graphql/queries/roomQuery.ts index 7612641f3..01b24654e 100644 --- a/backend/src/graphql/rooms.ts +++ b/backend/src/graphql/queries/roomQuery.ts @@ -1,28 +1,5 @@ import gql from 'graphql-tag' -export const createRoomMutation = () => { - return gql` - mutation ($userId: ID!) { - CreateRoom(userId: $userId) { - id - roomId - roomName - lastMessageAt - unreadCount - #avatar - users { - _id - id - name - avatar { - url - } - } - } - } - ` -} - export const roomQuery = () => { return gql` query Room($first: Int, $offset: Int, $id: ID) { @@ -57,11 +34,3 @@ export const roomQuery = () => { } ` } - -export const unreadRoomsQuery = () => { - return gql` - query { - UnreadRooms - } - ` -} diff --git a/backend/src/graphql/queries/searchPosts.ts b/backend/src/graphql/queries/searchPosts.ts new file mode 100644 index 000000000..ed9e9a641 --- /dev/null +++ b/backend/src/graphql/queries/searchPosts.ts @@ -0,0 +1,16 @@ +import gql from 'graphql-tag' + +export const searchPosts = () => { + return gql` + query ($query: String!, $firstPosts: Int, $postsOffset: Int) { + searchPosts(query: $query, firstPosts: $firstPosts, postsOffset: $postsOffset) { + postCount + posts { + id + title + content + } + } + } + ` +} diff --git a/backend/src/graphql/queries/setTrophyBadgeSelected.ts b/backend/src/graphql/queries/setTrophyBadgeSelected.ts new file mode 100644 index 000000000..62450809f --- /dev/null +++ b/backend/src/graphql/queries/setTrophyBadgeSelected.ts @@ -0,0 +1,17 @@ +import gql from 'graphql-tag' + +export const setTrophyBadgeSelected = gql` + mutation setTrophyBadgeSelected($slot: Int!, $badgeId: ID) { + setTrophyBadgeSelected(slot: $slot, badgeId: $badgeId) { + badgeTrophiesCount + badgeTrophiesSelected { + id + isDefault + } + badgeTrophiesUnused { + id + } + badgeTrophiesUnusedCount + } + } +` diff --git a/backend/src/graphql/authentications.ts b/backend/src/graphql/queries/signupVerificationMutation.ts similarity index 88% rename from backend/src/graphql/authentications.ts rename to backend/src/graphql/queries/signupVerificationMutation.ts index 91605ec9f..f504da0ce 100644 --- a/backend/src/graphql/authentications.ts +++ b/backend/src/graphql/queries/signupVerificationMutation.ts @@ -1,7 +1,5 @@ import gql from 'graphql-tag' -// ------ mutations - export const signupVerificationMutation = gql` mutation ( $password: String! @@ -24,7 +22,3 @@ export const signupVerificationMutation = gql` } } ` - -// ------ queries - -// fill queries in here diff --git a/backend/src/graphql/queries/statistics.ts b/backend/src/graphql/queries/statistics.ts new file mode 100644 index 000000000..0463b63a4 --- /dev/null +++ b/backend/src/graphql/queries/statistics.ts @@ -0,0 +1,29 @@ +import gql from 'graphql-tag' + +export const statistics = gql` + query statistics { + statistics { + users + usersDeleted + posts + comments + notifications + emails + follows + shouts + invites + chatMessages + chatRooms + tags + locations + groups + inviteCodes + inviteCodesExpired + inviteCodesRedeemed + badgesRewarded + badgesDisplayed + usersVerified + reports + } + } +` diff --git a/backend/src/graphql/queries/unpushPost.ts b/backend/src/graphql/queries/unpushPost.ts new file mode 100644 index 000000000..dcf3ac0c8 --- /dev/null +++ b/backend/src/graphql/queries/unpushPost.ts @@ -0,0 +1,9 @@ +import gql from 'graphql-tag' + +export const unpushPost = gql` + mutation unpushPost($id: ID!) { + unpushPost(id: $id) { + id + } + } +` diff --git a/backend/src/graphql/queries/unreadRoomsQuery.ts b/backend/src/graphql/queries/unreadRoomsQuery.ts new file mode 100644 index 000000000..d5612dcad --- /dev/null +++ b/backend/src/graphql/queries/unreadRoomsQuery.ts @@ -0,0 +1,9 @@ +import gql from 'graphql-tag' + +export const unreadRoomsQuery = () => { + return gql` + query { + UnreadRooms + } + ` +} diff --git a/backend/src/graphql/queries/updateGroupMutation.ts b/backend/src/graphql/queries/updateGroupMutation.ts new file mode 100644 index 000000000..826a9c9d4 --- /dev/null +++ b/backend/src/graphql/queries/updateGroupMutation.ts @@ -0,0 +1,56 @@ +import gql from 'graphql-tag' + +export const updateGroupMutation = () => { + return gql` + mutation ( + $id: ID! + $name: String + $slug: String + $about: String + $description: String + $actionRadius: GroupActionRadius + $categoryIds: [ID] + $avatar: ImageInput + $locationName: String # empty string '' sets it to null + ) { + UpdateGroup( + id: $id + name: $name + slug: $slug + about: $about + description: $description + actionRadius: $actionRadius + categoryIds: $categoryIds + avatar: $avatar + locationName: $locationName + ) { + id + name + slug + createdAt + updatedAt + disabled + deleted + about + description + descriptionExcerpt + groupType + actionRadius + categories { + id + slug + name + icon + } + # avatar # test this as result + locationName + location { + name + nameDE + nameEN + } + myRole + } + } + ` +} diff --git a/backend/src/graphql/queries/validateInviteCode.ts b/backend/src/graphql/queries/validateInviteCode.ts new file mode 100644 index 000000000..bcae09254 --- /dev/null +++ b/backend/src/graphql/queries/validateInviteCode.ts @@ -0,0 +1,49 @@ +import gql from 'graphql-tag' + +export const unauthenticatedValidateInviteCode = gql` + query validateInviteCode($code: String!) { + validateInviteCode(code: $code) { + code + invitedTo { + groupType + name + about + avatar { + url + } + } + generatedBy { + name + avatar { + url + } + } + isValid + } + } +` + +export const authenticatedValidateInviteCode = gql` + query validateInviteCode($code: String!) { + validateInviteCode(code: $code) { + code + invitedTo { + id + groupType + name + about + avatar { + url + } + } + generatedBy { + id + name + avatar { + url + } + } + isValid + } + } +` diff --git a/backend/src/schema/resolvers/Upload.ts b/backend/src/graphql/resolvers/Upload.ts similarity index 100% rename from backend/src/schema/resolvers/Upload.ts rename to backend/src/graphql/resolvers/Upload.ts diff --git a/backend/src/graphql/resolvers/badges.spec.ts b/backend/src/graphql/resolvers/badges.spec.ts new file mode 100644 index 000000000..6ebed7990 --- /dev/null +++ b/backend/src/graphql/resolvers/badges.spec.ts @@ -0,0 +1,1178 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { ApolloServer } from 'apollo-server-express' +import { createTestClient } from 'apollo-server-testing' +import gql from 'graphql-tag' + +import { TROPHY_BADGES_SELECTED_MAX } from '@constants/badges' +import databaseContext from '@context/database' +import Factory, { cleanDatabase } from '@db/factories' +import { rewardTrophyBadge } from '@graphql/queries/rewardTrophyBadge' +import { setTrophyBadgeSelected } from '@graphql/queries/setTrophyBadgeSelected' +import createServer, { getContext } from '@src/server' + +let regularUser, administrator, moderator, badge, verification + +const database = databaseContext() + +let server: ApolloServer +let authenticatedUser +let query, mutate + +beforeAll(async () => { + await cleanDatabase() + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/require-await + const contextUser = async (_req) => authenticatedUser + const context = getContext({ user: contextUser, database }) + + server = createServer({ context }).server + + const createTestClientResult = createTestClient(server) + query = createTestClientResult.query + mutate = createTestClientResult.mutate +}) + +afterAll(() => { + void server.stop() + void database.driver.close() + database.neode.close() +}) + +describe('Badges', () => { + beforeEach(async () => { + regularUser = await Factory.build( + 'user', + { + id: 'regular-user-id', + role: 'user', + }, + { + email: 'user@example.org', + password: '1234', + }, + ) + moderator = await Factory.build( + 'user', + { + id: 'moderator-id', + role: 'moderator', + }, + { + email: 'moderator@example.org', + }, + ) + administrator = await Factory.build( + 'user', + { + id: 'admin-id', + role: 'admin', + }, + { + email: 'admin@example.org', + }, + ) + badge = await Factory.build('badge', { + id: 'trophy_rhino', + type: 'trophy', + description: 'You earned a rhino', + icon: '/img/badges/trophy_blue_rhino.svg', + }) + + verification = await Factory.build('badge', { + id: 'verification_moderator', + type: 'verification', + description: 'You are a moderator', + icon: '/img/badges/verification_red_moderator.svg', + }) + }) + + afterEach(async () => { + await cleanDatabase() + }) + + describe('setVerificationBadge', () => { + const variables = { + badgeId: 'verification_moderator', + userId: 'regular-user-id', + } + + const setVerificationBadgeMutation = gql` + mutation ($badgeId: ID!, $userId: ID!) { + setVerificationBadge(badgeId: $badgeId, userId: $userId) { + id + badgeVerification { + id + isDefault + } + badgeTrophies { + id + } + } + } + ` + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + authenticatedUser = null + await expect( + mutate({ mutation: setVerificationBadgeMutation, variables }), + ).resolves.toMatchObject({ + data: { setVerificationBadge: null }, + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + + describe('authenticated as moderator', () => { + beforeEach(() => { + authenticatedUser = moderator.toJson() + }) + + describe('rewards badge to user', () => { + it('throws authorization error', async () => { + await expect( + mutate({ mutation: setVerificationBadgeMutation, variables }), + ).resolves.toMatchObject({ + data: { setVerificationBadge: null }, + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + }) + + describe('authenticated as admin', () => { + beforeEach(async () => { + authenticatedUser = await administrator.toJson() + }) + + describe('badge for id does not exist', () => { + it('rejects with an informative error message', async () => { + await expect( + mutate({ + mutation: setVerificationBadgeMutation, + variables: { userId: 'regular-user-id', badgeId: 'non-existent-badge-id' }, + }), + ).resolves.toMatchObject({ + data: { setVerificationBadge: null }, + errors: [ + { + message: + 'Error: Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.', + }, + ], + }) + }) + }) + + describe('non-existent user', () => { + it('rejects with a telling error message', async () => { + await expect( + mutate({ + mutation: setVerificationBadgeMutation, + variables: { userId: 'non-existent-user-id', badgeId: 'verification_moderator' }, + }), + ).resolves.toMatchObject({ + data: { setVerificationBadge: null }, + errors: [ + { + message: + 'Error: Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.', + }, + ], + }) + }) + }) + + describe('badge is not a verification badge', () => { + it('rejects with a telling error message', async () => { + await expect( + mutate({ + mutation: setVerificationBadgeMutation, + variables: { userId: 'regular-user-id', badgeId: 'trophy_rhino' }, + }), + ).resolves.toMatchObject({ + data: { setVerificationBadge: null }, + errors: [ + { + message: + 'Error: Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.', + }, + ], + }) + }) + }) + + it('rewards a verification badge to the user', async () => { + const expected = { + data: { + setVerificationBadge: { + id: 'regular-user-id', + badgeVerification: { id: 'verification_moderator', isDefault: false }, + badgeTrophies: [], + }, + }, + errors: undefined, + } + await expect( + mutate({ mutation: setVerificationBadgeMutation, variables }), + ).resolves.toMatchObject(expected) + }) + + it('overrides the existing verification if a second verification badge is rewarded to the same user', async () => { + await Factory.build('badge', { + id: 'verification_admin', + type: 'verification', + description: 'You are an admin', + icon: '/img/badges/verification_red_admin.svg', + }) + const expected = { + data: { + setVerificationBadge: { + id: 'regular-user-id', + badgeVerification: { id: 'verification_admin', isDefault: false }, + badgeTrophies: [], + }, + }, + errors: undefined, + } + await mutate({ + mutation: setVerificationBadgeMutation, + variables: { + userId: 'regular-user-id', + badgeId: 'verification_moderator', + }, + }) + await expect( + mutate({ + mutation: setVerificationBadgeMutation, + variables: { + userId: 'regular-user-id', + badgeId: 'verification_admin', + }, + }), + ).resolves.toMatchObject(expected) + }) + + it('rewards the same verification badge as well to another user', async () => { + const expected = { + data: { + setVerificationBadge: { + id: 'regular-user-2-id', + badgeVerification: { id: 'verification_moderator', isDefault: false }, + badgeTrophies: [], + }, + }, + errors: undefined, + } + await Factory.build( + 'user', + { + id: 'regular-user-2-id', + }, + { + email: 'regular2@email.com', + }, + ) + await mutate({ + mutation: setVerificationBadgeMutation, + variables, + }) + await expect( + mutate({ + mutation: setVerificationBadgeMutation, + variables: { + userId: 'regular-user-2-id', + badgeId: 'verification_moderator', + }, + }), + ).resolves.toMatchObject(expected) + }) + }) + }) + + describe('rewardTrophyBadge', () => { + const variables = { + badgeId: 'trophy_rhino', + userId: 'regular-user-id', + } + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + authenticatedUser = null + await expect(mutate({ mutation: rewardTrophyBadge, variables })).resolves.toMatchObject({ + data: { rewardTrophyBadge: null }, + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + + describe('authenticated as moderator', () => { + beforeEach(() => { + authenticatedUser = moderator.toJson() + }) + + describe('rewards badge to user', () => { + it('throws authorization error', async () => { + await expect(mutate({ mutation: rewardTrophyBadge, variables })).resolves.toMatchObject({ + data: { rewardTrophyBadge: null }, + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + }) + + describe('authenticated as admin', () => { + beforeEach(async () => { + authenticatedUser = await administrator.toJson() + }) + + describe('badge for id does not exist', () => { + it('rejects with an informative error message', async () => { + await expect( + mutate({ + mutation: rewardTrophyBadge, + variables: { userId: 'regular-user-id', badgeId: 'non-existent-badge-id' }, + }), + ).resolves.toMatchObject({ + data: { rewardTrophyBadge: null }, + errors: [ + { + message: + 'Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.', + }, + ], + }) + }) + }) + + describe('non-existent user', () => { + it('rejects with a telling error message', async () => { + await expect( + mutate({ + mutation: rewardTrophyBadge, + variables: { userId: 'non-existent-user-id', badgeId: 'trophy_rhino' }, + }), + ).resolves.toMatchObject({ + data: { rewardTrophyBadge: null }, + errors: [ + { + message: + 'Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.', + }, + ], + }) + }) + }) + + describe('badge is a verification Badge', () => { + it('rejects with a telling error message', async () => { + await expect( + mutate({ + mutation: rewardTrophyBadge, + variables: { userId: 'regular-user-id', badgeId: 'verification_moderator' }, + }), + ).resolves.toMatchObject({ + data: { rewardTrophyBadge: null }, + errors: [ + { + message: + 'Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.', + }, + ], + }) + }) + }) + + it('rewards a badge to the user', async () => { + await expect(mutate({ mutation: rewardTrophyBadge, variables })).resolves.toMatchObject({ + data: { + rewardTrophyBadge: { + id: 'regular-user-id', + badgeVerification: { id: 'default_verification', isDefault: true }, + badgeTrophies: [{ id: 'trophy_rhino' }], + badgeTrophiesSelected: [ + { id: 'trophy_rhino' }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + ], + }, + }, + errors: undefined, + }) + }) + + it('rewards a second different badge to the same user', async () => { + await Factory.build('badge', { + id: 'trophy_racoon', + type: 'trophy', + description: 'You earned a racoon', + icon: '/img/badges/trophy_blue_racoon.svg', + }) + await mutate({ + mutation: rewardTrophyBadge, + variables: { + userId: 'regular-user-id', + badgeId: 'trophy_racoon', + }, + }) + await expect( + mutate({ + mutation: rewardTrophyBadge, + variables: { + userId: 'regular-user-id', + badgeId: 'trophy_rhino', + }, + }), + ).resolves.toMatchObject({ + data: { + rewardTrophyBadge: { + id: 'regular-user-id', + badgeTrophies: expect.arrayContaining([ + { id: 'trophy_racoon' }, + { id: 'trophy_rhino' }, + ]), + badgeTrophiesSelected: [ + { id: 'trophy_racoon' }, + { + id: 'trophy_rhino', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + ], + }, + }, + errors: undefined, + }) + }) + + it('does not select a badge again when already rewarded and unselected by the user', async () => { + await Factory.build('badge', { + id: 'trophy_racoon', + type: 'trophy', + description: 'You earned a racoon', + icon: '/img/badges/trophy_blue_racoon.svg', + }) + await mutate({ + mutation: rewardTrophyBadge, + variables: { + userId: 'regular-user-id', + badgeId: 'trophy_rhino', + }, + }) + await mutate({ + mutation: rewardTrophyBadge, + variables: { + userId: 'regular-user-id', + badgeId: 'trophy_racoon', + }, + }) + authenticatedUser = await regularUser.toJson() + await mutate({ + mutation: setTrophyBadgeSelected, + variables: { + slot: 0, + badgeId: null, + }, + }) + authenticatedUser = await administrator.toJson() + await expect( + mutate({ + mutation: rewardTrophyBadge, + variables: { + userId: 'regular-user-id', + badgeId: 'trophy_rhino', + }, + }), + ).resolves.toMatchObject({ + data: { + rewardTrophyBadge: { + id: 'regular-user-id', + badgeTrophies: expect.arrayContaining([ + { id: 'trophy_racoon' }, + { id: 'trophy_rhino' }, + ]), + badgeTrophiesSelected: [ + { + id: 'default_trophy', + }, + { id: 'trophy_racoon' }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + ], + }, + }, + errors: undefined, + }) + }) + + it('does fill gaps in the selection array when rewarding new badges', async () => { + await Factory.build('badge', { + id: 'trophy_racoon', + type: 'trophy', + description: 'You earned a racoon', + icon: '/img/badges/trophy_blue_racoon.svg', + }) + await mutate({ + mutation: rewardTrophyBadge, + variables: { + userId: 'regular-user-id', + badgeId: 'trophy_rhino', + }, + }) + authenticatedUser = await regularUser.toJson() + await mutate({ + mutation: setTrophyBadgeSelected, + variables: { + slot: 1, + badgeId: 'trophy_rhino', + }, + }) + authenticatedUser = await administrator.toJson() + await expect( + mutate({ + mutation: rewardTrophyBadge, + variables: { + userId: 'regular-user-id', + badgeId: 'trophy_racoon', + }, + }), + ).resolves.toMatchObject({ + data: { + rewardTrophyBadge: { + id: 'regular-user-id', + badgeTrophies: expect.arrayContaining([ + { id: 'trophy_racoon' }, + { id: 'trophy_rhino' }, + ]), + badgeTrophiesSelected: [ + { id: 'trophy_racoon' }, + { id: 'trophy_rhino' }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + ], + }, + }, + errors: undefined, + }) + }) + + it('does not select badge when maximum selected are already reached', async () => { + for (let i = 0; i < TROPHY_BADGES_SELECTED_MAX; i++) { + await Factory.build('badge', { + id: `trophy_${i}`, + type: 'trophy', + description: `You earned a ${i}`, + icon: `/img/badges/trophy_blue_${i}.svg`, + }) + await mutate({ + mutation: rewardTrophyBadge, + variables: { + userId: 'regular-user-id', + badgeId: `trophy_${i}`, + }, + }) + } + await expect( + mutate({ + mutation: rewardTrophyBadge, + variables: { + userId: 'regular-user-id', + badgeId: 'trophy_rhino', + }, + }), + ).resolves.toMatchObject({ + data: { + rewardTrophyBadge: { + id: 'regular-user-id', + badgeTrophies: expect.arrayContaining([ + { id: 'trophy_0' }, + { id: 'trophy_1' }, + { id: 'trophy_2' }, + { id: 'trophy_3' }, + { id: 'trophy_4' }, + { id: 'trophy_5' }, + { id: 'trophy_6' }, + { id: 'trophy_7' }, + { id: 'trophy_8' }, + { id: 'trophy_rhino' }, + ]), + badgeTrophiesSelected: [ + { id: 'trophy_0' }, + { id: 'trophy_1' }, + { id: 'trophy_2' }, + { id: 'trophy_3' }, + { id: 'trophy_4' }, + { id: 'trophy_5' }, + { id: 'trophy_6' }, + { id: 'trophy_7' }, + { id: 'trophy_8' }, + ], + }, + }, + errors: undefined, + }) + }) + + it('rewards the same badge as well to another user', async () => { + await Factory.build( + 'user', + { + id: 'regular-user-2-id', + }, + { + email: 'regular2@email.com', + }, + ) + await mutate({ + mutation: rewardTrophyBadge, + variables, + }) + await expect( + mutate({ + mutation: rewardTrophyBadge, + variables: { + userId: 'regular-user-2-id', + badgeId: 'trophy_rhino', + }, + }), + ).resolves.toMatchObject({ + data: { + rewardTrophyBadge: { + id: 'regular-user-2-id', + badgeTrophies: [{ id: 'trophy_rhino' }], + badgeTrophiesSelected: [ + { id: 'trophy_rhino' }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + ], + }, + }, + errors: undefined, + }) + }) + + it('creates no duplicate reward relationships', async () => { + await mutate({ + mutation: rewardTrophyBadge, + variables, + }) + await expect( + mutate({ + mutation: rewardTrophyBadge, + variables, + }), + ).resolves.toMatchObject({ + data: { + rewardTrophyBadge: { + id: 'regular-user-id', + badgeTrophiesCount: 1, + badgeTrophies: [{ id: 'trophy_rhino' }], + badgeTrophiesSelected: [ + { id: 'trophy_rhino' }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + { + id: 'default_trophy', + }, + ], + }, + }, + errors: undefined, + }) + }) + }) + }) + + describe('revokeBadge', () => { + const variables = { + badgeId: 'trophy_rhino', + userId: 'regular-user-id', + } + + beforeEach(async () => { + await regularUser.relateTo(badge, 'rewarded') + await regularUser.relateTo(verification, 'verifies') + await regularUser.relateTo(badge, 'selected', { slot: 6 }) + }) + + const revokeBadgeMutation = gql` + mutation ($badgeId: ID!, $userId: ID!) { + revokeBadge(badgeId: $badgeId, userId: $userId) { + id + badgeTrophies { + id + } + badgeVerification { + id + isDefault + } + badgeTrophiesSelected { + id + isDefault + } + } + } + ` + + describe('check test setup', () => { + it('user has one badge and has it selected', async () => { + authenticatedUser = regularUser.toJson() + const userQuery = gql` + { + User(id: "regular-user-id") { + badgeTrophiesCount + badgeTrophies { + id + } + badgeVerification { + id + isDefault + } + badgeTrophiesSelected { + id + isDefault + } + } + } + ` + const expected = { + data: { + User: [ + { + badgeTrophiesCount: 1, + badgeTrophies: [{ id: 'trophy_rhino' }], + badgeVerification: { + id: 'verification_moderator', + isDefault: false, + }, + badgeTrophiesSelected: [ + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'trophy_rhino', + isDefault: false, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + ], + }, + ], + }, + errors: undefined, + } + await expect(query({ query: userQuery })).resolves.toMatchObject(expected) + }) + }) + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + authenticatedUser = null + await expect(mutate({ mutation: revokeBadgeMutation, variables })).resolves.toMatchObject({ + data: { revokeBadge: null }, + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + + describe('authenticated moderator', () => { + beforeEach(async () => { + authenticatedUser = await moderator.toJson() + }) + + describe('removes badge from user', () => { + it('throws authorization error', async () => { + await expect(mutate({ mutation: revokeBadgeMutation, variables })).resolves.toMatchObject( + { + data: { revokeBadge: null }, + errors: [{ message: 'Not Authorized!' }], + }, + ) + }) + }) + }) + + describe('authenticated admin', () => { + beforeEach(async () => { + authenticatedUser = await administrator.toJson() + }) + + it('removes a badge from user', async () => { + await expect(mutate({ mutation: revokeBadgeMutation, variables })).resolves.toMatchObject({ + data: { + revokeBadge: { + id: 'regular-user-id', + badgeVerification: { id: 'verification_moderator', isDefault: false }, + badgeTrophies: [], + badgeTrophiesSelected: [ + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + ], + }, + }, + errors: undefined, + }) + }) + + it('does not crash when revoking multiple times', async () => { + await mutate({ mutation: revokeBadgeMutation, variables }) + await expect(mutate({ mutation: revokeBadgeMutation, variables })).resolves.toMatchObject({ + data: { + revokeBadge: { + id: 'regular-user-id', + badgeVerification: { id: 'verification_moderator', isDefault: false }, + badgeTrophies: [], + badgeTrophiesSelected: [ + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + ], + }, + }, + errors: undefined, + }) + }) + + it('removes a verification from user', async () => { + await expect( + mutate({ + mutation: revokeBadgeMutation, + variables: { + badgeId: 'verification_moderator', + userId: 'regular-user-id', + }, + }), + ).resolves.toMatchObject({ + data: { + revokeBadge: { + id: 'regular-user-id', + badgeVerification: { id: 'default_verification', isDefault: true }, + badgeTrophies: [{ id: 'trophy_rhino' }], + badgeTrophiesSelected: [ + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'trophy_rhino', + isDefault: false, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + ], + }, + }, + errors: undefined, + }) + }) + + it('does not crash when removing verification multiple times', async () => { + await mutate({ + mutation: revokeBadgeMutation, + variables: { + badgeId: 'verification_moderator', + userId: 'regular-user-id', + }, + }) + await expect( + mutate({ + mutation: revokeBadgeMutation, + variables: { + badgeId: 'verification_moderator', + userId: 'regular-user-id', + }, + }), + ).resolves.toMatchObject({ + data: { + revokeBadge: { + id: 'regular-user-id', + badgeVerification: { id: 'default_verification', isDefault: true }, + badgeTrophies: [{ id: 'trophy_rhino' }], + badgeTrophiesSelected: [ + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'trophy_rhino', + isDefault: false, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + ], + }, + }, + errors: undefined, + }) + }) + }) + }) +}) diff --git a/backend/src/graphql/resolvers/badges.ts b/backend/src/graphql/resolvers/badges.ts new file mode 100644 index 000000000..700e18d89 --- /dev/null +++ b/backend/src/graphql/resolvers/badges.ts @@ -0,0 +1,170 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { neo4jgraphql } from 'neo4j-graphql-js' + +import { TROPHY_BADGES_SELECTED_MAX } from '@constants/badges' +import { Context } from '@src/server' + +export const defaultTrophyBadge = { + id: 'default_trophy', + type: 'trophy', + icon: '/img/badges/default_trophy.svg', + description: '', + createdAt: '', +} + +export const defaultVerificationBadge = { + id: 'default_verification', + type: 'verification', + icon: '/img/badges/default_verification.svg', + description: '', + createdAt: '', +} + +export default { + Query: { + Badge: async (object, args, context, resolveInfo) => + neo4jgraphql(object, args, context, resolveInfo), + }, + + Mutation: { + setVerificationBadge: async (_object, args, context, _resolveInfo) => { + const { + user: { id: currentUserId }, + } = context + const { badgeId, userId } = args + const session = context.driver.session() + + const writeTxResultPromise = session.writeTransaction(async (transaction) => { + const response = await transaction.run( + ` + MATCH (badge:Badge {id: $badgeId, type: 'verification'}), (user:User {id: $userId}) + OPTIONAL MATCH (:Badge {type: 'verification'})-[verify:VERIFIES]->(user) + DELETE verify + MERGE (badge)-[relation:VERIFIES {by: $currentUserId}]->(user) + RETURN relation, user {.*} + `, + { + badgeId, + userId, + currentUserId, + }, + ) + return { + relation: response.records.map((record) => record.get('relation'))[0], + user: response.records.map((record) => record.get('user'))[0], + } + }) + try { + const { relation, user } = await writeTxResultPromise + if (!relation) { + throw new Error( + 'Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.', + ) + } + return user + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, + + rewardTrophyBadge: async (_object, args, context: Context, _resolveInfo) => { + const { + user: { id: currentUserId }, + } = context + const { badgeId, userId } = args + + // Find used slot + const userBadges = ( + await context.database.query({ + query: ` + MATCH (rewardedBadge:Badge)-[rewarded:REWARDED]->(user:User {id: $userId}) + OPTIONAL MATCH (rewardedBadge)<-[selected:SELECTED]-(user) + RETURN collect(rewardedBadge {.*}) AS rewardedBadges, collect(toString(selected.slot)) AS usedSlots + `, + variables: { userId }, + }) + ).records.map((record) => { + return { + rewardedBadges: record.get('rewardedBadges'), + usedSlots: record.get('usedSlots'), + } + }) + + const { rewardedBadges, usedSlots } = userBadges[0] + + let slot + if ( + !rewardedBadges.find((item) => item.id === badgeId) && // badge was not rewarded yet + usedSlots.length < TROPHY_BADGES_SELECTED_MAX // there is free slots left + ) { + for (slot = 0; slot <= TROPHY_BADGES_SELECTED_MAX; slot++) { + if (!usedSlots.find((item) => parseInt(item) === slot)) { + break + } + } + } + + // reward badge and assign slot + const users = ( + await context.database.write({ + query: ` + MATCH (badge:Badge {id: $badgeId, type: 'trophy'}), (user:User {id: $userId}) + MERGE (badge)-[:REWARDED {by: $currentUserId}]->(user) + ${slot === undefined ? '' : 'MERGE (badge)<-[:SELECTED {slot: $slot}]-(user)'} + RETURN user {.*} + `, + variables: { badgeId, userId, currentUserId, slot }, + }) + ).records.map((record) => record.get('user')) + + if (users.length !== 1) { + throw new Error( + 'Could not reward badge! Ensure the user and the badge exist and the badge is of the correct type.', + ) + } + + return users[0] + }, + + revokeBadge: async (_object, args, context, _resolveInfo) => { + const { badgeId, userId } = args + const session = context.driver.session() + + const writeTxResultPromise = session.writeTransaction(async (transaction) => { + const response = await transaction.run( + ` + MATCH (user:User {id: $userId}) + OPTIONAL MATCH (badge:Badge {id: $badgeId})-[rewarded:REWARDED|VERIFIES]->(user) + OPTIONAL MATCH (user)-[selected:SELECTED]->(badge) + DELETE rewarded + DELETE selected + RETURN user {.*} + `, + { + badgeId, + userId, + }, + ) + return response.records.map((record) => record.get('user'))[0] + }) + try { + return await writeTxResultPromise + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, + }, + Badge: { + isDefault: async (parent, _params, _context, _resolveInfo) => + [defaultTrophyBadge.id, defaultVerificationBadge.id].includes(parent.id), + }, +} diff --git a/backend/src/schema/resolvers/comments.spec.ts b/backend/src/graphql/resolvers/comments.spec.ts similarity index 88% rename from backend/src/schema/resolvers/comments.spec.ts rename to backend/src/graphql/resolvers/comments.spec.ts index b2730dad4..9681abe9a 100644 --- a/backend/src/schema/resolvers/comments.spec.ts +++ b/backend/src/graphql/resolvers/comments.spec.ts @@ -1,36 +1,42 @@ -import Factory, { cleanDatabase } from '../../db/factories' -import gql from 'graphql-tag' +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { ApolloServer } from 'apollo-server-express' import { createTestClient } from 'apollo-server-testing' -import createServer from '../../server' -import { getNeode, getDriver } from '../../db/neo4j' +import gql from 'graphql-tag' -const driver = getDriver() -const neode = getNeode() +import databaseContext from '@context/database' +import Factory, { cleanDatabase } from '@db/factories' +import createServer, { getContext } from '@src/server' + +const database = databaseContext() let variables, mutate, authenticatedUser, commentAuthor, newlyCreatedComment +let server: ApolloServer beforeAll(async () => { await cleanDatabase() - const { server } = createServer({ - context: () => { - return { - driver, - user: authenticatedUser, - } - }, - }) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/require-await + const contextUser = async (_req) => authenticatedUser + const context = getContext({ user: contextUser, database }) + + server = createServer({ context }).server + mutate = createTestClient(server).mutate }) afterAll(async () => { await cleanDatabase() - driver.close() + void server.stop() + void database.driver.close() + database.neode.close() }) beforeEach(async () => { variables = {} - await neode.create('Category', { + await database.neode.create('Category', { id: 'cat9', name: 'Democracy & Politics', icon: 'university', @@ -98,7 +104,7 @@ describe('CreateComment', () => { describe('authenticated', () => { beforeEach(async () => { - const user = await neode.create('User', { name: 'Author' }) + const user = await database.neode.create('User', { name: 'Author' }) authenticatedUser = await user.toJson() }) diff --git a/backend/src/schema/resolvers/comments.ts b/backend/src/graphql/resolvers/comments.ts similarity index 87% rename from backend/src/schema/resolvers/comments.ts rename to backend/src/graphql/resolvers/comments.ts index b9c0271c1..e07c6791d 100644 --- a/backend/src/schema/resolvers/comments.ts +++ b/backend/src/graphql/resolvers/comments.ts @@ -1,9 +1,14 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { v4 as uuid } from 'uuid' + import Resolver from './helpers/Resolver' export default { Mutation: { - CreateComment: async (object, params, context, resolveInfo) => { + CreateComment: async (_object, params, context, _resolveInfo) => { const { postId } = params const { user, driver } = context // Adding relationship from comment to post by passing in the postId, @@ -103,10 +108,14 @@ export default { count: { postObservingUsersCount: '-[:COMMENTS]->(:Post)<-[obs:OBSERVES]-(related:User) WHERE obs.active = true AND NOT related.deleted AND NOT related.disabled', + shoutedCount: + '<-[:SHOUTED]-(related:User) WHERE NOT related.deleted = true AND NOT related.disabled = true', }, boolean: { isPostObservedByMe: 'MATCH (this)-[:COMMENTS]->(:Post)<-[obs:OBSERVES]-(related:User {id: $cypherParams.currentUserId}) WHERE obs.active = true RETURN COUNT(related) >= 1', + shoutedByCurrentUser: + 'MATCH (this) RETURN EXISTS((this)<-[:SHOUTED]-(:User {id: $cypherParams.currentUserId}))', }, }), }, diff --git a/backend/src/schema/resolvers/donations.spec.ts b/backend/src/graphql/resolvers/donations.spec.ts similarity index 93% rename from backend/src/schema/resolvers/donations.spec.ts rename to backend/src/graphql/resolvers/donations.spec.ts index 9fc010eca..8fc23d4e9 100644 --- a/backend/src/schema/resolvers/donations.spec.ts +++ b/backend/src/graphql/resolvers/donations.spec.ts @@ -1,8 +1,13 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../../db/factories' import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' let mutate, query, authenticatedUser, variables const instance = getNeode() @@ -37,7 +42,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('donations', () => { diff --git a/backend/src/schema/resolvers/donations.ts b/backend/src/graphql/resolvers/donations.ts similarity index 88% rename from backend/src/schema/resolvers/donations.ts rename to backend/src/graphql/resolvers/donations.ts index d077e7bed..017a97f5f 100644 --- a/backend/src/schema/resolvers/donations.ts +++ b/backend/src/graphql/resolvers/donations.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ export default { Query: { Donations: async (_parent, _params, context, _resolveInfo) => { diff --git a/backend/src/schema/resolvers/emails.spec.ts b/backend/src/graphql/resolvers/emails.spec.ts similarity index 85% rename from backend/src/schema/resolvers/emails.spec.ts rename to backend/src/graphql/resolvers/emails.spec.ts index 02a631495..f77602463 100644 --- a/backend/src/schema/resolvers/emails.spec.ts +++ b/backend/src/graphql/resolvers/emails.spec.ts @@ -1,8 +1,14 @@ -import Factory, { cleanDatabase } from '../../db/factories' -import gql from 'graphql-tag' -import { getDriver, getNeode } from '../../db/neo4j' -import createServer from '../../server' +/* eslint-disable @typescript-eslint/await-thenable */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' +import gql from 'graphql-tag' + +import Factory, { cleanDatabase } from '@db/factories' +import { getDriver, getNeode } from '@db/neo4j' +import createServer from '@src/server' const neode = getNeode() @@ -30,7 +36,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { @@ -104,11 +110,14 @@ describe('AddEmailAddress', () => { it('connects `UnverifiedEmailAddress` to the authenticated user', async () => { await mutate({ mutation, variables }) - const result = await neode.cypher(` + const result = await neode.cypher( + ` MATCH(u:User)-[:PRIMARY_EMAIL]->(:EmailAddress {email: "user@example.org"}) MATCH(u:User)<-[:BELONGS_TO]-(e:UnverifiedEmailAddress {email: "new-email@example.org"}) RETURN e - `) + `, + {}, + ) const email = neode.hydrateFirst(result, 'e', neode.model('UnverifiedEmailAddress')) await expect(email.toJson()).resolves.toMatchObject({ email: 'new-email@example.org', @@ -251,10 +260,13 @@ describe('VerifyEmailAddress', () => { it('connects the new `EmailAddress` as PRIMARY', async () => { await mutate({ mutation, variables }) - const result = await neode.cypher(` + const result = await neode.cypher( + ` MATCH(u:User {id: "567"})-[:PRIMARY_EMAIL]->(e:EmailAddress {email: "to-be-verified@example.org"}) RETURN e - `) + `, + {}, + ) const email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress')) await expect(email.toJson()).resolves.toMatchObject({ email: 'to-be-verified@example.org', @@ -266,13 +278,13 @@ describe('VerifyEmailAddress', () => { MATCH(u:User {id: "567"})-[:PRIMARY_EMAIL]->(e:EmailAddress {email: "user@example.org"}) RETURN e ` - let result = await neode.cypher(cypherStatement) + let result = await neode.cypher(cypherStatement, {}) let email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress')) await expect(email.toJson()).resolves.toMatchObject({ email: 'user@example.org', }) await mutate({ mutation, variables }) - result = await neode.cypher(cypherStatement) + result = await neode.cypher(cypherStatement, {}) email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress')) await expect(email).toBe(false) }) @@ -282,20 +294,20 @@ describe('VerifyEmailAddress', () => { MATCH(u:User {id: "567"})<-[:BELONGS_TO]-(e:EmailAddress {email: "user@example.org"}) RETURN e ` - let result = await neode.cypher(cypherStatement) + let result = await neode.cypher(cypherStatement, {}) let email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress')) await expect(email.toJson()).resolves.toMatchObject({ email: 'user@example.org', }) await mutate({ mutation, variables }) - result = await neode.cypher(cypherStatement) + result = await neode.cypher(cypherStatement, {}) email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress')) await expect(email).toBe(false) }) - describe('Edge case: In the meantime someone created an `EmailAddress` node with the given email', () => { + describe('Edge case: In the meantime someone created an `EmailAddress` node with the given email belonging to a user', () => { beforeEach(async () => { - await Factory.build('emailAddress', { email: 'to-be-verified@example.org' }) + await Factory.build('user', { id: '568' }, { email: 'to-be-verified@example.org' }) }) it('throws UserInputError because of unique constraints', async () => { @@ -305,6 +317,27 @@ describe('VerifyEmailAddress', () => { }) }) }) + + describe('Edge case: We have an abandoned `EmailAddress` node with the given email', () => { + beforeEach(async () => { + await Factory.build('emailAddress', { email: 'to-be-verified@example.org' }) + }) + + it('connects the new `EmailAddress` as PRIMARY', async () => { + await mutate({ mutation, variables }) + const result = await neode.cypher( + ` + MATCH(u:User {id: "567"})-[:PRIMARY_EMAIL]->(e:EmailAddress {email: "to-be-verified@example.org"}) + RETURN e + `, + {}, + ) + const email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress')) + await expect(email.toJson()).resolves.toMatchObject({ + email: 'to-be-verified@example.org', + }) + }) + }) }) }) }) diff --git a/backend/src/schema/resolvers/emails.ts b/backend/src/graphql/resolvers/emails.ts similarity index 84% rename from backend/src/schema/resolvers/emails.ts rename to backend/src/graphql/resolvers/emails.ts index d705781ca..f5c4eb0ad 100644 --- a/backend/src/schema/resolvers/emails.ts +++ b/backend/src/graphql/resolvers/emails.ts @@ -1,13 +1,21 @@ -import generateNonce from './helpers/generateNonce' -import Resolver from './helpers/Resolver' -import existingEmailAddress from './helpers/existingEmailAddress' +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { UserInputError } from 'apollo-server' +// eslint-disable-next-line import/extensions import Validator from 'neode/build/Services/Validator.js' + +import existingEmailAddress from './helpers/existingEmailAddress' +import generateNonce from './helpers/generateNonce' import normalizeEmail from './helpers/normalizeEmail' +import Resolver from './helpers/Resolver' export default { Query: { VerifyNonce: async (_parent, args, context, _resolveInfo) => { + args.email = normalizeEmail(args.email) const session = context.driver.session() const readTxResultPromise = session.readTransaction(async (txc) => { const result = await txc.run( @@ -31,7 +39,6 @@ export default { AddEmailAddress: async (_parent, args, context, _resolveInfo) => { let response args.email = normalizeEmail(args.email) - try { const { neode } = context await new Validator(neode, neode.model('UnverifiedEmailAddress'), args) @@ -41,7 +48,7 @@ export default { // check email does not belong to anybody const existingEmail = await existingEmailAddress({ args, context }) - if (existingEmail && existingEmail.alreadyExistingEmail && existingEmail.user) + if (existingEmail?.alreadyExistingEmail && existingEmail.user) return existingEmail.alreadyExistingEmail const nonce = generateNonce() @@ -62,6 +69,7 @@ export default { ) return result.records.map((record) => ({ name: record.get('user').properties.name, + locale: record.get('user').properties.locale, ...record.get('email').properties, })) }) @@ -78,6 +86,7 @@ export default { const { user: { id: userId }, } = context + args.email = normalizeEmail(args.email) const { nonce, email } = args const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (txc) => { @@ -85,6 +94,8 @@ export default { ` MATCH (user:User {id: $userId})-[:PRIMARY_EMAIL]->(previous:EmailAddress) MATCH (user)<-[:BELONGS_TO]-(email:UnverifiedEmailAddress {email: $email, nonce: $nonce}) + OPTIONAL MATCH (abandonedEmail:EmailAddress{email: $email}) WHERE NOT EXISTS ((abandonedEmail)<-[]-()) + DELETE abandonedEmail MERGE (user)-[:PRIMARY_EMAIL]->(email) SET email:EmailAddress SET email.verifiedAt = toString(datetime()) diff --git a/backend/src/schema/resolvers/embeds.spec.ts b/backend/src/graphql/resolvers/embeds.spec.ts similarity index 94% rename from backend/src/schema/resolvers/embeds.spec.ts rename to backend/src/graphql/resolvers/embeds.spec.ts index 8e7a69891..f6de4d13e 100644 --- a/backend/src/schema/resolvers/embeds.spec.ts +++ b/backend/src/graphql/resolvers/embeds.spec.ts @@ -1,9 +1,14 @@ -import fetch from 'node-fetch' -import fs from 'fs' -import path from 'path' +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +import fs from 'node:fs' +import path from 'node:path' + import { createTestClient } from 'apollo-server-testing' -import createServer from '../../server' import gql from 'graphql-tag' +import fetch from 'node-fetch' + +import createServer from '@src/server' jest.mock('node-fetch') const mockedFetch = jest.mocked(fetch) @@ -15,14 +20,17 @@ afterEach(() => { let variables = {} +// eslint-disable-next-line n/no-sync const HumanConnectionOrg = fs.readFileSync( path.join(__dirname, '../../../snapshots/embeds/HumanConnectionOrg.html'), 'utf8', ) +// eslint-disable-next-line n/no-sync const pr3934 = fs.readFileSync( path.join(__dirname, '../../../snapshots/embeds/pr3934.html'), 'utf8', ) +// eslint-disable-next-line n/no-sync const babyLovesCat = fs.readFileSync( path.join(__dirname, '../../../snapshots/embeds/babyLovesCat.html'), 'utf8', @@ -53,6 +61,7 @@ describe('Query', () => { beforeEach(() => { embedAction = async (variables) => { const { server } = createServer({ + // eslint-disable-next-line @typescript-eslint/no-empty-function context: () => {}, }) const { query } = createTestClient(server) diff --git a/backend/src/schema/resolvers/embeds.ts b/backend/src/graphql/resolvers/embeds.ts similarity index 61% rename from backend/src/schema/resolvers/embeds.ts rename to backend/src/graphql/resolvers/embeds.ts index a75365ec7..8ce144b4f 100644 --- a/backend/src/schema/resolvers/embeds.ts +++ b/backend/src/graphql/resolvers/embeds.ts @@ -1,9 +1,12 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import scrape from './embeds/scraper' import { undefinedToNullResolver } from './helpers/Resolver' export default { Query: { - embed: async (object, { url }, context, resolveInfo) => { + embed: async (_object, { url }, _context, _resolveInfo) => { return scrape(url) }, }, @@ -22,7 +25,7 @@ export default { 'lang', 'html', ]), - sources: async (parent, params, context, resolveInfo) => { + sources: async (parent, _params, _context, _resolveInfo) => { return typeof parent.sources === 'undefined' ? [] : parent.sources }, }, diff --git a/backend/src/schema/resolvers/embeds/findProvider.spec.ts b/backend/src/graphql/resolvers/embeds/findProvider.spec.ts similarity index 100% rename from backend/src/schema/resolvers/embeds/findProvider.spec.ts rename to backend/src/graphql/resolvers/embeds/findProvider.spec.ts diff --git a/backend/src/schema/resolvers/embeds/findProvider.ts b/backend/src/graphql/resolvers/embeds/findProvider.ts similarity index 67% rename from backend/src/schema/resolvers/embeds/findProvider.ts rename to backend/src/graphql/resolvers/embeds/findProvider.ts index 1b875b180..6f5e0df90 100644 --- a/backend/src/schema/resolvers/embeds/findProvider.ts +++ b/backend/src/graphql/resolvers/embeds/findProvider.ts @@ -1,7 +1,14 @@ -import fs from 'fs' -import path from 'path' +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import fs from 'node:fs' +import path from 'node:path' + import { minimatch } from 'minimatch' +// eslint-disable-next-line n/no-sync let oEmbedProvidersFile = fs.readFileSync( path.join(__dirname, '../../../../public/providers.json'), 'utf8', diff --git a/backend/src/schema/resolvers/embeds/scraper.ts b/backend/src/graphql/resolvers/embeds/scraper.ts similarity index 72% rename from backend/src/schema/resolvers/embeds/scraper.ts rename to backend/src/graphql/resolvers/embeds/scraper.ts index 79dd5a368..a8cd07a76 100644 --- a/backend/src/schema/resolvers/embeds/scraper.ts +++ b/backend/src/graphql/resolvers/embeds/scraper.ts @@ -1,12 +1,25 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable n/no-extraneous-require */ +/* eslint-disable n/global-require */ +/* eslint-disable import/no-commonjs */ +/* eslint-disable import/no-named-as-default */ + +import { ApolloError } from 'apollo-server' +import isArray from 'lodash/isArray' +import isEmpty from 'lodash/isEmpty' +import mergeWith from 'lodash/mergeWith' import Metascraper from 'metascraper' import fetch from 'node-fetch' -import { ApolloError } from 'apollo-server' -import isEmpty from 'lodash/isEmpty' -import isArray from 'lodash/isArray' -import mergeWith from 'lodash/mergeWith' import findProvider from './findProvider' +// eslint-disable-next-line import/no-extraneous-dependencies const error = require('debug')('embed:error') const metascraper = Metascraper([ @@ -37,6 +50,7 @@ const fetchEmbed = async (url) => { try { const response = await fetch(endpointUrl) json = await response.json() + // eslint-disable-next-line no-catch-all/no-catch-all } catch (err) { error(`Error fetching embed data: ${err.message}`) return {} @@ -73,8 +87,9 @@ export default async function scrape(url) { throw new ApolloError('Not found', 'NOT_FOUND') } - return { - type: 'link', - ...output, + if (!output.type) { + output.type = 'link' } + + return output } diff --git a/backend/src/schema/resolvers/filter-posts.spec.ts b/backend/src/graphql/resolvers/filter-posts.spec.ts similarity index 92% rename from backend/src/schema/resolvers/filter-posts.spec.ts rename to backend/src/graphql/resolvers/filter-posts.spec.ts index 95a072d8a..c29b98365 100644 --- a/backend/src/schema/resolvers/filter-posts.spec.ts +++ b/backend/src/graphql/resolvers/filter-posts.spec.ts @@ -1,9 +1,14 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../../db/factories' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' -import CONFIG from '../../config' -import { filterPosts, createPostMutation } from '../../graphql/posts' + +import CONFIG from '@config/index' +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import { createPostMutation } from '@graphql/queries/createPostMutation' +import { filterPosts } from '@graphql/queries/filterPosts' +import createServer from '@src/server' CONFIG.CATEGORIES_ACTIVE = false @@ -33,7 +38,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('Filter Posts', () => { diff --git a/backend/src/schema/resolvers/follow.spec.ts b/backend/src/graphql/resolvers/follow.spec.ts similarity index 92% rename from backend/src/schema/resolvers/follow.spec.ts rename to backend/src/graphql/resolvers/follow.spec.ts index c9d8dc1bf..e846eb56f 100644 --- a/backend/src/schema/resolvers/follow.spec.ts +++ b/backend/src/graphql/resolvers/follow.spec.ts @@ -1,9 +1,14 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../../db/factories' -import { getDriver, getNeode } from '../../db/neo4j' -import createServer from '../../server' import gql from 'graphql-tag' +import Factory, { cleanDatabase } from '@db/factories' +import { getDriver, getNeode } from '@db/neo4j' +import createServer from '@src/server' + const driver = getDriver() const neode = getNeode() @@ -71,7 +76,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { diff --git a/backend/src/schema/resolvers/follow.ts b/backend/src/graphql/resolvers/follow.ts similarity index 85% rename from backend/src/schema/resolvers/follow.ts rename to backend/src/graphql/resolvers/follow.ts index 6cf4938c7..8d69a7d5b 100644 --- a/backend/src/schema/resolvers/follow.ts +++ b/backend/src/graphql/resolvers/follow.ts @@ -1,4 +1,7 @@ -import { getNeode } from '../../db/neo4j' +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import { getNeode } from '@db/neo4j' const neode = getNeode() diff --git a/backend/src/schema/resolvers/groups.spec.ts b/backend/src/graphql/resolvers/groups.spec.ts similarity index 97% rename from backend/src/schema/resolvers/groups.spec.ts rename to backend/src/graphql/resolvers/groups.spec.ts index 1d66b376c..333bc03c1 100644 --- a/backend/src/schema/resolvers/groups.spec.ts +++ b/backend/src/graphql/resolvers/groups.spec.ts @@ -1,21 +1,22 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../../db/factories' -import { - createGroupMutation, - updateGroupMutation, - joinGroupMutation, - leaveGroupMutation, - changeGroupMemberRoleMutation, - removeUserFromGroupMutation, - groupMembersQuery, - groupQuery, -} from '../../graphql/groups' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' -import CONFIG from '../../config' -const driver = getDriver() -const neode = getNeode() +import CONFIG from '@config/index' +import databaseContext from '@context/database' +import Factory, { cleanDatabase } from '@db/factories' +import { changeGroupMemberRoleMutation } from '@graphql/queries/changeGroupMemberRoleMutation' +import { createGroupMutation } from '@graphql/queries/createGroupMutation' +import { Group as groupQuery } from '@graphql/queries/Group' +import { GroupMembers as groupMembersQuery } from '@graphql/queries/GroupMembers' +import { joinGroupMutation } from '@graphql/queries/joinGroupMutation' +import { leaveGroupMutation } from '@graphql/queries/leaveGroupMutation' +import { removeUserFromGroupMutation } from '@graphql/queries/removeUserFromGroupMutation' +import { updateGroupMutation } from '@graphql/queries/updateGroupMutation' +import createServer, { getContext } from '@src/server' let authenticatedUser let user @@ -31,15 +32,12 @@ const descriptionAdditional100 = ' 123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789' let variables = {} -const { server } = createServer({ - context: () => { - return { - driver, - neode, - user: authenticatedUser, - } - }, -}) +const database = databaseContext() +// eslint-disable-next-line @typescript-eslint/no-unsafe-return +const contextUser = async (_req) => authenticatedUser +const context = getContext({ user: contextUser, database }) + +const { server } = createServer({ context }) const { mutate, query } = createTestClient(server) const seedBasicsAndClearAuthentication = async () => { @@ -56,25 +54,25 @@ const seedBasicsAndClearAuthentication = async () => { }, ) await Promise.all([ - neode.create('Category', { + database.neode.create('Category', { id: 'cat4', name: 'Environment & Nature', slug: 'environment-nature', icon: 'tree', }), - neode.create('Category', { + database.neode.create('Category', { id: 'cat9', name: 'Democracy & Politics', slug: 'democracy-politics', icon: 'university', }), - neode.create('Category', { + database.neode.create('Category', { id: 'cat15', name: 'Consumption & Sustainability', slug: 'consumption-sustainability', icon: 'shopping-cart', }), - neode.create('Category', { + database.neode.create('Category', { id: 'cat27', name: 'Animal Protection', slug: 'animal-protection', @@ -237,7 +235,9 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + void server.stop() + void database.driver.close() + database.neode.close() }) describe('in mode', () => { @@ -423,7 +423,7 @@ describe('in mode', () => { describe('unauthenticated', () => { it('throws authorization error', async () => { - const { errors } = await query({ query: groupQuery(), variables: {} }) + const { errors } = await query({ query: groupQuery, variables: {} }) expect(errors![0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -541,7 +541,7 @@ describe('in mode', () => { describe('in general finds only listed groups – no hidden groups where user is none or pending member', () => { describe('without any filters', () => { it('finds all listed groups – including the set descriptionExcerpts and locations', async () => { - const result = await query({ query: groupQuery(), variables: {} }) + const result = await query({ query: groupQuery, variables: {} }) expect(result).toMatchObject({ data: { Group: expect.arrayContaining([ @@ -586,9 +586,7 @@ describe('in mode', () => { }) it('has set categories', async () => { - await expect( - query({ query: groupQuery(), variables: {} }), - ).resolves.toMatchObject({ + await expect(query({ query: groupQuery, variables: {} })).resolves.toMatchObject({ data: { Group: expect.arrayContaining([ expect.objectContaining({ @@ -622,7 +620,7 @@ describe('in mode', () => { describe('with given id', () => { describe("id = 'my-group'", () => { it('finds only the listed group with this id', async () => { - const result = await query({ query: groupQuery(), variables: { id: 'my-group' } }) + const result = await query({ query: groupQuery, variables: { id: 'my-group' } }) expect(result).toMatchObject({ data: { Group: [ @@ -642,7 +640,7 @@ describe('in mode', () => { describe("id = 'third-hidden-group'", () => { it("finds only the hidden group where I'm 'usual' member", async () => { const result = await query({ - query: groupQuery(), + query: groupQuery, variables: { id: 'third-hidden-group' }, }) expect(result).toMatchObject({ @@ -664,7 +662,7 @@ describe('in mode', () => { describe("id = 'second-hidden-group'", () => { it("finds no hidden group where I'm 'pending' member", async () => { const result = await query({ - query: groupQuery(), + query: groupQuery, variables: { id: 'second-hidden-group' }, }) expect(result.data?.Group.length).toBe(0) @@ -674,7 +672,7 @@ describe('in mode', () => { describe("id = 'hidden-group'", () => { it("finds no hidden group where I'm not(!) a member at all", async () => { const result = await query({ - query: groupQuery(), + query: groupQuery, variables: { id: 'hidden-group' }, }) expect(result.data?.Group.length).toBe(0) @@ -686,7 +684,7 @@ describe('in mode', () => { describe("slug = 'the-best-group'", () => { it('finds only the listed group with this slug', async () => { const result = await query({ - query: groupQuery(), + query: groupQuery, variables: { slug: 'the-best-group' }, }) expect(result).toMatchObject({ @@ -708,7 +706,7 @@ describe('in mode', () => { describe("slug = 'third-investigative-journalism-group'", () => { it("finds only the hidden group where I'm 'usual' member", async () => { const result = await query({ - query: groupQuery(), + query: groupQuery, variables: { slug: 'third-investigative-journalism-group' }, }) expect(result).toMatchObject({ @@ -730,7 +728,7 @@ describe('in mode', () => { describe("slug = 'second-investigative-journalism-group'", () => { it("finds no hidden group where I'm 'pending' member", async () => { const result = await query({ - query: groupQuery(), + query: groupQuery, variables: { slug: 'second-investigative-journalism-group' }, }) expect(result.data?.Group.length).toBe(0) @@ -740,7 +738,7 @@ describe('in mode', () => { describe("slug = 'investigative-journalism-group'", () => { it("finds no hidden group where I'm not(!) a member at all", async () => { const result = await query({ - query: groupQuery(), + query: groupQuery, variables: { slug: 'investigative-journalism-group' }, }) expect(result.data?.Group.length).toBe(0) @@ -750,7 +748,7 @@ describe('in mode', () => { describe('isMember = true', () => { it('finds only listed groups where user is member', async () => { - const result = await query({ query: groupQuery(), variables: { isMember: true } }) + const result = await query({ query: groupQuery, variables: { isMember: true } }) expect(result).toMatchObject({ data: { Group: expect.arrayContaining([ @@ -774,7 +772,7 @@ describe('in mode', () => { describe('isMember = false', () => { it('finds only listed groups where user is not(!) member', async () => { - const result = await query({ query: groupQuery(), variables: { isMember: false } }) + const result = await query({ query: groupQuery, variables: { isMember: false } }) expect(result).toMatchObject({ data: { Group: expect.arrayContaining([ @@ -1039,7 +1037,7 @@ describe('in mode', () => { variables = { id: 'not-existing-group', } - const { errors } = await query({ query: groupMembersQuery(), variables }) + const { errors } = await query({ query: groupMembersQuery, variables }) expect(errors![0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -1212,7 +1210,7 @@ describe('in mode', () => { it('finds all members', async () => { const result = await query({ - query: groupMembersQuery(), + query: groupMembersQuery, variables, }) expect(result).toMatchObject({ @@ -1245,7 +1243,7 @@ describe('in mode', () => { it('finds all members', async () => { const result = await query({ - query: groupMembersQuery(), + query: groupMembersQuery, variables, }) expect(result).toMatchObject({ @@ -1278,7 +1276,7 @@ describe('in mode', () => { it('finds all members', async () => { const result = await query({ - query: groupMembersQuery(), + query: groupMembersQuery, variables, }) expect(result).toMatchObject({ @@ -1321,7 +1319,7 @@ describe('in mode', () => { it('finds all members', async () => { const result = await query({ - query: groupMembersQuery(), + query: groupMembersQuery, variables, }) expect(result).toMatchObject({ @@ -1354,7 +1352,7 @@ describe('in mode', () => { it('finds all members', async () => { const result = await query({ - query: groupMembersQuery(), + query: groupMembersQuery, variables, }) expect(result).toMatchObject({ @@ -1386,7 +1384,7 @@ describe('in mode', () => { }) it('throws authorization error', async () => { - const { errors } = await query({ query: groupMembersQuery(), variables }) + const { errors } = await query({ query: groupMembersQuery, variables }) expect(errors![0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -1397,7 +1395,7 @@ describe('in mode', () => { }) it('throws authorization error', async () => { - const { errors } = await query({ query: groupMembersQuery(), variables }) + const { errors } = await query({ query: groupMembersQuery, variables }) expect(errors![0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -1419,7 +1417,7 @@ describe('in mode', () => { it('finds all members', async () => { const result = await query({ - query: groupMembersQuery(), + query: groupMembersQuery, variables, }) expect(result).toMatchObject({ @@ -1456,7 +1454,7 @@ describe('in mode', () => { it('finds all members', async () => { const result = await query({ - query: groupMembersQuery(), + query: groupMembersQuery, variables, }) expect(result).toMatchObject({ @@ -1493,7 +1491,7 @@ describe('in mode', () => { it('finds all members', async () => { const result = await query({ - query: groupMembersQuery(), + query: groupMembersQuery, variables, }) expect(result).toMatchObject({ @@ -1529,7 +1527,7 @@ describe('in mode', () => { }) it('throws authorization error', async () => { - const { errors } = await query({ query: groupMembersQuery(), variables }) + const { errors } = await query({ query: groupMembersQuery, variables }) expect(errors![0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -1540,7 +1538,7 @@ describe('in mode', () => { }) it('throws authorization error', async () => { - const { errors } = await query({ query: groupMembersQuery(), variables }) + const { errors } = await query({ query: groupMembersQuery, variables }) expect(errors![0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -2418,12 +2416,12 @@ describe('in mode', () => { describe('here "closed-group" for example', () => { const memberInGroup = async (userId, groupId) => { const result = await query({ - query: groupMembersQuery(), + query: groupMembersQuery, variables: { id: groupId, }, }) - return result.data && result.data.GroupMembers + return result.data?.GroupMembers ? !!result.data.GroupMembers.find((member) => member.id === userId) : null } diff --git a/backend/src/schema/resolvers/groups.ts b/backend/src/graphql/resolvers/groups.ts similarity index 73% rename from backend/src/schema/resolvers/groups.ts rename to backend/src/graphql/resolvers/groups.ts index f5282a3bb..9e330bade 100644 --- a/backend/src/schema/resolvers/groups.ts +++ b/backend/src/graphql/resolvers/groups.ts @@ -1,19 +1,29 @@ -import { v4 as uuid } from 'uuid' +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { UserInputError } from 'apollo-server' -import CONFIG from '../../config' -import { CATEGORIES_MIN, CATEGORIES_MAX } from '../../constants/categories' -import { DESCRIPTION_WITHOUT_HTML_LENGTH_MIN } from '../../constants/groups' -import { removeHtmlTags } from '../../middleware/helpers/cleanHtml' +import { v4 as uuid } from 'uuid' + +import CONFIG from '@config/index' +import { CATEGORIES_MIN, CATEGORIES_MAX } from '@constants/categories' +import { DESCRIPTION_WITHOUT_HTML_LENGTH_MIN } from '@constants/groups' +import { removeHtmlTags } from '@middleware/helpers/cleanHtml' +import type { Context } from '@src/server' + import Resolver, { removeUndefinedNullValuesFromObject, convertObjectToCypherMapLiteral, } from './helpers/Resolver' -import { mergeImage } from './images/images' +import { images } from './images/images' import { createOrUpdateLocations } from './users/location' export default { Query: { - Group: async (_object, params, context, _resolveInfo) => { + Group: async (_object, params, context: Context, _resolveInfo) => { const { isMember, id, slug, first, offset } = params let pagination = '' const orderBy = 'ORDER BY group.createdAt DESC' @@ -66,10 +76,10 @@ export default { } catch (error) { throw new Error(error) } finally { - session.close() + await session.close() } }, - GroupMembers: async (_object, params, context, _resolveInfo) => { + GroupMembers: async (_object, params, context: Context, _resolveInfo) => { const { id: groupId } = params const session = context.driver.session() const readTxResultPromise = session.readTransaction(async (txc) => { @@ -87,7 +97,7 @@ export default { } catch (error) { throw new Error(error) } finally { - session.close() + await session.close() } }, GroupCount: async (_object, params, context, _resolveInfo) => { @@ -125,7 +135,7 @@ export default { }, }, Mutation: { - CreateGroup: async (_parent, params, context, _resolveInfo) => { + CreateGroup: async (_parent, params, context: Context, _resolveInfo) => { const { categoryIds } = params delete params.categoryIds params.locationName = params.locationName === '' ? null : params.locationName @@ -173,7 +183,7 @@ export default { `, { userId: context.user.id, categoryIds, params }, ) - const [group] = await ownerCreateGroupTransactionResponse.records.map((record) => + const [group] = ownerCreateGroupTransactionResponse.records.map((record) => record.get('group'), ) return group @@ -188,10 +198,10 @@ export default { throw new UserInputError('Group with this slug already exists!') throw new Error(error) } finally { - session.close() + await session.close() } }, - UpdateGroup: async (_parent, params, context, _resolveInfo) => { + UpdateGroup: async (_parent, params, context: Context, _resolveInfo) => { const { categoryIds } = params delete params.categoryIds const { id: groupId, avatar: avatarInput } = params @@ -248,9 +258,9 @@ export default { categoryIds, params, }) - const [group] = await transactionResponse.records.map((record) => record.get('group')) + const [group] = transactionResponse.records.map((record) => record.get('group')) if (avatarInput) { - await mergeImage(group, 'AVATAR_IMAGE', avatarInput, { transaction }) + await images.mergeImage(group, 'AVATAR_IMAGE', avatarInput, { transaction }) } return group }) @@ -264,10 +274,10 @@ export default { throw new UserInputError('Group with this slug already exists!') throw new Error(error) } finally { - session.close() + await session.close() } }, - JoinGroup: async (_parent, params, context, _resolveInfo) => { + JoinGroup: async (_parent, params, context: Context, _resolveInfo) => { const { groupId, userId } = params const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { @@ -285,7 +295,7 @@ export default { RETURN member {.*, myRoleInGroup: membership.role} ` const transactionResponse = await transaction.run(joinGroupCypher, { groupId, userId }) - const [member] = await transactionResponse.records.map((record) => record.get('member')) + const [member] = transactionResponse.records.map((record) => record.get('member')) return member }) try { @@ -293,10 +303,10 @@ export default { } catch (error) { throw new Error(error) } finally { - session.close() + await session.close() } }, - LeaveGroup: async (_parent, params, context, _resolveInfo) => { + LeaveGroup: async (_parent, params, context: Context, _resolveInfo) => { const { groupId, userId } = params const session = context.driver.session() try { @@ -304,10 +314,10 @@ export default { } catch (error) { throw new Error(error) } finally { - session.close() + await session.close() } }, - ChangeGroupMemberRole: async (_parent, params, context, _resolveInfo) => { + ChangeGroupMemberRole: async (_parent, params, context: Context, _resolveInfo) => { const { groupId, userId, roleInGroup } = params const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { @@ -344,7 +354,7 @@ export default { userId, roleInGroup, }) - const [member] = await transactionResponse.records.map((record) => record.get('member')) + const [member] = transactionResponse.records.map((record) => record.get('member')) return member }) try { @@ -352,10 +362,10 @@ export default { } catch (error) { throw new Error(error) } finally { - session.close() + await session.close() } }, - RemoveUserFromGroup: async (_parent, params, context, _resolveInfo) => { + RemoveUserFromGroup: async (_parent, params, context: Context, _resolveInfo) => { const { groupId, userId } = params const session = context.driver.session() try { @@ -363,11 +373,87 @@ export default { } catch (error) { throw new Error(error) } finally { - session.close() + await session.close() + } + }, + muteGroup: async (_parent, params, context: Context, _resolveInfo) => { + const { groupId } = params + const userId = context.user.id + const session = context.driver.session() + const writeTxResultPromise = session.writeTransaction(async (transaction) => { + const transactionResponse = await transaction.run( + ` + MATCH (group:Group { id: $groupId }) + MATCH (user:User { id: $userId }) + MERGE (user)-[m:MUTED]->(group) + SET m.createdAt = toString(datetime()) + RETURN group { .* } + `, + { + groupId, + userId, + }, + ) + const [group] = transactionResponse.records.map((record) => record.get('group')) + return group + }) + try { + return await writeTxResultPromise + } catch (error) { + throw new Error(error) + } finally { + await session.close() + } + }, + unmuteGroup: async (_parent, params, context: Context, _resolveInfo) => { + const { groupId } = params + const userId = context.user.id + const session = context.driver.session() + const writeTxResultPromise = session.writeTransaction(async (transaction) => { + const transactionResponse = await transaction.run( + ` + MATCH (group:Group { id: $groupId }) + MATCH (user:User { id: $userId }) + OPTIONAL MATCH (user)-[m:MUTED]->(group) + DELETE m + RETURN group { .* } + `, + { + groupId, + userId, + }, + ) + const [group] = transactionResponse.records.map((record) => record.get('group')) + return group + }) + try { + return await writeTxResultPromise + } catch (error) { + throw new Error(error) + } finally { + await session.close() } }, }, Group: { + inviteCodes: async (parent, _args, context: Context, _resolveInfo) => { + if (!parent.id) { + throw new Error('Can not identify selected Group!') + } + return ( + await context.database.query({ + query: ` + MATCH (user:User {id: $user.id})-[:GENERATED]->(inviteCodes:InviteCode)-[:INVITES_TO]->(g:Group {id: $parent.id}) + RETURN inviteCodes {.*} + ORDER BY inviteCodes.createdAt ASC + `, + variables: { + user: context.user, + parent, + }, + }) + ).records.map((r) => r.get('inviteCodes')) + }, ...Resolver('Group', { undefinedToNull: ['deleted', 'disabled', 'locationName', 'about'], hasMany: { @@ -378,7 +464,23 @@ export default { avatar: '-[:AVATAR_IMAGE]->(related:Image)', location: '-[:IS_IN]->(related:Location)', }, + boolean: { + isMutedByMe: + 'MATCH (this) RETURN EXISTS( (this)<-[:MUTED]-(:User {id: $cypherParams.currentUserId}) )', + }, }), + name: async (parent, _args, context: Context, _resolveInfo) => { + if (!context.user) { + return parent.groupType === 'hidden' ? '' : parent.name + } + return parent.name + }, + about: async (parent, _args, context: Context, _resolveInfo) => { + if (!context.user) { + return parent.groupType === 'hidden' ? '' : parent.about + } + return parent.about + }, }, } diff --git a/backend/src/schema/resolvers/helpers/Resolver.ts b/backend/src/graphql/resolvers/helpers/Resolver.ts similarity index 81% rename from backend/src/schema/resolvers/helpers/Resolver.ts rename to backend/src/graphql/resolvers/helpers/Resolver.ts index 58d1512d7..71d7602a4 100644 --- a/backend/src/schema/resolvers/helpers/Resolver.ts +++ b/backend/src/graphql/resolvers/helpers/Resolver.ts @@ -1,5 +1,12 @@ -import log from './databaseLogger' - +/* eslint-disable @typescript-eslint/no-dynamic-delete */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable security/detect-object-injection */ export const undefinedToNullResolver = (list) => { const resolvers = {} list.forEach((key) => { @@ -10,6 +17,7 @@ export const undefinedToNullResolver = (list) => { return resolvers } +// eslint-disable-next-line @typescript-eslint/no-explicit-any export default function Resolver(type, options: any = {}) { const { idAttribute = 'id', @@ -20,8 +28,8 @@ export default function Resolver(type, options: any = {}) { hasMany = {}, } = options - const _hasResolver = (resolvers, { key, connection }, { returnType }) => { - return async (parent, params, { driver, cypherParams }, resolveInfo) => { + const _hasResolver = (_resolvers, { key, connection }, { returnType }) => { + return async (parent, _params, { driver, cypherParams }, _resolveInfo) => { if (typeof parent[key] !== 'undefined') return parent[key] const id = parent[idAttribute] const session = driver.session() @@ -31,7 +39,6 @@ export default function Resolver(type, options: any = {}) { RETURN related {.*} as related ` const result = await txc.run(cypher, { id, cypherParams }) - log(result) return result.records.map((r) => r.get('related')) }) try { @@ -44,10 +51,11 @@ export default function Resolver(type, options: any = {}) { } } + // eslint-disable-next-line @typescript-eslint/no-explicit-any const booleanResolver = (obj: any[]) => { const resolvers = {} for (const [key, condition] of Object.entries(obj)) { - resolvers[key] = async (parent, params, { cypherParams, driver }, resolveInfo) => { + resolvers[key] = async (parent, _params, { cypherParams, driver }, _resolveInfo) => { if (typeof parent[key] !== 'undefined') return parent[key] const id = parent[idAttribute] const session = driver.session() @@ -55,7 +63,6 @@ export default function Resolver(type, options: any = {}) { const nodeCondition = condition.replace('this', 'this {id: $id}') const cypher = `${nodeCondition} as ${key}` const result = await txc.run(cypher, { id, cypherParams }) - log(result) const [response] = result.records.map((r) => r.get(key)) return response }) @@ -72,7 +79,7 @@ export default function Resolver(type, options: any = {}) { const countResolver = (obj) => { const resolvers = {} for (const [key, connection] of Object.entries(obj)) { - resolvers[key] = async (parent, params, { driver, cypherParams }, resolveInfo) => { + resolvers[key] = async (parent, _params, { driver, cypherParams }, _resolveInfo) => { if (typeof parent[key] !== 'undefined') return parent[key] const session = driver.session() const readTxResultPromise = session.readTransaction(async (txc) => { @@ -82,7 +89,6 @@ export default function Resolver(type, options: any = {}) { RETURN COUNT(DISTINCT(related)) as count ` const result = await txc.run(cypher, { id, cypherParams }) - log(result) const [response] = result.records.map((r) => r.get('count').toNumber()) return response }) diff --git a/backend/src/schema/resolvers/helpers/createPasswordReset.ts b/backend/src/graphql/resolvers/helpers/createPasswordReset.ts similarity index 84% rename from backend/src/schema/resolvers/helpers/createPasswordReset.ts rename to backend/src/graphql/resolvers/helpers/createPasswordReset.ts index ec0349c18..0727c5d4e 100644 --- a/backend/src/schema/resolvers/helpers/createPasswordReset.ts +++ b/backend/src/graphql/resolvers/helpers/createPasswordReset.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import normalizeEmail from './normalizeEmail' export default async function createPasswordReset(options) { diff --git a/backend/src/schema/resolvers/helpers/events.ts b/backend/src/graphql/resolvers/helpers/events.ts similarity index 85% rename from backend/src/schema/resolvers/helpers/events.ts rename to backend/src/graphql/resolvers/helpers/events.ts index d4fc1fb11..3e5f8d5a8 100644 --- a/backend/src/schema/resolvers/helpers/events.ts +++ b/backend/src/graphql/resolvers/helpers/events.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { UserInputError } from 'apollo-server' export const validateEventParams = (params) => { @@ -18,7 +22,7 @@ export const validateEventParams = (params) => { throw new UserInputError('Event venue must be present if event location is given!') } params.eventVenue = eventInput.eventVenue - params.eventLocationName = eventInput.eventLocationName && eventInput.eventLocationName.trim() + params.eventLocationName = eventInput.eventLocationName?.trim() if (params.eventLocationName) { locationName = params.eventLocationName } else { diff --git a/backend/src/schema/resolvers/helpers/existingEmailAddress.ts b/backend/src/graphql/resolvers/helpers/existingEmailAddress.ts similarity index 76% rename from backend/src/schema/resolvers/helpers/existingEmailAddress.ts rename to backend/src/graphql/resolvers/helpers/existingEmailAddress.ts index 288a14a6d..03d902881 100644 --- a/backend/src/schema/resolvers/helpers/existingEmailAddress.ts +++ b/backend/src/graphql/resolvers/helpers/existingEmailAddress.ts @@ -1,4 +1,11 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +import normalizeEmail from './normalizeEmail' + export default async function alreadyExistingMail({ args, context }) { + args.email = normalizeEmail(args.email) const session = context.driver.session() try { const existingEmailAddressTxPromise = session.writeTransaction(async (transaction) => { diff --git a/backend/src/schema/resolvers/helpers/filterForMutedUsers.ts b/backend/src/graphql/resolvers/helpers/filterForMutedUsers.ts similarity index 65% rename from backend/src/schema/resolvers/helpers/filterForMutedUsers.ts rename to backend/src/graphql/resolvers/helpers/filterForMutedUsers.ts index 1d1369e0d..967ed2265 100644 --- a/backend/src/schema/resolvers/helpers/filterForMutedUsers.ts +++ b/backend/src/graphql/resolvers/helpers/filterForMutedUsers.ts @@ -1,6 +1,11 @@ -import { getMutedUsers } from '../users' +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { mergeWith, isArray } from 'lodash' +import { getMutedUsers } from '@graphql/resolvers/users' + export const filterForMutedUsers = async (params, context) => { if (!context.user) return params const [mutedUsers] = await Promise.all([getMutedUsers(context)]) diff --git a/backend/src/schema/resolvers/helpers/filterInvisiblePosts.ts b/backend/src/graphql/resolvers/helpers/filterInvisiblePosts.ts similarity index 83% rename from backend/src/schema/resolvers/helpers/filterInvisiblePosts.ts rename to backend/src/graphql/resolvers/helpers/filterInvisiblePosts.ts index 73dfaad91..2a264ced4 100644 --- a/backend/src/schema/resolvers/helpers/filterInvisiblePosts.ts +++ b/backend/src/graphql/resolvers/helpers/filterInvisiblePosts.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { mergeWith, isArray } from 'lodash' const getInvisiblePosts = async (context) => { @@ -5,7 +9,7 @@ const getInvisiblePosts = async (context) => { const readTxResultPromise = await session.readTransaction(async (transaction) => { let cypher = '' const { user } = context - if (user && user.id) { + if (user?.id) { cypher = ` MATCH (post:Post)<-[:CANNOT_SEE]-(user:User { id: $userId }) RETURN collect(post.id) AS invisiblePostIds` diff --git a/backend/src/schema/resolvers/helpers/filterPostsOfMyGroups.ts b/backend/src/graphql/resolvers/helpers/filterPostsOfMyGroups.ts similarity index 78% rename from backend/src/schema/resolvers/helpers/filterPostsOfMyGroups.ts rename to backend/src/graphql/resolvers/helpers/filterPostsOfMyGroups.ts index a808a5582..9d40b097e 100644 --- a/backend/src/schema/resolvers/helpers/filterPostsOfMyGroups.ts +++ b/backend/src/graphql/resolvers/helpers/filterPostsOfMyGroups.ts @@ -1,8 +1,12 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { mergeWith, isArray } from 'lodash' const getMyGroupIds = async (context) => { const { user } = context - if (!(user && user.id)) return [] + if (!user?.id) return [] const session = context.driver.session() const readTxResultPromise = await session.readTransaction(async (transaction) => { @@ -22,7 +26,7 @@ const getMyGroupIds = async (context) => { } export const filterPostsOfMyGroups = async (params, context) => { - if (!(params.filter && params.filter.postsInMyGroups)) return params + if (!params.filter?.postsInMyGroups) return params delete params.filter.postsInMyGroups const myGroupIds = await getMyGroupIds(context) params.filter = mergeWith( diff --git a/backend/src/schema/resolvers/helpers/generateNonce.ts b/backend/src/graphql/resolvers/helpers/generateNonce.ts similarity index 66% rename from backend/src/schema/resolvers/helpers/generateNonce.ts rename to backend/src/graphql/resolvers/helpers/generateNonce.ts index f08b3ccd6..b7585b24f 100644 --- a/backend/src/schema/resolvers/helpers/generateNonce.ts +++ b/backend/src/graphql/resolvers/helpers/generateNonce.ts @@ -1,9 +1,9 @@ -import CONSTANTS_REGISTRATION from './../../../constants/registration' +import registrationConstants from '@constants/registrationBranded' // TODO: why this is not used in resolver 'requestPasswordReset'? export default function generateNonce() { return Array.from( - { length: CONSTANTS_REGISTRATION.NONCE_LENGTH }, + { length: registrationConstants.NONCE_LENGTH }, (n: number = Math.floor(Math.random() * 10)) => { return String.fromCharCode(n + 48) }, diff --git a/backend/src/schema/resolvers/helpers/normalizeEmail.ts b/backend/src/graphql/resolvers/helpers/normalizeEmail.ts similarity index 75% rename from backend/src/schema/resolvers/helpers/normalizeEmail.ts rename to backend/src/graphql/resolvers/helpers/normalizeEmail.ts index bc13467c3..9b6be73d7 100644 --- a/backend/src/schema/resolvers/helpers/normalizeEmail.ts +++ b/backend/src/graphql/resolvers/helpers/normalizeEmail.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import { normalizeEmail } from 'validator' export default (email) => diff --git a/backend/src/schema/resolvers/images.ts b/backend/src/graphql/resolvers/images.ts similarity index 99% rename from backend/src/schema/resolvers/images.ts rename to backend/src/graphql/resolvers/images.ts index 111f84888..ea596a183 100644 --- a/backend/src/schema/resolvers/images.ts +++ b/backend/src/graphql/resolvers/images.ts @@ -1,4 +1,5 @@ import Resolver from './helpers/Resolver' + export default { Image: { ...Resolver('Image', { diff --git a/backend/src/graphql/resolvers/images/images.ts b/backend/src/graphql/resolvers/images/images.ts new file mode 100644 index 000000000..6c2fa8b3a --- /dev/null +++ b/backend/src/graphql/resolvers/images/images.ts @@ -0,0 +1,58 @@ +import CONFIG, { isS3configured } from '@config/index' + +import { images as imagesLocal } from './imagesLocal' +import { images as imagesS3 } from './imagesS3' + +import type { FileUpload } from 'graphql-upload' +import type { Transaction } from 'neo4j-driver' + +export type FileDeleteCallback = (url: string) => Promise + +export type FileUploadCallback = ( + upload: Pick & { uniqueFilename: string }, +) => Promise +export interface DeleteImageOpts { + transaction?: Transaction + deleteCallback?: FileDeleteCallback +} + +export interface MergeImageOpts { + transaction?: Transaction + uploadCallback?: FileUploadCallback + deleteCallback?: FileDeleteCallback +} + +export interface ImageInput { + upload?: Promise + alt?: string + sensitive?: boolean + aspectRatio?: number + type?: string +} + +export interface Image { + url: string + alt?: string + sensitive?: boolean + aspectRatio?: number + type?: string +} + +export interface Images { + deleteImage: ( + resource: { id: string }, + relationshipType: 'HERO_IMAGE' | 'AVATAR_IMAGE', + opts?: DeleteImageOpts, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ) => Promise + + mergeImage: ( + resource: { id: string }, + relationshipType: 'HERO_IMAGE' | 'AVATAR_IMAGE', + imageInput: ImageInput | null | undefined, + opts?: MergeImageOpts, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ) => Promise +} + +export const images = isS3configured(CONFIG) ? imagesS3(CONFIG) : imagesLocal diff --git a/backend/src/schema/resolvers/images/images.spec.ts b/backend/src/graphql/resolvers/images/imagesLocal.spec.ts similarity index 76% rename from backend/src/schema/resolvers/images/images.spec.ts rename to backend/src/graphql/resolvers/images/imagesLocal.spec.ts index d46972ce0..257a24e78 100644 --- a/backend/src/schema/resolvers/images/images.spec.ts +++ b/backend/src/graphql/resolvers/images/imagesLocal.spec.ts @@ -1,8 +1,21 @@ -import { deleteImage, mergeImage } from './images' -import { getNeode, getDriver } from '../../../db/neo4j' -import Factory, { cleanDatabase } from '../../../db/factories' +/* eslint-disable @typescript-eslint/require-await */ + +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable promise/prefer-await-to-callbacks */ import { UserInputError } from 'apollo-server' +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' + +import { images } from './imagesLocal' + +import type { ImageInput } from './images' +import type { FileUpload } from 'graphql-upload' + const driver = getDriver() const neode = getNeode() const uuid = '[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}' @@ -15,7 +28,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { @@ -29,31 +42,33 @@ afterEach(async () => { }) describe('deleteImage', () => { + const { deleteImage } = images + describe('given a resource with an image', () => { - let user + let user: { id: string } beforeEach(async () => { - user = await Factory.build( + const u = await Factory.build( 'user', {}, { avatar: Factory.build('image', { - url: '/some/avatar/url/', + url: 'http://localhost/some/avatar/url/', alt: 'This is the avatar image of a user', }), }, ) - user = await user.toJson() + user = await u.toJson() }) - it('soft deletes `Image` node', async () => { + it('deletes `Image` node', async () => { await expect(neode.all('Image')).resolves.toHaveLength(1) await deleteImage(user, 'AVATAR_IMAGE', { deleteCallback }) await expect(neode.all('Image')).resolves.toHaveLength(0) }) it('calls deleteCallback', async () => { - user = await Factory.build('user') - user = await user.toJson() + const u = await Factory.build('user') + user = await u.toJson() await deleteImage(user, 'AVATAR_IMAGE', { deleteCallback }) expect(deleteCallback).toHaveBeenCalled() }) @@ -61,7 +76,7 @@ describe('deleteImage', () => { describe('given a transaction parameter', () => { it('executes cypher statements within the transaction', async () => { const session = driver.session() - let someString + let someString: string try { someString = await session.writeTransaction(async (transaction) => { await deleteImage(user, 'AVATAR_IMAGE', { @@ -73,10 +88,10 @@ describe('deleteImage', () => { return result }) } finally { - session.close() + await session.close() } await expect(neode.all('Image')).resolves.toHaveLength(0) - await expect(someString).toEqual('Hello') + expect(someString).toEqual('Hello') }) it('rolls back the transaction in case of errors', async () => { @@ -90,12 +105,13 @@ describe('deleteImage', () => { }) throw new Error('Ouch!') }) + // eslint-disable-next-line no-catch-all/no-catch-all } catch (err) { // nothing has been deleted await expect(neode.all('Image')).resolves.toHaveLength(1) // all good } finally { - session.close() + await session.close() } }) }) @@ -103,8 +119,9 @@ describe('deleteImage', () => { }) describe('mergeImage', () => { - let imageInput - let post + const { mergeImage } = images + let imageInput: ImageInput + let post: { id: string } beforeEach(() => { imageInput = { alt: 'A description of the new image', @@ -113,24 +130,25 @@ describe('mergeImage', () => { describe('given image.upload', () => { beforeEach(() => { + const createReadStream: FileUpload['createReadStream'] = (() => ({ + pipe: () => ({ + on: (_, callback) => callback(), + }), + })) as unknown as FileUpload['createReadStream'] imageInput = { ...imageInput, - upload: { + upload: Promise.resolve({ filename: 'image.jpg', mimetype: 'image/jpeg', encoding: '7bit', - createReadStream: () => ({ - pipe: () => ({ - on: (_, callback) => callback(), - }), - }), - }, + createReadStream, + }), } }) describe('on existing resource', () => { beforeEach(async () => { - post = await Factory.build( + const p = await Factory.build( 'post', { id: 'p99' }, { @@ -138,7 +156,7 @@ describe('mergeImage', () => { image: null, }, ) - post = await post.toJson() + post = await p.toJson() }) it('returns new image', async () => { @@ -162,7 +180,12 @@ describe('mergeImage', () => { }) it('creates a url safe name', async () => { - imageInput.upload.filename = '/path/to/awkward?/ file-location/?foo- bar-avatar.jpg' + if (!imageInput.upload) { + throw new Error('Test imageInput was not setup correctly.') + } + const upload = await imageInput.upload + upload.filename = '/path/to/awkward?/ file-location/?foo- bar-avatar.jpg' + imageInput.upload = Promise.resolve(upload) await expect( mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }), ).resolves.toMatchObject({ @@ -170,27 +193,13 @@ describe('mergeImage', () => { }) }) - // eslint-disable-next-line jest/no-disabled-tests - it.skip('automatically creates different image sizes', async () => { - await expect( - mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }), - ).resolves.toEqual({ - url: expect.any(String), - alt: expect.any(String), - urlW34: expect.stringMatching(new RegExp(`^/uploads/W34/${uuid}-image.jpg`)), - urlW160: expect.stringMatching(new RegExp(`^/uploads/W160/${uuid}-image.jpg`)), - urlW320: expect.stringMatching(new RegExp(`^/uploads/W320/${uuid}-image.jpg`)), - urlW640: expect.stringMatching(new RegExp(`^/uploads/W640/${uuid}-image.jpg`)), - urlW1024: expect.stringMatching(new RegExp(`^/uploads/W1024/${uuid}-image.jpg`)), - }) - }) - it('connects resource with image via given image type', async () => { await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }) - const result = await neode.cypher(` - MATCH(p:Post {id: "p99"})-[:HERO_IMAGE]->(i:Image) RETURN i,p - `) - post = neode.hydrateFirst(result, 'p', neode.model('Post')) + const result = await neode.cypher( + `MATCH(p:Post {id: "p99"})-[:HERO_IMAGE]->(i:Image) RETURN i,p`, + {}, + ) + post = neode.hydrateFirst<{ id: string }>(result, 'p', neode.model('Post')).properties() const image = neode.hydrateFirst(result, 'i', neode.model('Image')) expect(post).toBeTruthy() expect(image).toBeTruthy() @@ -198,13 +207,16 @@ describe('mergeImage', () => { it('whitelists relationship types', async () => { await expect( - mergeImage(post, 'WHATEVER', imageInput, { uploadCallback, deleteCallback }), + mergeImage(post, 'WHATEVER' as 'HERO_IMAGE', imageInput, { + uploadCallback, + deleteCallback, + }), ).rejects.toEqual(new Error('Unknown relationship type WHATEVER')) }) it('sets metadata', async () => { await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }) - const image = await neode.first('Image', {}) + const image = await neode.first('Image', {}, undefined) await expect(image.toJson()).resolves.toMatchObject({ alt: 'A description of the new image', createdAt: expect.any(String), @@ -232,9 +244,13 @@ describe('mergeImage', () => { ) }) } finally { - session.close() + await session.close() } - const image = await neode.first('Image', { alt: 'This alt text gets overwritten' }) + const image = await neode.first( + 'Image', + { alt: 'This alt text gets overwritten' }, + undefined, + ) await expect(image.toJson()).resolves.toMatchObject({ alt: 'This alt text gets overwritten', }) @@ -251,12 +267,13 @@ describe('mergeImage', () => { }) return transaction.run('Ooops invalid cypher!', { image }) }) + // eslint-disable-next-line no-catch-all/no-catch-all } catch (err) { // nothing has been created await expect(neode.all('Image')).resolves.toHaveLength(0) // all good } finally { - session.close() + await session.close() } }) }) @@ -284,7 +301,7 @@ describe('mergeImage', () => { await expect(neode.all('Image')).resolves.toHaveLength(1) await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }) await expect(neode.all('Image')).resolves.toHaveLength(1) - const image = await neode.first('Image', {}) + const image = await neode.first('Image', {}, undefined) await expect(image.toJson()).resolves.toMatchObject({ alt: 'A description of the new image', createdAt: expect.any(String), @@ -300,8 +317,8 @@ describe('mergeImage', () => { describe('without image.upload', () => { it('throws UserInputError', async () => { - post = await Factory.build('post', { id: 'p99' }, { image: null }) - post = await post.toJson() + const p = await Factory.build('post', { id: 'p99' }, { image: null }) + post = await p.toJson() await expect(mergeImage(post, 'HERO_IMAGE', imageInput)).rejects.toEqual( new UserInputError('Cannot find image for given resource'), ) @@ -309,7 +326,7 @@ describe('mergeImage', () => { describe('if resource has an image already', () => { beforeEach(async () => { - post = await Factory.build( + const p = await Factory.build( 'post', { id: 'p99', @@ -324,11 +341,11 @@ describe('mergeImage', () => { ), image: Factory.build('image', { alt: 'This is the previous, not updated image', - url: '/some/original/url', + url: 'http://localhost/some/original/url', }), }, ) - post = await post.toJson() + post = await p.toJson() }) it('does not call deleteCallback', async () => { diff --git a/backend/src/schema/resolvers/images/images.ts b/backend/src/graphql/resolvers/images/imagesLocal.ts similarity index 58% rename from backend/src/schema/resolvers/images/images.ts rename to backend/src/graphql/resolvers/images/imagesLocal.ts index b99b13a10..c9f575777 100644 --- a/backend/src/schema/resolvers/images/images.ts +++ b/backend/src/graphql/resolvers/images/imagesLocal.ts @@ -1,16 +1,27 @@ -import path from 'path' -import { v4 as uuid } from 'uuid' -import { S3 } from 'aws-sdk' -import slug from 'slug' -import { existsSync, unlinkSync, createWriteStream } from 'fs' +/* eslint-disable @typescript-eslint/require-await */ + +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable promise/avoid-new */ +/* eslint-disable security/detect-non-literal-fs-filename */ + +import { existsSync, unlinkSync, createWriteStream } from 'node:fs' +import path from 'node:path' + import { UserInputError } from 'apollo-server' -import { getDriver } from '../../../db/neo4j' -import CONFIG from '../../../config' +import slug from 'slug' +import { v4 as uuid } from 'uuid' -// const widths = [34, 160, 320, 640, 1024] -const { AWS_ENDPOINT: endpoint, AWS_REGION: region, AWS_BUCKET: Bucket, S3_CONFIGURED } = CONFIG +import { sanitizeRelationshipType } from './sanitizeRelationshipTypes' +import { wrapTransaction } from './wrapTransaction' -export async function deleteImage(resource, relationshipType, opts: any = {}) { +import type { Images, FileDeleteCallback, FileUploadCallback } from './images' +import type { FileUpload } from 'graphql-upload' + +const deleteImage: Images['deleteImage'] = async (resource, relationshipType, opts = {}) => { sanitizeRelationshipType(relationshipType) const { transaction, deleteCallback } = opts if (!transaction) return wrapTransaction(deleteImage, [resource, relationshipType], opts) @@ -32,7 +43,12 @@ export async function deleteImage(resource, relationshipType, opts: any = {}) { return image } -export async function mergeImage(resource, relationshipType, imageInput, opts: any = {}) { +const mergeImage: Images['mergeImage'] = async ( + resource, + relationshipType, + imageInput, + opts = {}, +) => { if (typeof imageInput === 'undefined') return if (imageInput === null) return deleteImage(resource, relationshipType, opts) sanitizeRelationshipType(relationshipType) @@ -70,47 +86,38 @@ export async function mergeImage(resource, relationshipType, imageInput, opts: a return mergedImage } -const wrapTransaction = async (wrappedCallback, args, opts) => { - const session = getDriver().session() - try { - const result = await session.writeTransaction(async (transaction) => { - return wrappedCallback(...args, { ...opts, transaction }) - }) - return result - } finally { - session.close() - } +const localFileDelete: FileDeleteCallback = async (url) => { + const location = `public${url}` + // eslint-disable-next-line n/no-sync + if (existsSync(location)) unlinkSync(location) } -const deleteImageFile = (image, deleteCallback) => { +const deleteImageFile = (image, deleteCallback: FileDeleteCallback | undefined) => { if (!deleteCallback) { - deleteCallback = S3_CONFIGURED ? s3Delete : localFileDelete + deleteCallback = localFileDelete } const { url } = image + // eslint-disable-next-line @typescript-eslint/no-floating-promises deleteCallback(url) return url } -const uploadImageFile = async (upload, uploadCallback) => { +const uploadImageFile = async ( + upload: Promise | undefined, + uploadCallback: FileUploadCallback | undefined, +) => { if (!upload) return undefined if (!uploadCallback) { - uploadCallback = S3_CONFIGURED ? s3Upload : localFileUpload + uploadCallback = localFileUpload } + // eslint-disable-next-line @typescript-eslint/unbound-method const { createReadStream, filename, mimetype } = await upload const { name, ext } = path.parse(filename) const uniqueFilename = `${uuid()}-${slug(name)}${ext}` return uploadCallback({ createReadStream, uniqueFilename, mimetype }) } -const sanitizeRelationshipType = (relationshipType) => { - // Cypher query language does not allow to parameterize relationship types - // See: https://github.com/neo4j/neo4j/issues/340 - if (!['HERO_IMAGE', 'AVATAR_IMAGE'].includes(relationshipType)) { - throw new Error(`Unknown relationship type ${relationshipType}`) - } -} - -const localFileUpload = ({ createReadStream, uniqueFilename }) => { +const localFileUpload: FileUploadCallback = ({ createReadStream, uniqueFilename }) => { const destination = `/uploads/${uniqueFilename}` return new Promise((resolve, reject) => createReadStream().pipe( @@ -121,34 +128,7 @@ const localFileUpload = ({ createReadStream, uniqueFilename }) => { ) } -const s3Upload = async ({ createReadStream, uniqueFilename, mimetype }) => { - const s3 = new S3({ region, endpoint }) - const s3Location = `original/${uniqueFilename}` - - const params = { - Bucket, - Key: s3Location, - ACL: 'public-read', - ContentType: mimetype, - Body: createReadStream(), - } - const data = await s3.upload(params).promise() - const { Location } = data - return Location -} - -const localFileDelete = async (url) => { - const location = `public${url}` - if (existsSync(location)) unlinkSync(location) -} - -const s3Delete = async (url) => { - const s3 = new S3({ region, endpoint }) - let { pathname } = new URL(url, 'http://example.org') // dummy domain to avoid invalid URL error - pathname = pathname.substring(1) // remove first character '/' - const params = { - Bucket, - Key: pathname, - } - await s3.deleteObject(params).promise() +export const images: Images = { + deleteImage, + mergeImage, } diff --git a/backend/src/graphql/resolvers/images/imagesS3.spec.ts b/backend/src/graphql/resolvers/images/imagesS3.spec.ts new file mode 100644 index 000000000..2bedec3cd --- /dev/null +++ b/backend/src/graphql/resolvers/images/imagesS3.spec.ts @@ -0,0 +1,407 @@ +/* eslint-disable @typescript-eslint/require-await */ + +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable promise/prefer-await-to-callbacks */ +import { UserInputError } from 'apollo-server' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import type { S3Configured } from '@src/config' + +import { images } from './imagesS3' + +import type { ImageInput } from './images' +import type { FileUpload } from 'graphql-upload' + +const driver = getDriver() +const neode = getNeode() +const uuid = '[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}' +let uploadCallback +let deleteCallback + +const config: S3Configured = { + AWS_ACCESS_KEY_ID: 'AWS_ACCESS_KEY_ID', + AWS_SECRET_ACCESS_KEY: 'AWS_SECRET_ACCESS_KEY', + AWS_BUCKET: 'AWS_BUCKET', + AWS_ENDPOINT: 'AWS_ENDPOINT', + AWS_REGION: 'AWS_REGION', + S3_PUBLIC_GATEWAY: undefined, +} + +beforeAll(async () => { + await cleanDatabase() +}) + +afterAll(async () => { + await cleanDatabase() + await driver.close() +}) + +beforeEach(async () => { + uploadCallback = jest.fn( + ({ uniqueFilename }) => `http://your-objectstorage.com/bucket/${uniqueFilename}`, + ) + deleteCallback = jest.fn() +}) + +// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 +afterEach(async () => { + await cleanDatabase() +}) + +describe('deleteImage', () => { + const { deleteImage } = images(config) + describe('given a resource with an image', () => { + let user: { id: string } + beforeEach(async () => { + const u = await Factory.build( + 'user', + {}, + { + avatar: Factory.build('image', { + url: 'http://localhost/some/avatar/url/', + alt: 'This is the avatar image of a user', + }), + }, + ) + user = await u.toJson() + }) + + it('deletes `Image` node', async () => { + await expect(neode.all('Image')).resolves.toHaveLength(1) + await deleteImage(user, 'AVATAR_IMAGE', { deleteCallback }) + await expect(neode.all('Image')).resolves.toHaveLength(0) + }) + + it('calls deleteCallback', async () => { + await deleteImage(user, 'AVATAR_IMAGE', { deleteCallback }) + expect(deleteCallback).toHaveBeenCalled() + }) + + describe('given a transaction parameter', () => { + it('executes cypher statements within the transaction', async () => { + const session = driver.session() + let someString: string + try { + someString = await session.writeTransaction(async (transaction) => { + await deleteImage(user, 'AVATAR_IMAGE', { + deleteCallback, + transaction, + }) + const txResult = await transaction.run('RETURN "Hello" as result') + const [result] = txResult.records.map((record) => record.get('result')) + return result + }) + } finally { + await session.close() + } + await expect(neode.all('Image')).resolves.toHaveLength(0) + expect(someString).toEqual('Hello') + }) + + it('rolls back the transaction in case of errors', async () => { + await expect(neode.all('Image')).resolves.toHaveLength(1) + const session = driver.session() + try { + await session.writeTransaction(async (transaction) => { + await deleteImage(user, 'AVATAR_IMAGE', { + deleteCallback, + transaction, + }) + throw new Error('Ouch!') + }) + // eslint-disable-next-line no-catch-all/no-catch-all + } catch (err) { + // nothing has been deleted + await expect(neode.all('Image')).resolves.toHaveLength(1) + // all good + } finally { + await session.close() + } + }) + }) + }) +}) + +describe('mergeImage', () => { + const { mergeImage } = images(config) + let imageInput: ImageInput + let post: { id: string } + beforeEach(() => { + imageInput = { + alt: 'A description of the new image', + } + }) + + describe('given image.upload', () => { + beforeEach(() => { + const createReadStream: FileUpload['createReadStream'] = (() => ({ + pipe: () => ({ + on: (_, callback) => callback(), + }), + })) as unknown as FileUpload['createReadStream'] + imageInput = { + ...imageInput, + upload: Promise.resolve({ + filename: 'image.jpg', + mimetype: 'image/jpeg', + encoding: '7bit', + createReadStream, + }), + } + }) + + describe('on existing resource', () => { + beforeEach(async () => { + const p = await Factory.build( + 'post', + { id: 'p99' }, + { + author: Factory.build('user', {}, { avatar: null }), + image: null, + }, + ) + post = await p.toJson() + }) + + it('returns new image', async () => { + await expect( + mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }), + ).resolves.toMatchObject({ + url: expect.any(String), + alt: 'A description of the new image', + }) + }) + + it('calls upload callback', async () => { + await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }) + expect(uploadCallback).toHaveBeenCalled() + }) + + it('creates `:Image` node', async () => { + await expect(neode.all('Image')).resolves.toHaveLength(0) + await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }) + await expect(neode.all('Image')).resolves.toHaveLength(1) + }) + + it('creates a url safe name', async () => { + if (!imageInput.upload) { + throw new Error('Test imageInput was not setup correctly.') + } + const upload = await imageInput.upload + upload.filename = '/path/to/awkward?/ file-location/?foo- bar-avatar.jpg' + imageInput.upload = Promise.resolve(upload) + await expect( + mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }), + ).resolves.toMatchObject({ + url: expect.stringMatching( + new RegExp(`^http://your-objectstorage.com/bucket/${uuid}-foo-bar-avatar.jpg`), + ), + }) + }) + + describe('given a `S3_PUBLIC_GATEWAY` configuration', () => { + const { mergeImage } = images({ + ...config, + S3_PUBLIC_GATEWAY: 'http://s3-public-gateway.com', + }) + + it('changes the domain of the URL to a server that could e.g. apply image transformations', async () => { + if (!imageInput.upload) { + throw new Error('Test imageInput was not setup correctly.') + } + const upload = await imageInput.upload + upload.filename = '/path/to/file-location/foo-bar-avatar.jpg' + imageInput.upload = Promise.resolve(upload) + await expect( + mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }), + ).resolves.toMatchObject({ + url: expect.stringMatching( + new RegExp(`^http://s3-public-gateway.com/bucket/${uuid}-foo-bar-avatar.jpg`), + ), + }) + }) + }) + + it('connects resource with image via given image type', async () => { + await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }) + const result = await neode.cypher( + `MATCH(p:Post {id: "p99"})-[:HERO_IMAGE]->(i:Image) RETURN i,p`, + {}, + ) + post = neode.hydrateFirst<{ id: string }>(result, 'p', neode.model('Post')).properties() + const image = neode.hydrateFirst(result, 'i', neode.model('Image')) + expect(post).toBeTruthy() + expect(image).toBeTruthy() + }) + + it('whitelists relationship types', async () => { + await expect( + mergeImage(post, 'WHATEVER' as 'HERO_IMAGE', imageInput, { + uploadCallback, + deleteCallback, + }), + ).rejects.toEqual(new Error('Unknown relationship type WHATEVER')) + }) + + it('sets metadata', async () => { + await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }) + const image = await neode.first('Image', {}, undefined) + await expect(image.toJson()).resolves.toMatchObject({ + alt: 'A description of the new image', + createdAt: expect.any(String), + url: expect.any(String), + }) + }) + + describe('given a transaction parameter', () => { + it('executes cypher statements within the transaction', async () => { + const session = driver.session() + try { + await session.writeTransaction(async (transaction) => { + const image = await mergeImage(post, 'HERO_IMAGE', imageInput, { + uploadCallback, + deleteCallback, + transaction, + }) + return transaction.run( + ` + MATCH(image:Image {url: $image.url}) + SET image.alt = 'This alt text gets overwritten' + RETURN image {.*} + `, + { image }, + ) + }) + } finally { + await session.close() + } + const image = await neode.first( + 'Image', + { alt: 'This alt text gets overwritten' }, + undefined, + ) + await expect(image.toJson()).resolves.toMatchObject({ + alt: 'This alt text gets overwritten', + }) + }) + + it('rolls back the transaction in case of errors', async () => { + const session = driver.session() + try { + await session.writeTransaction(async (transaction) => { + const image = await mergeImage(post, 'HERO_IMAGE', imageInput, { + uploadCallback, + deleteCallback, + transaction, + }) + return transaction.run('Ooops invalid cypher!', { image }) + }) + // eslint-disable-next-line no-catch-all/no-catch-all + } catch (err) { + // nothing has been created + await expect(neode.all('Image')).resolves.toHaveLength(0) + // all good + } finally { + await session.close() + } + }) + }) + + describe('if resource has an image already', () => { + beforeEach(async () => { + const [post, image] = await Promise.all([ + neode.find('Post', 'p99'), + Factory.build('image'), + ]) + await post.relateTo(image, 'image') + }) + + it('calls deleteCallback', async () => { + await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }) + expect(deleteCallback).toHaveBeenCalled() + }) + + it('calls uploadCallback', async () => { + await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }) + expect(uploadCallback).toHaveBeenCalled() + }) + + it('updates metadata of existing image node', async () => { + await expect(neode.all('Image')).resolves.toHaveLength(1) + await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }) + await expect(neode.all('Image')).resolves.toHaveLength(1) + const image = await neode.first('Image', {}, undefined) + await expect(image.toJson()).resolves.toMatchObject({ + alt: 'A description of the new image', + createdAt: expect.any(String), + url: expect.any(String), + // TODO + // width: + // height: + }) + }) + }) + }) + }) + + describe('without image.upload', () => { + it('throws UserInputError', async () => { + const p = await Factory.build('post', { id: 'p99' }, { image: null }) + post = await p.toJson() + await expect(mergeImage(post, 'HERO_IMAGE', imageInput)).rejects.toEqual( + new UserInputError('Cannot find image for given resource'), + ) + }) + + describe('if resource has an image already', () => { + beforeEach(async () => { + const p = await Factory.build( + 'post', + { + id: 'p99', + }, + { + author: Factory.build( + 'user', + {}, + { + avatar: null, + }, + ), + image: Factory.build('image', { + alt: 'This is the previous, not updated image', + url: 'http://localhost/some/original/url', + }), + }, + ) + post = await p.toJson() + }) + + it('does not call deleteCallback', async () => { + await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }) + expect(deleteCallback).not.toHaveBeenCalled() + }) + + it('does not call uploadCallback', async () => { + await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }) + expect(uploadCallback).not.toHaveBeenCalled() + }) + + it('updates metadata', async () => { + await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }) + const images = await neode.all('Image') + expect(images).toHaveLength(1) + await expect(images.first().toJson()).resolves.toMatchObject({ + createdAt: expect.any(String), + url: expect.any(String), + alt: 'A description of the new image', + }) + }) + }) + }) +}) diff --git a/backend/src/graphql/resolvers/images/imagesS3.ts b/backend/src/graphql/resolvers/images/imagesS3.ts new file mode 100644 index 000000000..66c4a0a69 --- /dev/null +++ b/backend/src/graphql/resolvers/images/imagesS3.ts @@ -0,0 +1,153 @@ +import path from 'node:path' + +import { S3Client, DeleteObjectCommand, ObjectCannedACL } from '@aws-sdk/client-s3' +import { Upload } from '@aws-sdk/lib-storage' +import { UserInputError } from 'apollo-server' +import slug from 'slug' +import { v4 as uuid } from 'uuid' + +import { S3Configured } from '@config/index' + +import { sanitizeRelationshipType } from './sanitizeRelationshipTypes' +import { wrapTransaction } from './wrapTransaction' + +import type { Image, Images, FileDeleteCallback, FileUploadCallback } from './images' +import type { FileUpload } from 'graphql-upload' + +export const images = (config: S3Configured) => { + // const widths = [34, 160, 320, 640, 1024] + const { AWS_BUCKET: Bucket, S3_PUBLIC_GATEWAY } = config + + const { AWS_ENDPOINT, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY } = config + const s3 = new S3Client({ + credentials: { + accessKeyId: AWS_ACCESS_KEY_ID, + secretAccessKey: AWS_SECRET_ACCESS_KEY, + }, + endpoint: AWS_ENDPOINT, + forcePathStyle: true, + }) + + const deleteImage: Images['deleteImage'] = async (resource, relationshipType, opts = {}) => { + sanitizeRelationshipType(relationshipType) + const { transaction, deleteCallback = s3Delete } = opts + if (!transaction) return wrapTransaction(deleteImage, [resource, relationshipType], opts) + const txResult = await transaction.run( + ` + MATCH (resource {id: $resource.id})-[rel:${relationshipType}]->(image:Image) + WITH image, image {.*} as imageProps + DETACH DELETE image + RETURN imageProps + `, + { resource }, + ) + const [image] = txResult.records.map((record) => record.get('imageProps') as Image) + // This behaviour differs from `mergeImage`. If you call `mergeImage` + // with metadata for an image that does not exist, it's an indicator + // of an error (so throw an error). If we bulk delete an image, it + // could very well be that there is no image for the resource. + if (image) { + await deleteCallback(image.url) + } + return image + } + + const mergeImage: Images['mergeImage'] = async ( + resource, + relationshipType, + imageInput, + opts = {}, + ) => { + if (typeof imageInput === 'undefined') return + if (imageInput === null) return deleteImage(resource, relationshipType, opts) + sanitizeRelationshipType(relationshipType) + const { transaction, uploadCallback, deleteCallback = s3Delete } = opts + if (!transaction) + return wrapTransaction(mergeImage, [resource, relationshipType, imageInput], opts) + + let txResult = await transaction.run( + ` + MATCH (resource {id: $resource.id})-[:${relationshipType}]->(image:Image) + RETURN image {.*} + `, + { resource }, + ) + const [existingImage] = txResult.records.map((record) => record.get('image') as Image) + const { upload } = imageInput + if (!(existingImage || upload)) throw new UserInputError('Cannot find image for given resource') + if (existingImage && upload) { + await deleteCallback(existingImage.url) + } + const url = await uploadImageFile(upload, uploadCallback) + const { alt, sensitive, aspectRatio, type } = imageInput + const image = { alt, sensitive, aspectRatio, url, type } + txResult = await transaction.run( + ` + MATCH (resource {id: $resource.id}) + MERGE (resource)-[:${relationshipType}]->(image:Image) + ON CREATE SET image.createdAt = toString(datetime()) + ON MATCH SET image.updatedAt = toString(datetime()) + SET image += $image + RETURN image {.*} + `, + { resource, image }, + ) + const [mergedImage] = txResult.records.map((record) => record.get('image') as Image) + return mergedImage + } + + const uploadImageFile = async ( + uploadPromise: Promise | undefined, + uploadCallback: FileUploadCallback | undefined = s3Upload, + ) => { + if (!uploadPromise) return undefined + const upload = await uploadPromise + const { name, ext } = path.parse(upload.filename) + const uniqueFilename = `${uuid()}-${slug(name)}${ext}` + const Location = await uploadCallback({ ...upload, uniqueFilename }) + if (!S3_PUBLIC_GATEWAY) { + return Location + } + const publicLocation = new URL(S3_PUBLIC_GATEWAY) + publicLocation.pathname = new URL(Location).pathname + return publicLocation.href + } + + const s3Upload: FileUploadCallback = async ({ createReadStream, uniqueFilename, mimetype }) => { + const s3Location = `original/${uniqueFilename}` + const params = { + Bucket, + Key: s3Location, + ACL: ObjectCannedACL.public_read, + ContentType: mimetype, + Body: createReadStream(), + } + const command = new Upload({ client: s3, params }) + const data = await command.done() + const { Location } = data + if (!Location) { + throw new Error('File upload did not return `Location`') + } + return Location + } + + const s3Delete: FileDeleteCallback = async (url) => { + let { pathname } = new URL(url, 'http://example.org') // dummy domain to avoid invalid URL error + pathname = pathname.substring(1) // remove first character '/' + const prefix = `${Bucket}/` + if (pathname.startsWith(prefix)) { + pathname = pathname.slice(prefix.length) + } + const params = { + Bucket, + Key: pathname, + } + await s3.send(new DeleteObjectCommand(params)) + } + + const images: Images = { + deleteImage, + mergeImage, + } + return images +} diff --git a/backend/src/graphql/resolvers/images/sanitizeRelationshipTypes.ts b/backend/src/graphql/resolvers/images/sanitizeRelationshipTypes.ts new file mode 100644 index 000000000..a6b984a13 --- /dev/null +++ b/backend/src/graphql/resolvers/images/sanitizeRelationshipTypes.ts @@ -0,0 +1,9 @@ +export function sanitizeRelationshipType( + relationshipType: string, +): asserts relationshipType is 'HERO_IMAGE' | 'AVATAR_IMAGE' { + // Cypher query language does not allow to parameterize relationship types + // See: https://github.com/neo4j/neo4j/issues/340 + if (!['HERO_IMAGE', 'AVATAR_IMAGE'].includes(relationshipType)) { + throw new Error(`Unknown relationship type ${relationshipType}`) + } +} diff --git a/backend/src/graphql/resolvers/images/wrapTransaction.ts b/backend/src/graphql/resolvers/images/wrapTransaction.ts new file mode 100644 index 000000000..bcc17877d --- /dev/null +++ b/backend/src/graphql/resolvers/images/wrapTransaction.ts @@ -0,0 +1,23 @@ +import { getDriver } from '@db/neo4j' + +import type { DeleteImageOpts, MergeImageOpts } from './images' + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type AsyncFunc = (...args: any[]) => Promise +export const wrapTransaction = async ( + wrappedCallback: F, + args: unknown[], + opts: DeleteImageOpts | MergeImageOpts, +) => { + const session = getDriver().session() + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const result = await session.writeTransaction((transaction) => { + return wrappedCallback(...args, { ...opts, transaction }) + }) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return result + } finally { + await session.close() + } +} diff --git a/backend/src/schema/resolvers/index.ts b/backend/src/graphql/resolvers/index.ts similarity index 90% rename from backend/src/schema/resolvers/index.ts rename to backend/src/graphql/resolvers/index.ts index 1aeadbea2..9a21f9a9d 100644 --- a/backend/src/schema/resolvers/index.ts +++ b/backend/src/graphql/resolvers/index.ts @@ -1,4 +1,5 @@ -import path from 'path' +import path from 'node:path' + import { fileLoader, mergeResolvers } from 'merge-graphql-schemas' // the files must be correctly evaluated in built and dev state - therefore accept both js & ts files diff --git a/backend/src/graphql/resolvers/inviteCodes.spec.ts b/backend/src/graphql/resolvers/inviteCodes.spec.ts new file mode 100644 index 000000000..a2f43ecb6 --- /dev/null +++ b/backend/src/graphql/resolvers/inviteCodes.spec.ts @@ -0,0 +1,1199 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { ApolloServer } from 'apollo-server-express' +import { createTestClient } from 'apollo-server-testing' + +import CONFIG from '@config/index' +import databaseContext from '@context/database' +import Factory, { cleanDatabase } from '@db/factories' +import { createGroupMutation } from '@graphql/queries/createGroupMutation' +import { currentUser } from '@graphql/queries/currentUser' +import { generateGroupInviteCode } from '@graphql/queries/generateGroupInviteCode' +import { generatePersonalInviteCode } from '@graphql/queries/generatePersonalInviteCode' +import { Group } from '@graphql/queries/Group' +import { GroupMembers } from '@graphql/queries/GroupMembers' +import { invalidateInviteCode } from '@graphql/queries/invalidateInviteCode' +import { joinGroupMutation } from '@graphql/queries/joinGroupMutation' +import { redeemInviteCode } from '@graphql/queries/redeemInviteCode' +import { + authenticatedValidateInviteCode, + unauthenticatedValidateInviteCode, +} from '@graphql/queries/validateInviteCode' +import createServer, { getContext } from '@src/server' + +const database = databaseContext() + +let server: ApolloServer +let authenticatedUser +let query, mutate + +beforeAll(async () => { + await cleanDatabase() + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/require-await + const contextUser = async (_req) => authenticatedUser + const context = getContext({ user: contextUser, database }) + + server = createServer({ context }).server + + const createTestClientResult = createTestClient(server) + query = createTestClientResult.query + mutate = createTestClientResult.mutate +}) + +afterAll(() => { + void server.stop() + void database.driver.close() + database.neode.close() +}) + +describe('validateInviteCode', () => { + let invitingUser, user + beforeEach(async () => { + await cleanDatabase() + invitingUser = await Factory.build('user', { + id: 'inviting-user', + role: 'user', + name: 'Inviting User', + }) + user = await Factory.build('user', { + id: 'normal-user', + role: 'user', + name: 'Normal User', + }) + + authenticatedUser = await invitingUser.toJson() + await mutate({ + mutation: createGroupMutation(), + variables: { + id: 'hidden-group', + name: 'Hidden Group', + about: 'We are hidden', + description: 'anything', + groupType: 'hidden', + actionRadius: 'global', + categoryIds: ['cat6', 'cat12', 'cat16'], + locationName: 'Hamburg, Germany', + }, + }) + + await mutate({ + mutation: createGroupMutation(), + variables: { + id: 'public-group', + name: 'Public Group', + about: 'We are public', + description: 'anything', + groupType: 'public', + actionRadius: 'interplanetary', + categoryIds: ['cat4', 'cat5', 'cat17'], + }, + }) + + await Factory.build( + 'inviteCode', + { + code: 'EXPIRD', + expiresAt: new Date(1970, 1).toISOString(), + }, + { + generatedBy: invitingUser, + }, + ) + await Factory.build( + 'inviteCode', + { + code: 'PERSNL', + }, + { + generatedBy: invitingUser, + }, + ) + await Factory.build( + 'inviteCode', + { + code: 'GRPPBL', + }, + { + generatedBy: invitingUser, + groupId: 'public-group', + }, + ) + await Factory.build( + 'inviteCode', + { + code: 'GRPHDN', + }, + { + generatedBy: invitingUser, + groupId: 'hidden-group', + }, + ) + }) + describe('as unauthenticated user', () => { + beforeEach(() => { + authenticatedUser = null + }) + + it('returns null when the code does not exist', async () => { + await expect( + query({ query: unauthenticatedValidateInviteCode, variables: { code: 'INVALD' } }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + validateInviteCode: null, + }, + errors: undefined, + }), + ) + }) + + it('returns null when the code has expired', async () => { + await expect( + query({ query: unauthenticatedValidateInviteCode, variables: { code: 'EXPIRD' } }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + validateInviteCode: null, + }, + errors: undefined, + }), + ) + }) + + it('returns the inviteCode when the code exists and hs not expired', async () => { + await expect( + query({ query: unauthenticatedValidateInviteCode, variables: { code: 'PERSNL' } }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + validateInviteCode: { + code: 'PERSNL', + generatedBy: { + avatar: { + url: expect.any(String), + }, + name: 'Inviting User', + }, + invitedTo: null, + isValid: true, + }, + }, + errors: undefined, + }), + ) + }) + + it('returns the inviteCode with group details if the code invites to a public group', async () => { + await expect( + query({ query: unauthenticatedValidateInviteCode, variables: { code: 'GRPPBL' } }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + validateInviteCode: { + code: 'GRPPBL', + generatedBy: { + avatar: { + url: expect.any(String), + }, + name: 'Inviting User', + }, + invitedTo: { + groupType: 'public', + name: 'Public Group', + about: 'We are public', + avatar: null, + }, + isValid: true, + }, + }, + errors: undefined, + }), + ) + }) + + it('returns the inviteCode with redacted group details if the code invites to a hidden group', async () => { + await expect( + query({ query: unauthenticatedValidateInviteCode, variables: { code: 'GRPHDN' } }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + validateInviteCode: { + code: 'GRPHDN', + generatedBy: { + avatar: { + url: expect.any(String), + }, + name: 'Inviting User', + }, + invitedTo: { + groupType: 'hidden', + name: '', + about: '', + avatar: null, + }, + isValid: true, + }, + }, + errors: undefined, + }), + ) + }) + + // eslint-disable-next-line jest/no-disabled-tests + it.skip('throws authorization error when querying extended fields', async () => { + await expect( + query({ query: authenticatedValidateInviteCode, variables: { code: 'PERSNL' } }), + ).resolves.toMatchObject({ + data: { + validateInviteCode: { + code: 'PERSNL', + generatedBy: null, + invitedTo: null, + isValid: true, + }, + }, + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + + describe('as authenticated user', () => { + beforeAll(async () => { + authenticatedUser = await user.toJson() + }) + + it('throws no authorization error when querying extended fields', async () => { + await expect( + query({ query: authenticatedValidateInviteCode, variables: { code: 'PERSNL' } }), + ).resolves.toMatchObject({ + data: { + validateInviteCode: { + code: 'PERSNL', + generatedBy: { + id: 'inviting-user', + name: 'Inviting User', + avatar: { + url: expect.any(String), + }, + }, + invitedTo: null, + isValid: true, + }, + }, + errors: undefined, + }) + }) + + it('throws no authorization error when querying extended public group fields', async () => { + await expect( + query({ query: authenticatedValidateInviteCode, variables: { code: 'GRPPBL' } }), + ).resolves.toMatchObject({ + data: { + validateInviteCode: { + code: 'GRPPBL', + generatedBy: { + id: 'inviting-user', + name: 'Inviting User', + avatar: { + url: expect.any(String), + }, + }, + invitedTo: { + id: 'public-group', + groupType: 'public', + name: 'Public Group', + about: 'We are public', + avatar: null, + }, + isValid: true, + }, + }, + errors: undefined, + }) + }) + + // This doesn't work because group permissions are fucked + // eslint-disable-next-line jest/no-disabled-tests + it.skip('throws authorization error when querying extended hidden group fields', async () => { + await expect( + query({ query: authenticatedValidateInviteCode, variables: { code: 'GRPHDN' } }), + ).resolves.toMatchObject({ + data: { + validateInviteCode: { + code: 'GRPHDN', + generatedBy: null, + invitedTo: null, + isValid: true, + }, + }, + errors: [{ message: 'Not Authorized!' }], + }) + }) + + // eslint-disable-next-line jest/no-disabled-tests, @typescript-eslint/no-empty-function + it.skip('throws no authorization error when querying extended hidden group fields as member', async () => {}) + }) +}) + +describe('generatePersonalInviteCode', () => { + let invitingUser + beforeEach(async () => { + await cleanDatabase() + invitingUser = await Factory.build('user', { + id: 'inviting-user', + role: 'user', + name: 'Inviting User', + }) + }) + describe('as unauthenticated user', () => { + beforeEach(() => { + authenticatedUser = null + }) + + it('throws authorization error', async () => { + await expect(mutate({ mutation: generatePersonalInviteCode })).resolves.toMatchObject({ + data: null, + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + + describe('as authenticated user', () => { + beforeEach(async () => { + authenticatedUser = await invitingUser.toJson() + }) + + it('returns a new invite code', async () => { + await expect(mutate({ mutation: generatePersonalInviteCode })).resolves.toMatchObject({ + data: { + generatePersonalInviteCode: { + code: expect.any(String), + comment: null, + createdAt: expect.any(String), + expiresAt: null, + generatedBy: { + avatar: { + url: expect.any(String), + }, + id: 'inviting-user', + name: 'Inviting User', + }, + invitedTo: null, + isValid: true, + redeemedBy: [], + }, + }, + errors: undefined, + }) + }) + + it('returns a new invite code with comment', async () => { + await expect( + mutate({ mutation: generatePersonalInviteCode, variables: { comment: 'some text' } }), + ).resolves.toMatchObject({ + data: { + generatePersonalInviteCode: { + code: expect.any(String), + comment: 'some text', + createdAt: expect.any(String), + expiresAt: null, + generatedBy: { + avatar: { + url: expect.any(String), + }, + id: 'inviting-user', + name: 'Inviting User', + }, + invitedTo: null, + isValid: true, + redeemedBy: [], + }, + }, + errors: undefined, + }) + }) + + it('returns a new invite code with expireDate', async () => { + const date = new Date() + date.setFullYear(date.getFullYear() + 1) + await expect( + mutate({ + mutation: generatePersonalInviteCode, + variables: { expiresAt: date.toISOString() }, + }), + ).resolves.toMatchObject({ + data: { + generatePersonalInviteCode: { + code: expect.any(String), + comment: null, + createdAt: expect.any(String), + expiresAt: date.toISOString(), + generatedBy: { + avatar: { + url: expect.any(String), + }, + id: 'inviting-user', + name: 'Inviting User', + }, + invitedTo: null, + isValid: true, + redeemedBy: [], + }, + }, + errors: undefined, + }) + }) + + it('returns a new invalid invite code with expireDate in the past', async () => { + const date = new Date() + date.setFullYear(date.getFullYear() - 1) + await expect( + mutate({ + mutation: generatePersonalInviteCode, + variables: { expiresAt: date.toISOString() }, + }), + ).resolves.toMatchObject({ + data: { + generatePersonalInviteCode: { + code: expect.any(String), + comment: null, + createdAt: expect.any(String), + expiresAt: date.toISOString(), + generatedBy: { + avatar: { + url: expect.any(String), + }, + id: 'inviting-user', + name: 'Inviting User', + }, + invitedTo: null, + isValid: false, + redeemedBy: [], + }, + }, + errors: undefined, + }) + }) + + it('throws an error when the max amount of invite links was reached', async () => { + let lastCode + for (let i = 0; i < CONFIG.INVITE_CODES_PERSONAL_PER_USER; i++) { + lastCode = await mutate({ mutation: generatePersonalInviteCode }) + expect(lastCode).toMatchObject({ + errors: undefined, + }) + } + await expect(mutate({ mutation: generatePersonalInviteCode })).resolves.toMatchObject({ + errors: [ + { + message: 'You have reached the maximum of Invite Codes you can generate', + }, + ], + }) + await mutate({ + mutation: invalidateInviteCode, + variables: { code: lastCode.data.generatePersonalInviteCode.code }, + }) + await expect(mutate({ mutation: generatePersonalInviteCode })).resolves.toMatchObject({ + errors: undefined, + }) + }) + + // eslint-disable-next-line jest/no-disabled-tests, @typescript-eslint/no-empty-function + it.skip('returns a new invite code when colliding with an existing one', () => {}) + }) +}) + +describe('generateGroupInviteCode', () => { + let invitingUser, notMemberUser, pendingMemberUser + beforeEach(async () => { + await cleanDatabase() + invitingUser = await Factory.build('user', { + id: 'inviting-user', + role: 'user', + name: 'Inviting User', + }) + + notMemberUser = await Factory.build('user', { + id: 'not-member-user', + role: 'user', + name: 'Not a Member User', + }) + + pendingMemberUser = await Factory.build('user', { + id: 'pending-member-user', + role: 'user', + name: 'Pending Member User', + }) + + authenticatedUser = await invitingUser.toJson() + await mutate({ + mutation: createGroupMutation(), + variables: { + id: 'hidden-group', + name: 'Hidden Group', + about: 'We are hidden', + description: 'anything', + groupType: 'hidden', + actionRadius: 'global', + categoryIds: ['cat6', 'cat12', 'cat16'], + locationName: 'Hamburg, Germany', + }, + }) + + await mutate({ + mutation: createGroupMutation(), + variables: { + id: 'public-group', + name: 'Public Group', + about: 'We are public', + description: 'anything', + groupType: 'public', + actionRadius: 'interplanetary', + categoryIds: ['cat4', 'cat5', 'cat17'], + }, + }) + + await mutate({ + mutation: createGroupMutation(), + variables: { + id: 'closed-group', + name: 'Closed Group', + about: 'We are closed', + description: 'anything', + groupType: 'closed', + actionRadius: 'interplanetary', + categoryIds: ['cat4', 'cat5', 'cat17'], + }, + }) + + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'closed-group', + userId: 'pending-member-user', + }, + }) + }) + + describe('as unauthenticated user', () => { + beforeEach(() => { + authenticatedUser = null + }) + + it('throws authorization error', async () => { + await expect( + mutate({ mutation: generateGroupInviteCode, variables: { groupId: 'public-group' } }), + ).resolves.toMatchObject({ + data: null, + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + describe('as authenticated member', () => { + beforeEach(async () => { + authenticatedUser = await invitingUser.toJson() + }) + + it('returns a new group invite code', async () => { + await expect( + mutate({ mutation: generateGroupInviteCode, variables: { groupId: 'public-group' } }), + ).resolves.toMatchObject({ + data: { + generateGroupInviteCode: { + code: expect.any(String), + comment: null, + createdAt: expect.any(String), + expiresAt: null, + generatedBy: { + avatar: { + url: expect.any(String), + }, + id: 'inviting-user', + name: 'Inviting User', + }, + invitedTo: { + id: 'public-group', + groupType: 'public', + name: 'Public Group', + about: 'We are public', + avatar: null, + }, + isValid: true, + redeemedBy: [], + }, + }, + errors: undefined, + }) + }) + + it('returns a new group invite code with comment', async () => { + await expect( + mutate({ + mutation: generateGroupInviteCode, + variables: { groupId: 'public-group', comment: 'some text' }, + }), + ).resolves.toMatchObject({ + data: { + generateGroupInviteCode: { + code: expect.any(String), + comment: 'some text', + createdAt: expect.any(String), + expiresAt: null, + generatedBy: { + avatar: { + url: expect.any(String), + }, + id: 'inviting-user', + name: 'Inviting User', + }, + invitedTo: { + id: 'public-group', + groupType: 'public', + name: 'Public Group', + about: 'We are public', + avatar: null, + }, + isValid: true, + redeemedBy: [], + }, + }, + errors: undefined, + }) + }) + + it('returns a new group invite code with expireDate', async () => { + const date = new Date() + date.setFullYear(date.getFullYear() + 1) + await expect( + mutate({ + mutation: generateGroupInviteCode, + variables: { groupId: 'public-group', expiresAt: date.toISOString() }, + }), + ).resolves.toMatchObject({ + data: { + generateGroupInviteCode: { + code: expect.any(String), + comment: null, + createdAt: expect.any(String), + expiresAt: date.toISOString(), + generatedBy: { + avatar: { + url: expect.any(String), + }, + id: 'inviting-user', + name: 'Inviting User', + }, + invitedTo: { + id: 'public-group', + groupType: 'public', + name: 'Public Group', + about: 'We are public', + avatar: null, + }, + isValid: true, + redeemedBy: [], + }, + }, + errors: undefined, + }) + }) + + it('returns a new invalid group invite code with expireDate in the past', async () => { + const date = new Date() + date.setFullYear(date.getFullYear() - 1) + await expect( + mutate({ + mutation: generateGroupInviteCode, + variables: { groupId: 'public-group', expiresAt: date.toISOString() }, + }), + ).resolves.toMatchObject({ + data: { + generateGroupInviteCode: { + code: expect.any(String), + comment: null, + createdAt: expect.any(String), + expiresAt: date.toISOString(), + generatedBy: { + avatar: { + url: expect.any(String), + }, + id: 'inviting-user', + name: 'Inviting User', + }, + invitedTo: { + id: 'public-group', + groupType: 'public', + name: 'Public Group', + about: 'We are public', + avatar: null, + }, + isValid: false, + redeemedBy: [], + }, + }, + errors: undefined, + }) + }) + + it('throws an error when the max amount of invite links was reached', async () => { + let lastCode + for (let i = 0; i < CONFIG.INVITE_CODES_GROUP_PER_USER; i++) { + lastCode = await mutate({ + mutation: generateGroupInviteCode, + variables: { groupId: 'public-group' }, + }) + expect(lastCode).toMatchObject({ + errors: undefined, + }) + } + await expect( + mutate({ mutation: generateGroupInviteCode, variables: { groupId: 'public-group' } }), + ).resolves.toMatchObject({ + errors: [ + { + message: 'You have reached the maximum of Invite Codes you can generate for this group', + }, + ], + }) + await mutate({ + mutation: invalidateInviteCode, + variables: { code: lastCode.data.generateGroupInviteCode.code }, + }) + await expect( + mutate({ mutation: generateGroupInviteCode, variables: { groupId: 'public-group' } }), + ).resolves.toMatchObject({ + errors: undefined, + }) + }) + + // eslint-disable-next-line jest/no-disabled-tests, @typescript-eslint/no-empty-function + it.skip('returns a new group invite code when colliding with an existing one', () => {}) + }) + + describe('as authenticated not-member', () => { + beforeEach(async () => { + authenticatedUser = await notMemberUser.toJson() + }) + + it('throws authorization error', async () => { + const date = new Date() + date.setFullYear(date.getFullYear() - 1) + await expect( + mutate({ + mutation: generateGroupInviteCode, + variables: { groupId: 'public-group' }, + }), + ).resolves.toMatchObject({ + data: null, + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + + describe('as pending-member user', () => { + beforeEach(async () => { + authenticatedUser = await pendingMemberUser.toJson() + }) + + it('throws authorization error', async () => { + await expect( + mutate({ + mutation: generateGroupInviteCode, + variables: { groupId: 'hidden-group' }, + }), + ).resolves.toMatchObject({ + data: null, + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) +}) + +describe('invalidateInviteCode', () => { + let invitingUser, otherUser + beforeEach(async () => { + await cleanDatabase() + invitingUser = await Factory.build('user', { + id: 'inviting-user', + role: 'user', + name: 'Inviting User', + }) + + otherUser = await Factory.build('user', { + id: 'other-user', + role: 'user', + name: 'Other User', + }) + + await Factory.build( + 'inviteCode', + { + code: 'CODE33', + }, + { + generatedBy: invitingUser, + }, + ) + }) + + describe('as unauthenticated user', () => { + beforeEach(() => { + authenticatedUser = null + }) + + it('throws authorization error', async () => { + await expect( + mutate({ mutation: invalidateInviteCode, variables: { code: 'CODE33' } }), + ).resolves.toMatchObject({ + data: { + invalidateInviteCode: null, + }, + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + + describe('as authenticated user', () => { + describe('as link owner', () => { + beforeEach(async () => { + authenticatedUser = await invitingUser.toJson() + }) + + it('returns the invalidated InviteCode', async () => { + await expect( + mutate({ mutation: invalidateInviteCode, variables: { code: 'CODE33' } }), + ).resolves.toMatchObject({ + data: { + invalidateInviteCode: { + code: expect.any(String), + comment: null, + createdAt: expect.any(String), + expiresAt: expect.any(String), + generatedBy: { + avatar: { + url: expect.any(String), + }, + id: 'inviting-user', + name: 'Inviting User', + }, + invitedTo: null, + isValid: false, + redeemedBy: [], + }, + }, + errors: undefined, + }) + }) + }) + + describe('as not link owner', () => { + beforeEach(async () => { + authenticatedUser = await otherUser.toJson() + }) + + it('throws authorization error', async () => { + await expect( + mutate({ mutation: invalidateInviteCode, variables: { code: 'CODE33' } }), + ).resolves.toMatchObject({ + data: { + invalidateInviteCode: null, + }, + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + }) +}) + +describe('redeemInviteCode', () => { + let invitingUser, otherUser + beforeEach(async () => { + await cleanDatabase() + invitingUser = await Factory.build('user', { + id: 'inviting-user', + role: 'user', + name: 'Inviting User', + }) + + otherUser = await Factory.build('user', { + id: 'other-user', + role: 'user', + name: 'Other User', + }) + + authenticatedUser = await invitingUser.toJson() + await mutate({ + mutation: createGroupMutation(), + variables: { + id: 'hidden-group', + name: 'Hidden Group', + about: 'We are hidden', + description: 'anything', + groupType: 'hidden', + actionRadius: 'global', + categoryIds: ['cat6', 'cat12', 'cat16'], + locationName: 'Hamburg, Germany', + }, + }) + + await mutate({ + mutation: createGroupMutation(), + variables: { + id: 'public-group', + name: 'Public Group', + about: 'We are public', + description: 'anything', + groupType: 'public', + actionRadius: 'interplanetary', + categoryIds: ['cat4', 'cat5', 'cat17'], + }, + }) + + await Factory.build( + 'inviteCode', + { + code: 'CODE33', + }, + { + generatedBy: invitingUser, + }, + ) + await Factory.build( + 'inviteCode', + { + code: 'GRPPBL', + }, + { + generatedBy: invitingUser, + groupId: 'public-group', + }, + ) + await Factory.build( + 'inviteCode', + { + code: 'GRPHDN', + }, + { + generatedBy: invitingUser, + groupId: 'hidden-group', + }, + ) + }) + + describe('as unauthenticated user', () => { + beforeEach(() => { + authenticatedUser = null + }) + + it('throws authorization error', async () => { + await expect( + mutate({ mutation: redeemInviteCode, variables: { code: 'CODE33' } }), + ).resolves.toMatchObject({ + data: null, + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + + describe('as authenticated user', () => { + beforeEach(async () => { + authenticatedUser = await otherUser.toJson() + }) + + it('returns false for an invalid inviteCode', async () => { + await expect( + mutate({ mutation: redeemInviteCode, variables: { code: 'INVALD' } }), + ).resolves.toMatchObject({ + data: { + redeemInviteCode: false, + }, + errors: undefined, + }) + }) + + it('returns true for a personal inviteCode, but does nothing', async () => { + await expect( + mutate({ mutation: redeemInviteCode, variables: { code: 'CODE33' } }), + ).resolves.toMatchObject({ + data: { + redeemInviteCode: true, + }, + errors: undefined, + }) + authenticatedUser = await invitingUser.toJson() + await expect(query({ query: currentUser })).resolves.toMatchObject({ + data: { + currentUser: { + following: [], + inviteCodes: [ + { + code: 'CODE33', + redeemedByCount: 0, + }, + ], + }, + }, + errors: undefined, + }) + }) + + it('returns true for a public group inviteCode and makes the user a group member', async () => { + await expect( + mutate({ mutation: redeemInviteCode, variables: { code: 'GRPPBL' } }), + ).resolves.toMatchObject({ + data: { + redeemInviteCode: true, + }, + errors: undefined, + }) + await expect( + query({ query: Group, variables: { id: 'public-group' } }), + ).resolves.toMatchObject({ + data: { + Group: [ + { + myRole: 'usual', + }, + ], + }, + errors: undefined, + }) + authenticatedUser = await invitingUser.toJson() + await expect(query({ query: Group })).resolves.toMatchObject({ + data: { + Group: expect.arrayContaining([ + expect.objectContaining({ + inviteCodes: expect.arrayContaining([ + { + code: 'GRPPBL', + redeemedByCount: 1, + }, + ]), + }), + ]), + }, + errors: undefined, + }) + }) + + it('returns true for a hidden group inviteCode and makes the user a pending member', async () => { + await expect( + mutate({ mutation: redeemInviteCode, variables: { code: 'GRPHDN' } }), + ).resolves.toMatchObject({ + data: { + redeemInviteCode: true, + }, + errors: undefined, + }) + authenticatedUser = await invitingUser.toJson() + await expect( + query({ query: GroupMembers, variables: { id: 'hidden-group' } }), + ).resolves.toMatchObject({ + data: { + GroupMembers: expect.arrayContaining([ + { + id: 'inviting-user', + myRoleInGroup: 'owner', + name: 'Inviting User', + slug: 'inviting-user', + }, + { + id: 'other-user', + myRoleInGroup: 'pending', + name: 'Other User', + slug: 'other-user', + }, + ]), + }, + errors: undefined, + }) + await expect(query({ query: Group })).resolves.toMatchObject({ + data: { + Group: expect.arrayContaining([ + expect.objectContaining({ + inviteCodes: expect.arrayContaining([ + { + code: 'GRPHDN', + redeemedByCount: 1, + }, + ]), + }), + ]), + }, + errors: undefined, + }) + }) + }) + + describe('as authenticated self', () => { + beforeEach(async () => { + authenticatedUser = await invitingUser.toJson() + }) + + it('returns true for a personal inviteCode, but does nothing', async () => { + await expect( + mutate({ mutation: redeemInviteCode, variables: { code: 'CODE33' } }), + ).resolves.toMatchObject({ + data: { + redeemInviteCode: true, + }, + errors: undefined, + }) + await expect(query({ query: currentUser })).resolves.toMatchObject({ + data: { + currentUser: { + following: [], + inviteCodes: [ + { + code: 'CODE33', + redeemedByCount: 0, + }, + ], + }, + }, + errors: undefined, + }) + }) + + it('returns true for a public group inviteCode, but does nothing', async () => { + await expect( + mutate({ mutation: redeemInviteCode, variables: { code: 'GRPPBL' } }), + ).resolves.toMatchObject({ + data: { + redeemInviteCode: true, + }, + errors: undefined, + }) + await expect( + query({ query: Group, variables: { id: 'public-group' } }), + ).resolves.toMatchObject({ + data: { + Group: [ + { + myRole: 'owner', + }, + ], + }, + errors: undefined, + }) + await expect(query({ query: Group })).resolves.toMatchObject({ + data: { + Group: expect.arrayContaining([ + expect.objectContaining({ + inviteCodes: expect.arrayContaining([ + { + code: 'GRPPBL', + redeemedByCount: 0, + }, + ]), + }), + ]), + }, + errors: undefined, + }) + }) + }) +}) diff --git a/backend/src/graphql/resolvers/inviteCodes.ts b/backend/src/graphql/resolvers/inviteCodes.ts new file mode 100644 index 000000000..b17d32dd8 --- /dev/null +++ b/backend/src/graphql/resolvers/inviteCodes.ts @@ -0,0 +1,300 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +import CONFIG from '@config/index' +import registrationConstants from '@constants/registrationBranded' +// eslint-disable-next-line import/no-cycle +import { Context } from '@src/server' + +import Resolver from './helpers/Resolver' + +export const generateInviteCode = () => { + // 6 random numbers in [ 0, 35 ] are 36 possible numbers (10 [0-9] + 26 [A-Z]) + return Array.from( + { length: registrationConstants.INVITE_CODE_LENGTH }, + (n: number = Math.floor(Math.random() * 36)) => { + // n > 9: it is a letter (ASCII 65 is A) -> 10 + 55 = 65 + // else: it is a number (ASCII 48 is 0) -> 0 + 48 = 48 + return String.fromCharCode(n > 9 ? n + 55 : n + 48) + }, + ).join('') +} + +const uniqueInviteCode = async (context: Context, code: string) => { + return ( + ( + await context.database.query({ + query: `MATCH (inviteCode:InviteCode { code: toUpper($code) }) + WHERE inviteCode.expiresAt IS NULL + OR inviteCode.expiresAt >= datetime() + RETURN toString(count(inviteCode)) AS count`, + variables: { code }, + }) + ).records[0].get('count') === '0' + ) +} + +export const validateInviteCode = async (context: Context, inviteCode) => { + const result = ( + await context.database.query({ + query: ` + OPTIONAL MATCH (inviteCode:InviteCode { code: toUpper($inviteCode) }) + RETURN + CASE + WHEN inviteCode IS NULL THEN false + WHEN inviteCode.expiresAt IS NULL THEN true + WHEN datetime(inviteCode.expiresAt) >= datetime() THEN true + ELSE false END AS result + `, + variables: { inviteCode }, + }) + ).records + return result[0].get('result') === true +} + +export const redeemInviteCode = async (context: Context, code, newUser = false) => { + const result = ( + await context.database.query({ + query: ` + MATCH (inviteCode:InviteCode {code: toUpper($code)})<-[:GENERATED]-(host:User) + OPTIONAL MATCH (inviteCode)-[:INVITES_TO]->(group:Group) + WHERE inviteCode.expiresAt IS NULL + OR datetime(inviteCode.expiresAt) >= datetime() + RETURN inviteCode {.*}, group {.*}, host {.*}`, + variables: { code }, + }) + ).records + + if (result.length !== 1) { + return false + } + + const inviteCode = result[0].get('inviteCode') + const group = result[0].get('group') + const host = result[0].get('host') + + if (!inviteCode || !host) { + return false + } + + // self + if (host.id === context.user.id) { + return true + } + + // Personal Invite Link + if (!group) { + // We redeemed this link while having an account, hence we do nothing, but return true + if (!newUser) { + return true + } + + await context.database.write({ + query: ` + MATCH (user:User {id: $user.id}), (inviteCode:InviteCode {code: toUpper($code)})<-[:GENERATED]-(host:User) + MERGE (user)-[:REDEEMED { createdAt: toString(datetime()) }]->(inviteCode) + MERGE (host)-[:INVITED { createdAt: toString(datetime()) }]->(user) + MERGE (user)-[:FOLLOWS { createdAt: toString(datetime()) }]->(host) + MERGE (host)-[:FOLLOWS { createdAt: toString(datetime()) }]->(user) + `, + variables: { user: context.user, code }, + }) + // Group Invite Link + } else { + const role = ['closed', 'hidden'].includes(group.groupType as string) ? 'pending' : 'usual' + + const optionalInvited = newUser + ? 'MERGE (host)-[:INVITED { createdAt: toString(datetime()) }]->(user)' + : '' + + await context.database.write({ + query: ` + MATCH (user:User {id: $user.id}), (group:Group)<-[:INVITES_TO]-(inviteCode:InviteCode {code: toUpper($code)})<-[:GENERATED]-(host:User) + MERGE (user)-[:REDEEMED { createdAt: toString(datetime()) }]->(inviteCode) + ${optionalInvited} + MERGE (user)-[membership:MEMBER_OF]->(group) + ON CREATE SET + membership.createdAt = toString(datetime()), + membership.updatedAt = null, + membership.role = $role + `, + variables: { user: context.user, code, role }, + }) + } + return true +} + +export default { + Query: { + validateInviteCode: async (_parent, args, context: Context, _resolveInfo) => { + const result = ( + await context.database.query({ + query: ` + MATCH (inviteCode:InviteCode { code: toUpper($args.code) }) + WHERE inviteCode.expiresAt IS NULL + OR datetime(inviteCode.expiresAt) >= datetime() + RETURN inviteCode {.*}`, + variables: { args }, + }) + ).records + + if (result.length !== 1) { + return null + } + + return result[0].get('inviteCode') + }, + }, + Mutation: { + generatePersonalInviteCode: async (_parent, args, context: Context, _resolveInfo) => { + const userInviteCodeAmount = ( + await context.database.query({ + query: ` + MATCH (inviteCode:InviteCode)<-[:GENERATED]-(user:User {id: $user.id}) + WHERE NOT (inviteCode)-[:INVITES_TO]->(:Group) + AND (inviteCode.expiresAt IS NULL OR inviteCode.expiresAt >= datetime()) + RETURN toString(count(inviteCode)) as count + `, + variables: { user: context.user }, + }) + ).records[0].get('count') + + if (parseInt(userInviteCodeAmount as string) >= CONFIG.INVITE_CODES_PERSONAL_PER_USER) { + throw new Error('You have reached the maximum of Invite Codes you can generate') + } + + let code = generateInviteCode() + while (!(await uniqueInviteCode(context, code))) { + code = generateInviteCode() + } + + return ( + await context.database.write({ + // We delete a potential old invite code if there is a collision on an expired code + query: ` + MATCH (user:User {id: $user.id}) + OPTIONAL MATCH (oldInviteCode:InviteCode { code: toUpper($code) }) + DETACH DELETE oldInviteCode + MERGE (user)-[:GENERATED]->(inviteCode:InviteCode { code: toUpper($code)}) + ON CREATE SET + inviteCode.createdAt = toString(datetime()), + inviteCode.expiresAt = $args.expiresAt, + inviteCode.comment = $args.comment + RETURN inviteCode {.*}`, + variables: { user: context.user, code, args }, + }) + ).records[0].get('inviteCode') + }, + generateGroupInviteCode: async (_parent, args, context: Context, _resolveInfo) => { + const userInviteCodeAmount = ( + await context.database.query({ + query: ` + MATCH (:Group {id: $args.groupId})<-[:INVITES_TO]-(inviteCode:InviteCode)<-[:GENERATED]-(user:User {id: $user.id}) + WHERE inviteCode.expiresAt IS NULL + OR inviteCode.expiresAt >= datetime() + RETURN toString(count(inviteCode)) as count + `, + variables: { user: context.user, args }, + }) + ).records[0].get('count') + + if (parseInt(userInviteCodeAmount as string) >= CONFIG.INVITE_CODES_GROUP_PER_USER) { + throw new Error( + 'You have reached the maximum of Invite Codes you can generate for this group', + ) + } + + let code = generateInviteCode() + while (!(await uniqueInviteCode(context, code))) { + code = generateInviteCode() + } + + const inviteCode = ( + await context.database.write({ + query: ` + MATCH + (user:User {id: $user.id})-[membership:MEMBER_OF]->(group:Group {id: $args.groupId}) + WHERE NOT membership.role = 'pending' + OPTIONAL MATCH (oldInviteCode:InviteCode { code: toUpper($code) }) + DETACH DELETE oldInviteCode + MERGE (user)-[:GENERATED]->(inviteCode:InviteCode { code: toUpper($code) })-[:INVITES_TO]->(group) + ON CREATE SET + inviteCode.createdAt = toString(datetime()), + inviteCode.expiresAt = $args.expiresAt, + inviteCode.comment = $args.comment + RETURN inviteCode {.*}`, + variables: { user: context.user, code, args }, + }) + ).records + + if (inviteCode.length !== 1) { + // Not a member + throw new Error('Not Authorized!') + } + + return inviteCode[0].get('inviteCode') + }, + invalidateInviteCode: async (_parent, args, context: Context, _resolveInfo) => { + const result = ( + await context.database.write({ + query: ` + MATCH (user:User {id: $user.id})-[:GENERATED]-(inviteCode:InviteCode {code: toUpper($args.code)}) + SET inviteCode.expiresAt = toString(datetime()) + RETURN inviteCode {.*}`, + variables: { args, user: context.user }, + }) + ).records + + if (result.length !== 1) { + // Link not generated by this user or does not exist + throw new Error('Not Authorized!') + } + + return result[0].get('inviteCode') + }, + redeemInviteCode: async (_parent, args, context: Context, _resolveInfo) => { + return redeemInviteCode(context, args.code) + }, + }, + InviteCode: { + invitedTo: async (parent, _args, context: Context, _resolveInfo) => { + if (!parent.code) { + return null + } + + const result = ( + await context.database.query({ + query: ` + MATCH (inviteCode:InviteCode {code: $parent.code})-[:INVITES_TO]->(group:Group) + RETURN group {.*} + `, + variables: { parent }, + }) + ).records + + if (result.length !== 1) { + return null + } + return result[0].get('group') + }, + isValid: async (parent, _args, context: Context, _resolveInfo) => { + if (!parent.code) { + return false + } + return validateInviteCode(context, parent.code) + }, + ...Resolver('InviteCode', { + idAttribute: 'code', + undefinedToNull: ['expiresAt', 'comment'], + count: { + redeemedByCount: '<-[:REDEEMED]-(related:User)', + }, + hasOne: { + generatedBy: '<-[:GENERATED]-(related:User)', + }, + hasMany: { + redeemedBy: '<-[:REDEEMED]-(related:User)', + }, + }), + }, +} diff --git a/backend/src/graphql/resolvers/locations.spec.ts b/backend/src/graphql/resolvers/locations.spec.ts new file mode 100644 index 000000000..8b3c5b779 --- /dev/null +++ b/backend/src/graphql/resolvers/locations.spec.ts @@ -0,0 +1,302 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { createTestClient } from 'apollo-server-testing' +import gql from 'graphql-tag' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' + +let query, mutate, authenticatedUser + +const driver = getDriver() +const neode = getNeode() + +beforeAll(async () => { + await cleanDatabase() + + const { server } = createServer({ + context: () => { + return { + driver, + neode, + user: authenticatedUser, + } + }, + }) + query = createTestClient(server).query + mutate = createTestClient(server).mutate +}) + +afterAll(async () => { + await cleanDatabase() + await driver.close() +}) + +// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 +afterEach(async () => { + await cleanDatabase() +}) + +describe('resolvers', () => { + describe('Location', () => { + describe('custom mutation, not handled by neo4j-graphql-js', () => { + let variables + const updateUserMutation = gql` + mutation ($id: ID!, $name: String) { + UpdateUser(id: $id, name: $name) { + name + location { + name: nameRU + nameEN + } + } + } + ` + + beforeEach(async () => { + variables = { + id: 'u47', + name: 'John Doughnut', + } + const Paris = await Factory.build('location', { + id: 'region.9397217726497330', + name: 'Paris', + type: 'region', + lng: 2.35183, + lat: 48.85658, + nameEN: 'Paris', + }) + + const user = await Factory.build('user', { + id: 'u47', + name: 'John Doe', + }) + await user.relateTo(Paris, 'isIn') + authenticatedUser = await user.toJson() + }) + + it('returns `null` if location translation is not available', async () => { + await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject({ + data: { + UpdateUser: { + name: 'John Doughnut', + location: { + name: null, + nameEN: 'Paris', + }, + }, + }, + errors: undefined, + }) + }) + }) + }) +}) + +const distanceToMeQuery = gql` + query ($id: ID!) { + User(id: $id) { + location { + distanceToMe + } + } + } +` +let user, myPlaceUser, otherPlaceUser, noCordsPlaceUser, noPlaceUser + +describe('distanceToMe', () => { + beforeEach(async () => { + const Hamburg = await Factory.build('location', { + id: 'region.5127278006398860', + name: 'Hamburg', + type: 'region', + lng: 10.0, + lat: 53.55, + nameES: 'Hamburgo', + nameFR: 'Hambourg', + nameIT: 'Amburgo', + nameEN: 'Hamburg', + namePT: 'Hamburgo', + nameDE: 'Hamburg', + nameNL: 'Hamburg', + namePL: 'Hamburg', + nameRU: 'Гамбург', + }) + const Germany = await Factory.build('location', { + id: 'country.10743216036480410', + name: 'Germany', + type: 'country', + namePT: 'Alemanha', + nameDE: 'Deutschland', + nameES: 'Alemania', + nameNL: 'Duitsland', + namePL: 'Niemcy', + nameFR: 'Allemagne', + nameIT: 'Germania', + nameEN: 'Germany', + nameRU: 'Германия', + }) + const Paris = await Factory.build('location', { + id: 'region.9397217726497330', + name: 'Paris', + type: 'region', + lng: 2.35183, + lat: 48.85658, + nameES: 'París', + nameFR: 'Paris', + nameIT: 'Parigi', + nameEN: 'Paris', + namePT: 'Paris', + nameDE: 'Paris', + nameNL: 'Parijs', + namePL: 'Paryż', + nameRU: 'Париж', + }) + + user = await Factory.build('user', { + id: 'user', + role: 'user', + }) + await user.relateTo(Hamburg, 'isIn') + + myPlaceUser = await Factory.build('user', { + id: 'myPlaceUser', + role: 'user', + }) + await myPlaceUser.relateTo(Hamburg, 'isIn') + + otherPlaceUser = await Factory.build('user', { + id: 'otherPlaceUser', + role: 'user', + }) + await otherPlaceUser.relateTo(Paris, 'isIn') + + noCordsPlaceUser = await Factory.build('user', { + id: 'noCordsPlaceUser', + role: 'user', + }) + await noCordsPlaceUser.relateTo(Germany, 'isIn') + + noPlaceUser = await Factory.build('user', { + id: 'noPlaceUser', + role: 'user', + }) + }) + + describe('query the field', () => { + describe('for self user', () => { + it('returns 0', async () => { + authenticatedUser = await user.toJson() + const targetUser = await user.toJson() + await expect( + query({ query: distanceToMeQuery, variables: { id: targetUser.id } }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + User: [ + { + location: { + distanceToMe: 0, + }, + }, + ], + }, + errors: undefined, + }), + ) + }) + }) + + describe('for myPlaceUser', () => { + it('returns 0', async () => { + authenticatedUser = await user.toJson() + const targetUser = await myPlaceUser.toJson() + await expect( + query({ query: distanceToMeQuery, variables: { id: targetUser.id } }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + User: [ + { + location: { + distanceToMe: 0, + }, + }, + ], + }, + errors: undefined, + }), + ) + }) + }) + + describe('for otherPlaceUser', () => { + it('returns a number', async () => { + authenticatedUser = await user.toJson() + const targetUser = await otherPlaceUser.toJson() + await expect( + query({ query: distanceToMeQuery, variables: { id: targetUser.id } }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + User: [ + { + location: { + distanceToMe: 746, + }, + }, + ], + }, + errors: undefined, + }), + ) + }) + }) + + describe('for noCordsPlaceUser', () => { + it('returns null', async () => { + authenticatedUser = await user.toJson() + const targetUser = await noCordsPlaceUser.toJson() + await expect( + query({ query: distanceToMeQuery, variables: { id: targetUser.id } }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + User: [ + { + location: { + distanceToMe: null, + }, + }, + ], + }, + errors: undefined, + }), + ) + }) + }) + + describe('for noPlaceUser', () => { + it('returns null location', async () => { + authenticatedUser = await user.toJson() + const targetUser = await noPlaceUser.toJson() + await expect( + query({ query: distanceToMeQuery, variables: { id: targetUser.id } }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + User: [ + { + location: null, + }, + ], + }, + errors: undefined, + }), + ) + }) + }) + }) +}) diff --git a/backend/src/graphql/resolvers/locations.ts b/backend/src/graphql/resolvers/locations.ts new file mode 100644 index 000000000..fc69fab94 --- /dev/null +++ b/backend/src/graphql/resolvers/locations.ts @@ -0,0 +1,64 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { UserInputError } from 'apollo-server' + +import Resolver from './helpers/Resolver' +import { queryLocations } from './users/location' + +export default { + Location: { + ...Resolver('Location', { + undefinedToNull: [ + 'nameEN', + 'nameDE', + 'nameFR', + 'nameNL', + 'nameIT', + 'nameES', + 'namePT', + 'namePL', + 'nameRU', + ], + }), + distanceToMe: async (parent, _params, context, _resolveInfo) => { + if (!parent.id) { + throw new Error('Can not identify selected Location!') + } + const session = context.driver.session() + + const query = session.readTransaction(async (transaction) => { + const result = await transaction.run( + ` + MATCH (loc:Location {id: $parent.id}) + MATCH (me:User {id: $user.id})-[:IS_IN]->(meLoc:Location) + WITH + point({latitude: loc.lat, longitude: loc.lng}) as locPoint, + point({latitude: meLoc.lat, longitude: meLoc.lng}) as mePoint + RETURN round(point.distance(locPoint, mePoint) / 1000) as distance + `, + { parent, user: context.user }, + ) + + return result.records.map((record) => record.get('distance'))[0] + }) + + try { + return await query + } finally { + await session.close() + } + }, + }, + Query: { + queryLocations: async (_object, args, _context, _resolveInfo) => { + try { + return queryLocations(args) + } catch (e) { + throw new UserInputError(e.message) + } + }, + }, +} diff --git a/backend/src/schema/resolvers/messages.spec.ts b/backend/src/graphql/resolvers/messages.spec.ts similarity index 91% rename from backend/src/schema/resolvers/messages.spec.ts rename to backend/src/graphql/resolvers/messages.spec.ts index 83d9fdc6b..81799fdf1 100644 --- a/backend/src/schema/resolvers/messages.spec.ts +++ b/backend/src/graphql/resolvers/messages.spec.ts @@ -1,42 +1,49 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/await-thenable */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { ApolloServer } from 'apollo-server-express' import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../../db/factories' -import { getNeode, getDriver } from '../../db/neo4j' -import { createRoomMutation, roomQuery } from '../../graphql/rooms' -import { createMessageMutation, messageQuery, markMessagesAsSeen } from '../../graphql/messages' -import createServer, { pubsub } from '../../server' -const driver = getDriver() -const neode = getNeode() - -const pubsubSpy = jest.spyOn(pubsub, 'publish') +import databaseContext from '@context/database' +import pubsubContext from '@context/pubsub' +import Factory, { cleanDatabase } from '@db/factories' +import { createMessageMutation } from '@graphql/queries/createMessageMutation' +import { createRoomMutation } from '@graphql/queries/createRoomMutation' +import { markMessagesAsSeen } from '@graphql/queries/markMessagesAsSeen' +import { messageQuery } from '@graphql/queries/messageQuery' +import { roomQuery } from '@graphql/queries/roomQuery' +import createServer, { getContext } from '@src/server' let query let mutate let authenticatedUser let chattingUser, otherChattingUser, notChattingUser +const database = databaseContext() +const pubsub = pubsubContext() +const pubsubSpy = jest.spyOn(pubsub, 'publish') + +let server: ApolloServer beforeAll(async () => { await cleanDatabase() - const { server } = createServer({ - context: () => { - return { - driver, - neode, - user: authenticatedUser, - cypherParams: { - currentUserId: authenticatedUser ? authenticatedUser.id : null, - }, - } - }, - }) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/require-await + const contextUser = async (_req) => authenticatedUser + const context = getContext({ user: contextUser, database, pubsub }) + + server = createServer({ context }).server + query = createTestClient(server).query mutate = createTestClient(server).mutate }) afterAll(async () => { await cleanDatabase() - driver.close() + void server.stop() + void database.driver.close() + database.neode.close() }) describe('Message', () => { @@ -117,7 +124,7 @@ describe('Message', () => { }) describe('user chats in room', () => { - it('returns the message and publishes subscriptions', async () => { + it('returns the message', async () => { await expect( mutate({ mutation: createMessageMutation(), @@ -142,24 +149,6 @@ describe('Message', () => { }, }, }) - expect(pubsubSpy).toBeCalledWith('ROOM_COUNT_UPDATED', { - roomCountUpdated: '1', - userId: 'other-chatting-user', - }) - expect(pubsubSpy).toBeCalledWith('CHAT_MESSAGE_ADDED', { - chatMessageAdded: expect.objectContaining({ - id: expect.any(String), - content: 'Some nice message to other chatting user', - senderId: 'chatting-user', - username: 'Chatting User', - avatar: expect.any(String), - date: expect.any(String), - saved: true, - distributed: false, - seen: false, - }), - userId: 'other-chatting-user', - }) }) describe('room is updated as well', () => { diff --git a/backend/src/schema/resolvers/messages.ts b/backend/src/graphql/resolvers/messages.ts similarity index 87% rename from backend/src/schema/resolvers/messages.ts rename to backend/src/graphql/resolvers/messages.ts index c1381045f..6a5a59d27 100644 --- a/backend/src/schema/resolvers/messages.ts +++ b/backend/src/graphql/resolvers/messages.ts @@ -1,9 +1,15 @@ -import { neo4jgraphql } from 'neo4j-graphql-js' -import Resolver from './helpers/Resolver' - -import { getUnreadRoomsCount } from './rooms' -import { pubsub, ROOM_COUNT_UPDATED, CHAT_MESSAGE_ADDED } from '../../server' +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import { withFilter } from 'graphql-subscriptions' +import { neo4jgraphql } from 'neo4j-graphql-js' + +import { CHAT_MESSAGE_ADDED } from '@constants/subscriptions' + +import Resolver from './helpers/Resolver' const setMessagesAsDistributed = async (undistributedMessagesIds, session) => { return session.writeTransaction(async (transaction) => { @@ -24,7 +30,7 @@ export default { Subscription: { chatMessageAdded: { subscribe: withFilter( - () => pubsub.asyncIterator(CHAT_MESSAGE_ADDED), + (_, __, context) => context.pubsub.asyncIterator(CHAT_MESSAGE_ADDED), (payload, variables, context) => { return payload.userId === context.user?.id }, @@ -110,22 +116,7 @@ export default { return message }) try { - const message = await writeTxResultPromise - if (message) { - const roomCountUpdated = await getUnreadRoomsCount(message.recipientId, session) - - // send subscriptions - void pubsub.publish(ROOM_COUNT_UPDATED, { - roomCountUpdated, - userId: message.recipientId, - }) - void pubsub.publish(CHAT_MESSAGE_ADDED, { - chatMessageAdded: message, - userId: message.recipientId, - }) - } - - return message + return await writeTxResultPromise } catch (error) { throw new Error(error) } finally { diff --git a/backend/src/schema/resolvers/moderation.spec.ts b/backend/src/graphql/resolvers/moderation.spec.ts similarity index 98% rename from backend/src/schema/resolvers/moderation.spec.ts rename to backend/src/graphql/resolvers/moderation.spec.ts index 1665e9446..f3224421e 100644 --- a/backend/src/schema/resolvers/moderation.spec.ts +++ b/backend/src/graphql/resolvers/moderation.spec.ts @@ -1,8 +1,13 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../../db/factories' import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' const neode = getNeode() const driver = getDriver() @@ -70,7 +75,7 @@ describe('moderate resources', () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { @@ -189,7 +194,7 @@ describe('moderate resources', () => { ]) const cypher = 'MATCH (:Report)<-[review:REVIEWED]-(moderator:User {id: "moderator-id"}) RETURN review' - const reviews = await neode.cypher(cypher) + const reviews = await neode.cypher(cypher, {}) expect(reviews.records).toHaveLength(1) }) diff --git a/backend/src/schema/resolvers/moderation.ts b/backend/src/graphql/resolvers/moderation.ts similarity index 87% rename from backend/src/schema/resolvers/moderation.ts rename to backend/src/graphql/resolvers/moderation.ts index a29a411aa..bcdb3992a 100644 --- a/backend/src/schema/resolvers/moderation.ts +++ b/backend/src/graphql/resolvers/moderation.ts @@ -1,5 +1,7 @@ -import log from './helpers/databaseLogger' - +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ export default { Mutation: { review: async (_object, params, context, _resolveInfo) => { @@ -27,7 +29,6 @@ export default { moderatorId: moderator.id, dateTime: new Date().toISOString(), }) - log(reviewTransactionResponse) return reviewTransactionResponse.records.map((record) => record.get('review')) }) const [reviewed] = await reviewWriteTxResultPromise diff --git a/backend/src/schema/resolvers/notifications.spec.ts b/backend/src/graphql/resolvers/notifications.spec.ts similarity index 94% rename from backend/src/schema/resolvers/notifications.spec.ts rename to backend/src/graphql/resolvers/notifications.spec.ts index 58757c92d..d6d22e459 100644 --- a/backend/src/schema/resolvers/notifications.spec.ts +++ b/backend/src/graphql/resolvers/notifications.spec.ts @@ -1,13 +1,17 @@ -import Factory, { cleanDatabase } from '../../db/factories' -import gql from 'graphql-tag' -import { getDriver } from '../../db/neo4j' +/* eslint-disable @typescript-eslint/await-thenable */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' -import createServer from '../.././server' -import { - markAsReadMutation, - markAllAsReadMutation, - notificationQuery, -} from '../../graphql/notifications' +import gql from 'graphql-tag' + +import Factory, { cleanDatabase } from '@db/factories' +import { getDriver } from '@db/neo4j' +import { markAllAsReadMutation } from '@graphql/queries/markAllAsReadMutation' +import { markAsReadMutation } from '@graphql/queries/markAsReadMutation' +import { notificationQuery } from '@graphql/queries/notificationQuery' +import createServer from '@src/server' const driver = getDriver() let authenticatedUser @@ -34,7 +38,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { diff --git a/backend/src/schema/resolvers/notifications.ts b/backend/src/graphql/resolvers/notifications.ts similarity index 89% rename from backend/src/schema/resolvers/notifications.ts rename to backend/src/graphql/resolvers/notifications.ts index 6a3e232cc..0c35c249e 100644 --- a/backend/src/schema/resolvers/notifications.ts +++ b/backend/src/graphql/resolvers/notifications.ts @@ -1,12 +1,18 @@ -import log from './helpers/databaseLogger' +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { withFilter } from 'graphql-subscriptions' -import { pubsub, NOTIFICATION_ADDED } from '../../server' + +import { NOTIFICATION_ADDED } from '@constants/subscriptions' export default { Subscription: { notificationAdded: { subscribe: withFilter( - () => pubsub.asyncIterator(NOTIFICATION_ADDED), + (_, __, context) => context.pubsub.asyncIterator(NOTIFICATION_ADDED), (payload, variables, context) => { return payload.notificationAdded.to.id === context.user?.id }, @@ -48,7 +54,7 @@ export default { MATCH (resource {deleted: false, disabled: false})-[notification:NOTIFIED]->(user:User {id:$id}) ${whereClause} OPTIONAL MATCH (relatedUser:User { id: notification.relatedUserId }) - OPTIONAL MATCH (resource)<-[membership:MEMBER_OF]-(relatedUser) + OPTIONAL MATCH (resource)<-[membership:MEMBER_OF]-(user) WITH user, notification, resource, membership, relatedUser, [(resource)<-[:WROTE]-(author:User) | author {.*}] AS authors, [(resource)-[:COMMENTS]->(post:Post)<-[:WROTE]-(author:User) | post {.*, author: properties(author), postType: [l IN labels(post) WHERE NOT l = 'Post']} ] AS posts @@ -68,7 +74,6 @@ export default { `, { id: currentUser.id }, ) - log(notificationsTransactionResponse) return notificationsTransactionResponse.records.map((record) => record.get('notification')) }) try { @@ -80,7 +85,7 @@ export default { }, }, Mutation: { - markAsRead: async (parent, args, context, resolveInfo) => { + markAsRead: async (_parent, args, context, _resolveInfo) => { const { user: currentUser } = context const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { @@ -98,7 +103,6 @@ export default { `, { resourceId: args.id, id: currentUser.id }, ) - log(markNotificationAsReadTransactionResponse) return markNotificationAsReadTransactionResponse.records.map((record) => record.get('notification'), ) @@ -110,7 +114,7 @@ export default { session.close() } }, - markAllAsRead: async (parent, args, context, resolveInfo) => { + markAllAsRead: async (parent, args, context, _resolveInfo) => { const { user: currentUser } = context const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { @@ -128,7 +132,6 @@ export default { `, { id: currentUser.id }, ) - log(markAllNotificationAsReadTransactionResponse) return markAllNotificationAsReadTransactionResponse.records.map((record) => record.get('notification'), ) diff --git a/backend/src/schema/resolvers/observePosts.spec.ts b/backend/src/graphql/resolvers/observePosts.spec.ts similarity index 86% rename from backend/src/schema/resolvers/observePosts.spec.ts rename to backend/src/graphql/resolvers/observePosts.spec.ts index 2d98c33a7..fd2786fc9 100644 --- a/backend/src/schema/resolvers/observePosts.spec.ts +++ b/backend/src/graphql/resolvers/observePosts.spec.ts @@ -1,17 +1,18 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { ApolloServer } from 'apollo-server-express' import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../../db/factories' import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' -import { createPostMutation } from '../../graphql/posts' -import CONFIG from '../../config' +import CONFIG from '@config/index' +import databaseContext from '@context/database' +import Factory, { cleanDatabase } from '@db/factories' +import { createPostMutation } from '@graphql/queries/createPostMutation' +import createServer, { getContext } from '@src/server' CONFIG.CATEGORIES_ACTIVE = false -const driver = getDriver() -const neode = getNeode() - let query let mutate let authenticatedUser @@ -37,28 +38,27 @@ const postQuery = gql` } ` +const database = databaseContext() + +let server: ApolloServer beforeAll(async () => { await cleanDatabase() - const { server } = createServer({ - context: () => { - return { - driver, - neode, - user: authenticatedUser, - cypherParams: { - currentUserId: authenticatedUser ? authenticatedUser.id : null, - }, - } - }, - }) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/require-await + const contextUser = async (_req) => authenticatedUser + const context = getContext({ user: contextUser, database }) + + server = createServer({ context }).server + query = createTestClient(server).query mutate = createTestClient(server).mutate }) afterAll(async () => { await cleanDatabase() - driver.close() + void server.stop() + void database.driver.close() + database.neode.close() }) describe('observing posts', () => { diff --git a/backend/src/schema/resolvers/passwordReset.spec.ts b/backend/src/graphql/resolvers/passwordReset.spec.ts similarity index 88% rename from backend/src/schema/resolvers/passwordReset.spec.ts rename to backend/src/graphql/resolvers/passwordReset.spec.ts index 3d17ff481..3bc4d53ba 100644 --- a/backend/src/schema/resolvers/passwordReset.spec.ts +++ b/backend/src/graphql/resolvers/passwordReset.spec.ts @@ -1,10 +1,16 @@ -import Factory, { cleanDatabase } from '../../db/factories' -import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import CONSTANTS_REGISTRATION from './../../constants/registration' -import createPasswordReset from './helpers/createPasswordReset' -import createServer from '../../server' +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ import { createTestClient } from 'apollo-server-testing' +import gql from 'graphql-tag' + +import registrationConstants from '@constants/registrationBranded' +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' + +import createPasswordReset from './helpers/createPasswordReset' const neode = getNeode() const driver = getDriver() @@ -16,6 +22,7 @@ let variables const getAllPasswordResets = async () => { const passwordResetQuery = await neode.cypher( 'MATCH (passwordReset:PasswordReset) RETURN passwordReset', + {}, ) const resets = passwordResetQuery.records.map((record) => record.get('passwordReset')) return resets @@ -38,7 +45,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(() => { @@ -64,14 +71,14 @@ describe('passwordReset', () => { describe('requestPasswordReset', () => { const mutation = gql` - mutation ($email: String!) { - requestPasswordReset(email: $email) + mutation ($email: String!, $locale: String!) { + requestPasswordReset(email: $email, locale: $locale) } ` describe('with invalid email', () => { beforeEach(() => { - variables = { ...variables, email: 'non-existent@example.org' } + variables = { ...variables, email: 'non-existent@example.org', locale: 'de' } }) it('resolves anyways', async () => { @@ -89,7 +96,7 @@ describe('passwordReset', () => { describe('with a valid email', () => { beforeEach(() => { - variables = { ...variables, email: 'user@example.org' } + variables = { ...variables, email: 'user@example.org', locale: 'de' } }) it('resolves', async () => { @@ -111,7 +118,7 @@ describe('passwordReset', () => { const resets = await getAllPasswordResets() const [reset] = resets const { nonce } = reset.properties - expect(nonce).toHaveLength(CONSTANTS_REGISTRATION.NONCE_LENGTH) + expect(nonce).toHaveLength(registrationConstants.NONCE_LENGTH) }) }) }) @@ -119,6 +126,7 @@ describe('passwordReset', () => { }) describe('resetPassword', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const setup = async (options: any = {}) => { const { email = 'user@example.org', issuedAt = new Date(), nonce = '12345' } = options await createPasswordReset({ driver, email, issuedAt, nonce }) diff --git a/backend/src/schema/resolvers/passwordReset.ts b/backend/src/graphql/resolvers/passwordReset.ts similarity index 71% rename from backend/src/schema/resolvers/passwordReset.ts rename to backend/src/graphql/resolvers/passwordReset.ts index 6fea020dd..ac437a555 100644 --- a/backend/src/schema/resolvers/passwordReset.ts +++ b/backend/src/graphql/resolvers/passwordReset.ts @@ -1,19 +1,30 @@ -import { v4 as uuid } from 'uuid' +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import bcrypt from 'bcryptjs' -import CONSTANTS_REGISTRATION from './../../constants/registration' +import { v4 as uuid } from 'uuid' + +import registrationConstants from '@constants/registrationBranded' + import createPasswordReset from './helpers/createPasswordReset' +import normalizeEmail from './helpers/normalizeEmail' export default { Mutation: { requestPasswordReset: async (_parent, { email }, { driver }) => { + email = normalizeEmail(email) // TODO: why this is generated differntly from 'backend/src/schema/resolvers/helpers/generateNonce.js'? - const nonce = uuid().substring(0, CONSTANTS_REGISTRATION.NONCE_LENGTH) + const nonce = uuid().substring(0, registrationConstants.NONCE_LENGTH) return createPasswordReset({ driver, nonce, email }) }, resetPassword: async (_parent, { email, nonce, newPassword }, { driver }) => { + email = normalizeEmail(email) const stillValid = new Date() stillValid.setDate(stillValid.getDate() - 1) - const encryptedNewPassword = await bcrypt.hashSync(newPassword, 10) + const encryptedNewPassword = await bcrypt.hash(newPassword, 10) const session = driver.session() try { const passwordResetTxPromise = session.writeTransaction(async (transaction) => { @@ -39,7 +50,7 @@ export default { ) }) const [reset] = await passwordResetTxPromise - return !!(reset && reset.properties.usedAt) + return !!reset?.properties.usedAt } finally { session.close() } diff --git a/backend/src/schema/resolvers/posts.spec.ts b/backend/src/graphql/resolvers/posts.spec.ts similarity index 64% rename from backend/src/schema/resolvers/posts.spec.ts rename to backend/src/graphql/resolvers/posts.spec.ts index d7eb063d2..7f679d2b9 100644 --- a/backend/src/schema/resolvers/posts.spec.ts +++ b/backend/src/graphql/resolvers/posts.spec.ts @@ -1,48 +1,55 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { ApolloServer } from 'apollo-server-express' import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../../db/factories' import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' -import { createPostMutation } from '../../graphql/posts' -import CONFIG from '../../config' + +import CONFIG from '@config/index' +import databaseContext from '@context/database' +import Factory, { cleanDatabase } from '@db/factories' +import Image from '@db/models/Image' +import { createGroupMutation } from '@graphql/queries/createGroupMutation' +import { createPostMutation } from '@graphql/queries/createPostMutation' +import { Post } from '@graphql/queries/Post' +import { pushPost } from '@graphql/queries/pushPost' +import { unpushPost } from '@graphql/queries/unpushPost' +import createServer, { getContext } from '@src/server' CONFIG.CATEGORIES_ACTIVE = true -const driver = getDriver() -const neode = getNeode() - -let query -let mutate -let authenticatedUser let user -const categoryIds = ['cat9', 'cat4', 'cat15'] -let variables +const database = databaseContext() + +let server: ApolloServer +let authenticatedUser +let query, mutate beforeAll(async () => { await cleanDatabase() - const { server } = createServer({ - context: () => { - return { - driver, - neode, - user: authenticatedUser, - cypherParams: { - currentUserId: authenticatedUser ? authenticatedUser.id : null, - }, - } - }, - }) - query = createTestClient(server).query - mutate = createTestClient(server).mutate + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/require-await + const contextUser = async (_req) => authenticatedUser + const context = getContext({ user: contextUser, database }) + + server = createServer({ context }).server + + const createTestClientResult = createTestClient(server) + mutate = createTestClientResult.mutate + query = createTestClientResult.query }) -afterAll(async () => { - await cleanDatabase() - driver.close() +afterAll(() => { + void server.stop() + void database.driver.close() + database.neode.close() }) +const categoryIds = ['cat9', 'cat4', 'cat15'] +let variables + beforeEach(async () => { variables = {} user = await Factory.build( @@ -57,22 +64,22 @@ beforeEach(async () => { }, ) await Promise.all([ - neode.create('Category', { + database.neode.create('Category', { id: 'cat9', name: 'Democracy & Politics', icon: 'university', }), - neode.create('Category', { + database.neode.create('Category', { id: 'cat4', name: 'Environment & Nature', icon: 'tree', }), - neode.create('Category', { + database.neode.create('Category', { id: 'cat15', name: 'Consumption & Sustainability', icon: 'shopping-cart', }), - neode.create('Category', { + database.neode.create('Category', { id: 'cat27', name: 'Animal Protection', icon: 'paw', @@ -81,7 +88,6 @@ beforeEach(async () => { authenticatedUser = null }) -// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 afterEach(async () => { await cleanDatabase() }) @@ -226,7 +232,6 @@ describe('Post', () => { Post(filter: $filter) { id author { - id name } } @@ -242,7 +247,7 @@ describe('Post', () => { Post: [ { id: 'post-by-followed-user', - author: { id: 'followed-by-me', name: 'Followed User' }, + author: { name: 'Followed User' }, }, ], }, @@ -968,9 +973,13 @@ describe('UpdatePost', () => { variables = { ...variables, image: { sensitive: true } } }) it('updates the image', async () => { - await expect(neode.first('Image', { sensitive: true })).resolves.toBeFalsy() + await expect( + database.neode.first('Image', { sensitive: true }, undefined), + ).resolves.toBeFalsy() await mutate({ mutation: updatePostMutation, variables }) - await expect(neode.first('Image', { sensitive: true })).resolves.toBeTruthy() + await expect( + database.neode.first('Image', { sensitive: true }, undefined), + ).resolves.toBeTruthy() }) }) @@ -979,9 +988,9 @@ describe('UpdatePost', () => { variables = { ...variables, image: null } }) it('deletes the image', async () => { - await expect(neode.all('Image')).resolves.toHaveLength(6) + await expect(database.neode.all('Image')).resolves.toHaveLength(6) await mutate({ mutation: updatePostMutation, variables }) - await expect(neode.all('Image')).resolves.toHaveLength(5) + await expect(database.neode.all('Image')).resolves.toHaveLength(5) }) }) @@ -990,15 +999,294 @@ describe('UpdatePost', () => { delete variables.image }) it('keeps the image unchanged', async () => { - await expect(neode.first('Image', { sensitive: true })).resolves.toBeFalsy() + await expect( + database.neode.first('Image', { sensitive: true }, undefined), + ).resolves.toBeFalsy() await mutate({ mutation: updatePostMutation, variables }) - await expect(neode.first('Image', { sensitive: true })).resolves.toBeFalsy() + await expect( + database.neode.first('Image', { sensitive: true }, undefined), + ).resolves.toBeFalsy() }) }) }) }) }) +describe('push posts', () => { + let author + beforeEach(async () => { + author = await Factory.build('user', { slug: 'the-author' }) + await Factory.build( + 'post', + { + id: 'pFirst', + }, + { + author, + categoryIds, + }, + ) + await Factory.build( + 'post', + { + id: 'pSecond', + }, + { + author, + categoryIds, + }, + ) + await Factory.build( + 'post', + { + id: 'pThird', + }, + { + author, + categoryIds, + }, + ) + }) + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + authenticatedUser = null + await expect( + mutate({ mutation: pushPost, variables: { id: 'pSecond' } }), + ).resolves.toMatchObject({ + errors: [{ message: 'Not Authorized!' }], + data: null, + }) + }) + }) + + describe('ordinary users', () => { + it('throws authorization error', async () => { + await expect( + mutate({ mutation: pushPost, variables: { id: 'pSecond' } }), + ).resolves.toMatchObject({ + errors: [{ message: 'Not Authorized!' }], + data: null, + }) + }) + }) + + describe('moderators', () => { + let moderator + beforeEach(async () => { + moderator = await user.update({ role: 'moderator', updatedAt: new Date().toISOString() }) + authenticatedUser = await moderator.toJson() + }) + + it('throws authorization error', async () => { + await expect( + mutate({ mutation: pushPost, variables: { id: 'pSecond' } }), + ).resolves.toMatchObject({ + errors: [{ message: 'Not Authorized!' }], + data: null, + }) + }) + }) + + describe('admins', () => { + let admin + beforeEach(async () => { + admin = await Factory.build('user', { + id: 'admin', + role: 'admin', + }) + authenticatedUser = await admin.toJson() + }) + + it('pushes the post to the front of the feed', async () => { + await expect( + query({ query: Post, variables: { orderBy: ['sortDate_desc'] } }), + ).resolves.toMatchObject({ + errors: undefined, + data: { + Post: [ + { + id: 'pThird', + }, + { + id: 'pSecond', + }, + { + id: 'pFirst', + }, + ], + }, + }) + await expect( + mutate({ mutation: pushPost, variables: { id: 'pSecond' } }), + ).resolves.toMatchObject({ + errors: undefined, + data: { + pushPost: { + id: 'pSecond', + }, + }, + }) + await expect( + query({ query: Post, variables: { orderBy: ['sortDate_desc'] } }), + ).resolves.toMatchObject({ + errors: undefined, + data: { + Post: [ + { + id: 'pSecond', + }, + { + id: 'pThird', + }, + { + id: 'pFirst', + }, + ], + }, + }) + }) + }) +}) + +describe('unpush posts', () => { + let author + let admin + beforeEach(async () => { + author = await Factory.build('user', { slug: 'the-author' }) + await Factory.build( + 'post', + { + id: 'pFirst', + }, + { + author, + categoryIds, + }, + ) + await Factory.build( + 'post', + { + id: 'pSecond', + }, + { + author, + categoryIds, + }, + ) + await Factory.build( + 'post', + { + id: 'pThird', + }, + { + author, + categoryIds, + }, + ) + admin = await Factory.build('user', { + id: 'admin', + role: 'admin', + }) + authenticatedUser = await admin.toJson() + await mutate({ mutation: pushPost, variables: { id: 'pSecond' } }) + authenticatedUser = null + }) + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + authenticatedUser = null + await expect( + mutate({ mutation: unpushPost, variables: { id: 'pSecond' } }), + ).resolves.toMatchObject({ + errors: [{ message: 'Not Authorized!' }], + data: null, + }) + }) + }) + + describe('ordinary users', () => { + it('throws authorization error', async () => { + authenticatedUser = await user.toJson() + await expect( + mutate({ mutation: unpushPost, variables: { id: 'pSecond' } }), + ).resolves.toMatchObject({ + errors: [{ message: 'Not Authorized!' }], + data: null, + }) + }) + }) + + describe('moderators', () => { + let moderator + beforeEach(async () => { + moderator = await user.update({ role: 'moderator', updatedAt: new Date().toISOString() }) + authenticatedUser = await moderator.toJson() + }) + + it('throws authorization error', async () => { + await expect( + mutate({ mutation: unpushPost, variables: { id: 'pSecond' } }), + ).resolves.toMatchObject({ + errors: [{ message: 'Not Authorized!' }], + data: null, + }) + }) + }) + + describe('admins', () => { + it('cancels the push of the post and puts it in the original order', async () => { + authenticatedUser = await admin.toJson() + await expect( + query({ query: Post, variables: { orderBy: ['sortDate_desc'] } }), + ).resolves.toMatchObject({ + errors: undefined, + data: { + Post: [ + { + id: 'pSecond', + }, + { + id: 'pThird', + }, + { + id: 'pFirst', + }, + ], + }, + }) + await expect( + mutate({ mutation: unpushPost, variables: { id: 'pSecond' } }), + ).resolves.toMatchObject({ + errors: undefined, + data: { + unpushPost: { + id: 'pSecond', + }, + }, + }) + await expect( + query({ query: Post, variables: { orderBy: ['sortDate_desc'] } }), + ).resolves.toMatchObject({ + errors: undefined, + data: { + Post: [ + { + id: 'pThird', + }, + { + id: 'pSecond', + }, + { + id: 'pFirst', + }, + ], + }, + }) + }) + }) +}) + describe('pin posts', () => { let author const pinPostMutation = gql` @@ -1087,8 +1375,9 @@ describe('pin posts', () => { authenticatedUser = await admin.toJson() }) - describe('are allowed to pin posts', () => { + describe('MAX_PINNED_POSTS is 0', () => { beforeEach(async () => { + CONFIG.MAX_PINNED_POSTS = 0 await Factory.build( 'post', { @@ -1101,216 +1390,580 @@ describe('pin posts', () => { variables = { ...variables, id: 'created-and-pinned-by-same-admin' } }) - it('responds with the updated Post', async () => { - const expected = { - data: { - pinPost: { - id: 'created-and-pinned-by-same-admin', - author: { - name: 'Admin', - }, - pinnedBy: { - id: 'current-user', - name: 'Admin', - role: 'admin', - }, - }, - }, - errors: undefined, - } - - await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject( - expected, - ) - }) - - it('sets createdAt date for PINNED', async () => { - const expected = { - data: { - pinPost: { - id: 'created-and-pinned-by-same-admin', - pinnedAt: expect.any(String), - }, - }, - errors: undefined, - } - await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject( - expected, - ) - }) - - it('sets redundant `pinned` property for performant ordering', async () => { - variables = { ...variables, id: 'created-and-pinned-by-same-admin' } - const expected = { - data: { pinPost: { pinned: true } }, - errors: undefined, - } - await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject( - expected, - ) - }) - }) - - describe('post created by another admin', () => { - let otherAdmin - beforeEach(async () => { - otherAdmin = await Factory.build('user', { - role: 'admin', - name: 'otherAdmin', + it('throws with error that pinning posts is not allowed', async () => { + await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject({ + data: { pinPost: null }, + errors: [{ message: 'Pinned posts are not allowed!' }], }) - authenticatedUser = await otherAdmin.toJson() - await Factory.build( - 'post', - { - id: 'created-by-one-admin-pinned-by-different-one', - }, - { - author: otherAdmin, - }, - ) + }) + }) + + describe('MAX_PINNED_POSTS is 1', () => { + beforeEach(() => { + CONFIG.MAX_PINNED_POSTS = 1 }) - it('responds with the updated Post', async () => { - authenticatedUser = await admin.toJson() - variables = { ...variables, id: 'created-by-one-admin-pinned-by-different-one' } - const expected = { - data: { - pinPost: { + describe('are allowed to pin posts', () => { + beforeEach(async () => { + await Factory.build( + 'post', + { + id: 'created-and-pinned-by-same-admin', + }, + { + author: admin, + }, + ) + variables = { ...variables, id: 'created-and-pinned-by-same-admin' } + }) + + it('responds with the updated Post', async () => { + const expected = { + data: { + pinPost: { + id: 'created-and-pinned-by-same-admin', + author: { + name: 'Admin', + }, + pinnedBy: { + id: 'current-user', + name: 'Admin', + role: 'admin', + }, + }, + }, + errors: undefined, + } + + await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject( + expected, + ) + }) + + it('sets createdAt date for PINNED', async () => { + const expected = { + data: { + pinPost: { + id: 'created-and-pinned-by-same-admin', + pinnedAt: expect.any(String), + }, + }, + errors: undefined, + } + await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject( + expected, + ) + }) + + it('sets redundant `pinned` property for performant ordering', async () => { + variables = { ...variables, id: 'created-and-pinned-by-same-admin' } + const expected = { + data: { pinPost: { pinned: true } }, + errors: undefined, + } + await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject( + expected, + ) + }) + }) + + describe('post created by another admin', () => { + let otherAdmin + beforeEach(async () => { + otherAdmin = await Factory.build('user', { + role: 'admin', + name: 'otherAdmin', + }) + authenticatedUser = await otherAdmin.toJson() + await Factory.build( + 'post', + { id: 'created-by-one-admin-pinned-by-different-one', - author: { - name: 'otherAdmin', - }, - pinnedBy: { - id: 'current-user', - name: 'Admin', - role: 'admin', + }, + { + author: otherAdmin, + }, + ) + }) + + it('responds with the updated Post', async () => { + authenticatedUser = await admin.toJson() + variables = { ...variables, id: 'created-by-one-admin-pinned-by-different-one' } + const expected = { + data: { + pinPost: { + id: 'created-by-one-admin-pinned-by-different-one', + author: { + name: 'otherAdmin', + }, + pinnedBy: { + id: 'current-user', + name: 'Admin', + role: 'admin', + }, }, }, - }, - errors: undefined, - } + errors: undefined, + } - await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject( - expected, - ) + await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject( + expected, + ) + }) + }) + + describe('post created by another user', () => { + it('responds with the updated Post', async () => { + const expected = { + data: { + pinPost: { + id: 'p9876', + author: { + slug: 'the-author', + }, + pinnedBy: { + id: 'current-user', + name: 'Admin', + role: 'admin', + }, + }, + }, + errors: undefined, + } + + await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject( + expected, + ) + }) + }) + + describe('pinned post already exists', () => { + let pinnedPost + beforeEach(async () => { + await Factory.build( + 'post', + { + id: 'only-pinned-post', + }, + { + author: admin, + }, + ) + await mutate({ mutation: pinPostMutation, variables }) + }) + + it('removes previous `pinned` attribute', async () => { + const cypher = 'MATCH (post:Post) WHERE post.pinned IS NOT NULL RETURN post' + pinnedPost = await database.neode.cypher(cypher, {}) + expect(pinnedPost.records).toHaveLength(1) + variables = { ...variables, id: 'only-pinned-post' } + await mutate({ mutation: pinPostMutation, variables }) + pinnedPost = await database.neode.cypher(cypher, {}) + expect(pinnedPost.records).toHaveLength(1) + }) + + it('removes previous PINNED relationship', async () => { + variables = { ...variables, id: 'only-pinned-post' } + await mutate({ mutation: pinPostMutation, variables }) + pinnedPost = await database.neode.cypher( + `MATCH (:User)-[pinned:PINNED]->(post:Post) RETURN post, pinned`, + {}, + ) + expect(pinnedPost.records).toHaveLength(1) + }) + }) + + describe('post in public group', () => { + beforeEach(async () => { + await mutate({ + mutation: createGroupMutation(), + variables: { + name: 'Public Group', + id: 'public-group', + about: 'This is a public group', + groupType: 'public', + actionRadius: 'regional', + description: + 'This is a public group to test if the posts of this group can be pinned.', + categoryIds, + }, + }) + await mutate({ + mutation: createPostMutation(), + variables: { + id: 'public-group-post', + title: 'Public group post', + content: 'This is a post in a public group', + groupId: 'public-group', + categoryIds, + }, + }) + variables = { ...variables, id: 'public-group-post' } + }) + + it('can be pinned', async () => { + await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject({ + data: { + pinPost: { + id: 'public-group-post', + author: { + slug: 'testuser', + }, + pinnedBy: { + id: 'current-user', + name: 'Admin', + role: 'admin', + }, + }, + }, + errors: undefined, + }) + }) + }) + + describe('post in closed group', () => { + beforeEach(async () => { + await mutate({ + mutation: createGroupMutation(), + variables: { + name: 'Closed Group', + id: 'closed-group', + about: 'This is a closed group', + groupType: 'closed', + actionRadius: 'regional', + description: + 'This is a closed group to test if the posts of this group can be pinned.', + categoryIds, + }, + }) + await mutate({ + mutation: createPostMutation(), + variables: { + id: 'closed-group-post', + title: 'Closed group post', + content: 'This is a post in a closed group', + groupId: 'closed-group', + categoryIds, + }, + }) + variables = { ...variables, id: 'closed-group-post' } + }) + + it('can not be pinned', async () => { + await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject({ + data: { + pinPost: null, + }, + errors: undefined, + }) + }) + }) + + describe('post in hidden group', () => { + beforeEach(async () => { + await mutate({ + mutation: createGroupMutation(), + variables: { + name: 'Hidden Group', + id: 'hidden-group', + about: 'This is a hidden group', + groupType: 'hidden', + actionRadius: 'regional', + description: + 'This is a hidden group to test if the posts of this group can be pinned.', + categoryIds, + }, + }) + await mutate({ + mutation: createPostMutation(), + variables: { + id: 'hidden-group-post', + title: 'Hidden group post', + content: 'This is a post in a hidden group', + groupId: 'hidden-group', + categoryIds, + }, + }) + variables = { ...variables, id: 'hidden-group-post' } + }) + + it('can not be pinned', async () => { + await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject({ + data: { + pinPost: null, + }, + errors: undefined, + }) + }) + }) + + describe('PostOrdering', () => { + beforeEach(async () => { + await Factory.build('post', { + id: 'im-a-pinned-post', + createdAt: '2019-11-22T17:26:29.070Z', + pinned: true, + }) + await Factory.build('post', { + id: 'i-was-created-before-pinned-post', + // fairly old, so this should be 3rd + createdAt: '2019-10-22T17:26:29.070Z', + }) + }) + + describe('order by `pinned_asc` and `createdAt_desc`', () => { + beforeEach(() => { + // this is the ordering in the frontend + variables = { orderBy: ['pinned_asc', 'createdAt_desc'] } + }) + + it('pinned post appear first even when created before other posts', async () => { + await expect(query({ query: Post, variables })).resolves.toMatchObject({ + data: { + Post: [ + { + id: 'im-a-pinned-post', + pinned: true, + createdAt: '2019-11-22T17:26:29.070Z', + pinnedAt: expect.any(String), + }, + { + id: 'p9876', + pinned: null, + createdAt: expect.any(String), + pinnedAt: null, + }, + { + id: 'i-was-created-before-pinned-post', + pinned: null, + createdAt: '2019-10-22T17:26:29.070Z', + pinnedAt: null, + }, + ], + }, + errors: undefined, + }) + }) + }) }) }) - describe('post created by another user', () => { - it('responds with the updated Post', async () => { - const expected = { - data: { - pinPost: { - id: 'p9876', - author: { - slug: 'the-author', - }, - pinnedBy: { - id: 'current-user', - name: 'Admin', - role: 'admin', - }, - }, - }, - errors: undefined, - } + describe('MAX_PINNED_POSTS = 3', () => { + const postsPinnedCountsQuery = `query { PostsPinnedCounts { maxPinnedPosts, currentlyPinnedPosts } }` - await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject( - expected, - ) - }) - }) - - describe('pinned post already exists', () => { - let pinnedPost beforeEach(async () => { + CONFIG.MAX_PINNED_POSTS = 3 await Factory.build( 'post', { - id: 'only-pinned-post', + id: 'first-post', + createdAt: '2019-10-22T17:26:29.070Z', }, { author: admin, }, ) - await mutate({ mutation: pinPostMutation, variables }) - }) - - it('removes previous `pinned` attribute', async () => { - const cypher = 'MATCH (post:Post) WHERE post.pinned IS NOT NULL RETURN post' - pinnedPost = await neode.cypher(cypher) - expect(pinnedPost.records).toHaveLength(1) - variables = { ...variables, id: 'only-pinned-post' } - await mutate({ mutation: pinPostMutation, variables }) - pinnedPost = await neode.cypher(cypher) - expect(pinnedPost.records).toHaveLength(1) - }) - - it('removes previous PINNED relationship', async () => { - variables = { ...variables, id: 'only-pinned-post' } - await mutate({ mutation: pinPostMutation, variables }) - pinnedPost = await neode.cypher( - `MATCH (:User)-[pinned:PINNED]->(post:Post) RETURN post, pinned`, + await Factory.build( + 'post', + { + id: 'second-post', + createdAt: '2018-10-22T17:26:29.070Z', + }, + { + author: admin, + }, + ) + await Factory.build( + 'post', + { + id: 'third-post', + createdAt: '2017-10-22T17:26:29.070Z', + }, + { + author: admin, + }, + ) + await Factory.build( + 'post', + { + id: 'another-post', + }, + { + author: admin, + }, ) - expect(pinnedPost.records).toHaveLength(1) - }) - }) - - describe('PostOrdering', () => { - beforeEach(async () => { - await Factory.build('post', { - id: 'im-a-pinned-post', - createdAt: '2019-11-22T17:26:29.070Z', - pinned: true, - }) - await Factory.build('post', { - id: 'i-was-created-before-pinned-post', - // fairly old, so this should be 3rd - createdAt: '2019-10-22T17:26:29.070Z', - }) }) - describe('order by `pinned_asc` and `createdAt_desc`', () => { - beforeEach(() => { - // this is the ordering in the frontend - variables = { orderBy: ['pinned_asc', 'createdAt_desc'] } + describe('first post', () => { + let result + + beforeEach(async () => { + variables = { ...variables, id: 'first-post' } + result = await mutate({ mutation: pinPostMutation, variables }) }) - it('pinned post appear first even when created before other posts', async () => { - const postOrderingQuery = gql` - query ($orderBy: [_PostOrdering]) { - Post(orderBy: $orderBy) { - id - pinned - createdAt - pinnedAt - } - } - ` - await expect(query({ query: postOrderingQuery, variables })).resolves.toMatchObject({ + it('pins the first post', () => { + expect(result).toMatchObject({ data: { - Post: [ - { - id: 'im-a-pinned-post', - pinned: true, - createdAt: '2019-11-22T17:26:29.070Z', - pinnedAt: expect.any(String), + pinPost: { + id: 'first-post', + pinned: true, + pinnedAt: expect.any(String), + pinnedBy: { + id: 'current-user', }, - { - id: 'p9876', - pinned: null, - createdAt: expect.any(String), - pinnedAt: null, - }, - { - id: 'i-was-created-before-pinned-post', - pinned: null, - createdAt: '2019-10-22T17:26:29.070Z', - pinnedAt: null, - }, - ], + }, }, - errors: undefined, + }) + }) + + it('returns the correct counts', async () => { + await expect( + query({ + query: postsPinnedCountsQuery, + }), + ).resolves.toMatchObject({ + data: { + PostsPinnedCounts: { + maxPinnedPosts: 3, + currentlyPinnedPosts: 1, + }, + }, + }) + }) + + describe('second post', () => { + beforeEach(async () => { + variables = { ...variables, id: 'second-post' } + result = await mutate({ mutation: pinPostMutation, variables }) + }) + + it('pins the second post', () => { + expect(result).toMatchObject({ + data: { + pinPost: { + id: 'second-post', + pinned: true, + pinnedAt: expect.any(String), + pinnedBy: { + id: 'current-user', + }, + }, + }, + }) + }) + + it('returns the correct counts', async () => { + await expect( + query({ + query: postsPinnedCountsQuery, + }), + ).resolves.toMatchObject({ + data: { + PostsPinnedCounts: { + maxPinnedPosts: 3, + currentlyPinnedPosts: 2, + }, + }, + }) + }) + + describe('third post', () => { + beforeEach(async () => { + variables = { ...variables, id: 'third-post' } + result = await mutate({ mutation: pinPostMutation, variables }) + }) + + it('pins the second post', () => { + expect(result).toMatchObject({ + data: { + pinPost: { + id: 'third-post', + pinned: true, + pinnedAt: expect.any(String), + pinnedBy: { + id: 'current-user', + }, + }, + }, + }) + }) + + it('returns the correct counts', async () => { + await expect( + query({ + query: postsPinnedCountsQuery, + }), + ).resolves.toMatchObject({ + data: { + PostsPinnedCounts: { + maxPinnedPosts: 3, + currentlyPinnedPosts: 3, + }, + }, + }) + }) + + describe('another post', () => { + beforeEach(async () => { + variables = { ...variables, id: 'another-post' } + result = await mutate({ mutation: pinPostMutation, variables }) + }) + + it('throws with max pinned posts is reached', () => { + expect(result).toMatchObject({ + data: { pinPost: null }, + errors: [{ message: 'Max number of pinned posts is reached!' }], + }) + }) + }) + + describe('post ordering', () => { + beforeEach(() => { + // this is the ordering in the frontend + variables = { orderBy: ['pinned_asc', 'createdAt_desc'] } + }) + + it('places the pinned posts first, though they are much older', async () => { + await expect(query({ query: Post, variables })).resolves.toMatchObject({ + data: { + Post: [ + { + id: 'first-post', + pinned: true, + pinnedAt: expect.any(String), + createdAt: '2019-10-22T17:26:29.070Z', + }, + { + id: 'second-post', + pinned: true, + pinnedAt: expect.any(String), + createdAt: '2018-10-22T17:26:29.070Z', + }, + { + id: 'third-post', + pinned: true, + pinnedAt: expect.any(String), + createdAt: '2017-10-22T17:26:29.070Z', + }, + { + id: 'another-post', + pinned: null, + pinnedAt: null, + createdAt: expect.any(String), + }, + { + id: 'p9876', + pinned: null, + pinnedAt: null, + createdAt: expect.any(String), + }, + ], + }, + errors: undefined, + }) + }) + }) }) }) }) @@ -1462,7 +2115,7 @@ describe('DeletePost', () => { }, { image: Factory.build('image', { - url: 'path/to/some/image', + url: 'http://localhost/path/to/some/image', }), author, categoryIds, @@ -1577,7 +2230,7 @@ describe('emotions', () => { ` beforeEach(async () => { - author = await neode.create('User', { id: 'u257' }) + author = await database.neode.create('User', { id: 'u257' }) postToEmote = await Factory.build( 'post', { @@ -1612,7 +2265,7 @@ describe('emotions', () => { ` let postsEmotionsQueryVariables - beforeEach(async () => { + beforeEach(() => { postsEmotionsQueryVariables = { id: 'p1376' } }) diff --git a/backend/src/schema/resolvers/posts.ts b/backend/src/graphql/resolvers/posts.ts similarity index 77% rename from backend/src/schema/resolvers/posts.ts rename to backend/src/graphql/resolvers/posts.ts index ce342cea7..cef255634 100644 --- a/backend/src/schema/resolvers/posts.ts +++ b/backend/src/graphql/resolvers/posts.ts @@ -1,15 +1,24 @@ -import { v4 as uuid } from 'uuid' -import { neo4jgraphql } from 'neo4j-graphql-js' -import { isEmpty } from 'lodash' +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { UserInputError } from 'apollo-server' -import { mergeImage, deleteImage } from './images/images' -import Resolver from './helpers/Resolver' +import { isEmpty } from 'lodash' +import { neo4jgraphql } from 'neo4j-graphql-js' +import { v4 as uuid } from 'uuid' + +import CONFIG from '@config/index' +import { Context } from '@src/server' + +import { validateEventParams } from './helpers/events' import { filterForMutedUsers } from './helpers/filterForMutedUsers' import { filterInvisiblePosts } from './helpers/filterInvisiblePosts' import { filterPostsOfMyGroups } from './helpers/filterPostsOfMyGroups' -import { validateEventParams } from './helpers/events' +import Resolver from './helpers/Resolver' +import { images } from './images/images' import { createOrUpdateLocations } from './users/location' -import CONFIG from '../../config' const maintainPinnedPosts = (params) => { const pinnedPostFilter = { pinned: true } @@ -46,7 +55,7 @@ export default { params = await filterForMutedUsers(params, context) return neo4jgraphql(object, params, context, resolveInfo) }, - PostsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => { + PostsEmotionsCountByEmotion: async (_object, params, context, _resolveInfo) => { const { postId, data } = params const session = context.driver.session() const readTxResultPromise = session.readTransaction(async (transaction) => { @@ -68,7 +77,7 @@ export default { session.close() } }, - PostsEmotionsByCurrentUser: async (object, params, context, resolveInfo) => { + PostsEmotionsByCurrentUser: async (_object, params, context, _resolveInfo) => { const { postId } = params const session = context.driver.session() const readTxResultPromise = session.readTransaction(async (transaction) => { @@ -88,6 +97,17 @@ export default { session.close() } }, + PostsPinnedCounts: async (_object, params, context: Context, _resolveInfo) => { + const [postsPinnedCount] = ( + await context.database.query({ + query: 'MATCH (p:Post { pinned: true }) RETURN COUNT (p) AS count', + }) + ).records.map((r) => Number(r.get('count').toString())) + return { + maxPinnedPosts: CONFIG.MAX_PINNED_POSTS, + currentlyPinnedPosts: postsPinnedCount, + } + }, }, Mutation: { CreatePost: async (_parent, params, context, _resolveInfo) => { @@ -138,6 +158,7 @@ export default { SET post += $params SET post.createdAt = toString(datetime()) SET post.updatedAt = toString(datetime()) + SET post.sortDate = toString(datetime()) SET post.clickedCount = 0 SET post.viewedTeaserCount = 0 SET post:${params.postType} @@ -156,7 +177,7 @@ export default { ) const [post] = createPostTransactionResponse.records.map((record) => record.get('post')) if (imageInput) { - await mergeImage(post, 'HERO_IMAGE', imageInput, { transaction }) + await images.mergeImage(post, 'HERO_IMAGE', imageInput, { transaction }) } return post }) @@ -227,7 +248,7 @@ export default { updatePostVariables, ) const [post] = updatePostTransactionResponse.records.map((record) => record.get('post')) - await mergeImage(post, 'HERO_IMAGE', imageInput, { transaction }) + await images.mergeImage(post, 'HERO_IMAGE', imageInput, { transaction }) return post }) const post = await writeTxResultPromise @@ -240,7 +261,7 @@ export default { } }, - DeletePost: async (object, args, context, resolveInfo) => { + DeletePost: async (_object, args, context, _resolveInfo) => { const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { const deletePostTransactionResponse = await transaction.run( @@ -257,7 +278,7 @@ export default { { postId: args.id }, ) const [post] = deletePostTransactionResponse.records.map((record) => record.get('post')) - await deleteImage(post, 'HERO_IMAGE', { transaction }) + await images.deleteImage(post, 'HERO_IMAGE', { transaction }) return post }) try { @@ -267,7 +288,7 @@ export default { session.close() } }, - AddPostEmotions: async (object, params, context, resolveInfo) => { + AddPostEmotions: async (_object, params, context, _resolveInfo) => { const { to, data } = params const { user } = context const session = context.driver.session() @@ -294,7 +315,7 @@ export default { session.close() } }, - RemovePostEmotions: async (object, params, context, resolveInfo) => { + RemovePostEmotions: async (_object, params, context, _resolveInfo) => { const { to, data } = params const { id: from } = context.user const session = context.driver.session() @@ -322,56 +343,80 @@ export default { session.close() } }, - pinPost: async (_parent, params, context, _resolveInfo) => { + pinPost: async (_parent, params, context: Context, _resolveInfo) => { + if (CONFIG.MAX_PINNED_POSTS === 0) throw new Error('Pinned posts are not allowed!') let pinnedPostWithNestedAttributes const { driver, user } = context const session = driver.session() const { id: userId } = user - let writeTxResultPromise = session.writeTransaction(async (transaction) => { - const deletePreviousRelationsResponse = await transaction.run( - ` + const pinPostCypher = ` + MATCH (user:User {id: $userId}) WHERE user.role = 'admin' + MATCH (post:Post {id: $params.id}) + WHERE NOT EXISTS((post)-[:IN]->(:Group)) OR + (post)-[:IN]->(:Group { groupType: 'public'}) + MERGE (user)-[pinned:PINNED {createdAt: toString(datetime())}]->(post) + SET post.pinned = true + RETURN post, pinned.createdAt as pinnedAt` + + if (CONFIG.MAX_PINNED_POSTS === 1) { + let writeTxResultPromise = session.writeTransaction(async (transaction) => { + const deletePreviousRelationsResponse = await transaction.run( + ` MATCH (:User)-[previousRelations:PINNED]->(post:Post) REMOVE post.pinned DELETE previousRelations RETURN post `, - ) - return deletePreviousRelationsResponse.records.map( - (record) => record.get('post').properties, - ) - }) - try { - await writeTxResultPromise - - writeTxResultPromise = session.writeTransaction(async (transaction) => { - const pinPostTransactionResponse = await transaction.run( - ` - MATCH (user:User {id: $userId}) WHERE user.role = 'admin' - MATCH (post:Post {id: $params.id}) - WHERE NOT((post)-[:IN]->(:Group)) - MERGE (user)-[pinned:PINNED {createdAt: toString(datetime())}]->(post) - SET post.pinned = true - RETURN post, pinned.createdAt as pinnedAt - `, - { userId, params }, ) - return pinPostTransactionResponse.records.map((record) => ({ - pinnedPost: record.get('post').properties, - pinnedAt: record.get('pinnedAt'), - })) + return deletePreviousRelationsResponse.records.map( + (record) => record.get('post').properties, + ) }) - const [transactionResult] = await writeTxResultPromise - if (transactionResult) { - const { pinnedPost, pinnedAt } = transactionResult - pinnedPostWithNestedAttributes = { - ...pinnedPost, - pinnedAt, + try { + await writeTxResultPromise + + writeTxResultPromise = session.writeTransaction(async (transaction) => { + const pinPostTransactionResponse = await transaction.run(pinPostCypher, { + userId, + params, + }) + return pinPostTransactionResponse.records.map((record) => ({ + pinnedPost: record.get('post').properties, + pinnedAt: record.get('pinnedAt'), + })) + }) + const [transactionResult] = await writeTxResultPromise + if (transactionResult) { + const { pinnedPost, pinnedAt } = transactionResult + pinnedPostWithNestedAttributes = { + ...pinnedPost, + pinnedAt, + } } + } finally { + await session.close() } - } finally { - session.close() + return pinnedPostWithNestedAttributes + } else { + const [currentPinnedPostCount] = ( + await context.database.query({ + query: `MATCH (:User)-[:PINNED]->(post:Post { pinned: true }) RETURN COUNT(post) AS count`, + }) + ).records.map((r) => Number(r.get('count').toString())) + if (currentPinnedPostCount >= CONFIG.MAX_PINNED_POSTS) { + throw new Error('Max number of pinned posts is reached!') + } + const [pinPostResult] = ( + await context.database.write({ + query: pinPostCypher, + variables: { userId, params }, + }) + ).records.map((r) => ({ + ...r.get('post').properties, + pinnedAt: r.get('pinnedAt'), + })) + return pinPostResult } - return pinnedPostWithNestedAttributes }, unpinPost: async (_parent, params, context, _resolveInfo) => { let unpinnedPost @@ -449,6 +494,40 @@ export default { session.close() } }, + pushPost: async (_parent, params, context: Context, _resolveInfo) => { + const posts = ( + await context.database.write({ + query: ` + MATCH (post:Post {id: $id}) + SET post.sortDate = toString(datetime()) + RETURN post {.*}`, + variables: params, + }) + ).records.map((record) => record.get('post')) + + if (posts.length !== 1) { + throw new Error('Could not find Post') + } + + return posts[0] + }, + unpushPost: async (_parent, params, context: Context, _resolveInfo) => { + const posts = ( + await context.database.write({ + query: ` + MATCH (post:Post {id: $id}) + SET post.sortDate = post.createdAt + RETURN post {.*}`, + variables: params, + }) + ).records.map((record) => record.get('post')) + + if (posts.length !== 1) { + throw new Error('Could not find Post') + } + + return posts[0] + }, }, Post: { ...Resolver('Post', { @@ -497,7 +576,7 @@ export default { 'MATCH (this)<-[obs:OBSERVES]-(related:User {id: $cypherParams.currentUserId}) WHERE obs.active = true RETURN COUNT(related) >= 1', }, }), - relatedContributions: async (parent, params, context, resolveInfo) => { + relatedContributions: async (parent, _params, context, _resolveInfo) => { if (typeof parent.relatedContributions !== 'undefined') return parent.relatedContributions const { id } = parent const session = context.driver.session() diff --git a/backend/src/schema/resolvers/postsInGroups.spec.ts b/backend/src/graphql/resolvers/postsInGroups.spec.ts similarity index 97% rename from backend/src/schema/resolvers/postsInGroups.spec.ts rename to backend/src/graphql/resolvers/postsInGroups.spec.ts index c7fc34ec7..d50451468 100644 --- a/backend/src/schema/resolvers/postsInGroups.spec.ts +++ b/backend/src/graphql/resolvers/postsInGroups.spec.ts @@ -1,37 +1,34 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { ApolloServer } from 'apollo-server-express' import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../../db/factories' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' -import { - createGroupMutation, - changeGroupMemberRoleMutation, - leaveGroupMutation, -} from '../../graphql/groups' -import { - createPostMutation, - postQuery, - filterPosts, - profilePagePosts, - searchPosts, -} from '../../graphql/posts' -import { createCommentMutation } from '../../graphql/comments' -// eslint-disable-next-line no-unused-vars -import { DESCRIPTION_WITHOUT_HTML_LENGTH_MIN } from '../../constants/groups' -import CONFIG from '../../config' -import { signupVerificationMutation } from '../../graphql/authentications' + +import CONFIG from '@config/index' +import databaseContext from '@context/database' +import Factory, { cleanDatabase } from '@db/factories' +import { changeGroupMemberRoleMutation } from '@graphql/queries/changeGroupMemberRoleMutation' +import { createCommentMutation } from '@graphql/queries/createCommentMutation' +import { createGroupMutation } from '@graphql/queries/createGroupMutation' +import { createPostMutation } from '@graphql/queries/createPostMutation' +import { filterPosts } from '@graphql/queries/filterPosts' +import { leaveGroupMutation } from '@graphql/queries/leaveGroupMutation' +import { postQuery } from '@graphql/queries/postQuery' +import { profilePagePosts } from '@graphql/queries/profilePagePosts' +import { searchPosts } from '@graphql/queries/searchPosts' +import { signupVerificationMutation } from '@graphql/queries/signupVerificationMutation' +import createServer, { getContext } from '@src/server' CONFIG.CATEGORIES_ACTIVE = false -jest.mock('../../constants/groups', () => { +jest.mock('@constants/groups', () => { return { __esModule: true, DESCRIPTION_WITHOUT_HTML_LENGTH_MIN: 5, } }) -const driver = getDriver() -const neode = getNeode() - let query let mutate let anyUser @@ -43,28 +40,26 @@ let hiddenUser let authenticatedUser let newUser +const database = databaseContext() + +let server: ApolloServer beforeAll(async () => { await cleanDatabase() - const { server } = createServer({ - context: () => { - return { - driver, - neode, - user: authenticatedUser, - cypherParams: { - currentUserId: authenticatedUser ? authenticatedUser.id : null, - }, - } - }, - }) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + const contextUser = async (_req) => authenticatedUser + const context = getContext({ user: contextUser, database }) + + server = createServer({ context }).server query = createTestClient(server).query mutate = createTestClient(server).mutate }) afterAll(async () => { await cleanDatabase() - driver.close() + void server.stop() + void database.driver.close() + database.neode.close() }) describe('Posts in Groups', () => { diff --git a/backend/src/schema/resolvers/registration.spec.ts b/backend/src/graphql/resolvers/registration.spec.ts similarity index 75% rename from backend/src/schema/resolvers/registration.spec.ts rename to backend/src/graphql/resolvers/registration.spec.ts index 54e7f1ba7..fe8dc40e0 100644 --- a/backend/src/schema/resolvers/registration.spec.ts +++ b/backend/src/graphql/resolvers/registration.spec.ts @@ -1,56 +1,62 @@ -import Factory, { cleanDatabase } from '../../db/factories' -import gql from 'graphql-tag' -import { getDriver, getNeode } from '../../db/neo4j' -import createServer from '../../server' +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { ApolloServer } from 'apollo-server-express' import { createTestClient } from 'apollo-server-testing' -import CONFIG from '../../config' +import gql from 'graphql-tag' -const neode = getNeode() +import CONFIG from '@config/index' +import databaseContext from '@context/database' +import Factory, { cleanDatabase } from '@db/factories' +import EmailAddress from '@db/models/EmailAddress' +import User from '@db/models/User' +import createServer, { getContext } from '@src/server' -let mutate -let authenticatedUser let variables -const driver = getDriver() + +const database = databaseContext() + +let server: ApolloServer +let authenticatedUser +let mutate beforeAll(async () => { await cleanDatabase() - const { server } = createServer({ - context: () => { - return { - driver, - neode, - user: authenticatedUser, - } - }, - }) - mutate = createTestClient(server).mutate + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/require-await + const contextUser = async (_req) => authenticatedUser + const context = getContext({ user: contextUser, database }) + + server = createServer({ context }).server + + const createTestClientResult = createTestClient(server) + mutate = createTestClientResult.mutate }) -afterAll(async () => { - await cleanDatabase() - driver.close() +afterAll(() => { + void server.stop() + void database.driver.close() + database.neode.close() }) -beforeEach(async () => { +beforeEach(() => { variables = {} }) -// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 afterEach(async () => { await cleanDatabase() }) describe('Signup', () => { const mutation = gql` - mutation ($email: String!, $inviteCode: String) { - Signup(email: $email, inviteCode: $inviteCode) { + mutation ($email: String!, $locale: String!, $inviteCode: String) { + Signup(email: $email, locale: $locale, inviteCode: $inviteCode) { email } } ` beforeEach(() => { - variables = { ...variables, email: 'someuser@example.org' } + variables = { ...variables, email: 'someuser@example.org', locale: 'de' } }) describe('unauthenticated', () => { @@ -91,17 +97,27 @@ describe('Signup', () => { describe('creates a EmailAddress node', () => { it('with `createdAt` attribute', async () => { await mutate({ mutation, variables }) - let emailAddress = await neode.first('EmailAddress', { email: 'someuser@example.org' }) - emailAddress = await emailAddress.toJson() - expect(emailAddress.createdAt).toBeTruthy() - expect(Date.parse(emailAddress.createdAt)).toEqual(expect.any(Number)) + const emailAddress = await database.neode.first( + 'EmailAddress', + { email: 'someuser@example.org' }, + undefined, + ) + const emailAddressJson = await emailAddress.toJson() + expect(emailAddressJson.createdAt).toBeTruthy() + expect(Date.parse(emailAddressJson.createdAt as unknown as string)).toEqual( + expect.any(Number), + ) }) it('with a cryptographic `nonce`', async () => { await mutate({ mutation, variables }) - let emailAddress = await neode.first('EmailAddress', { email: 'someuser@example.org' }) - emailAddress = await emailAddress.toJson() - expect(emailAddress.nonce).toEqual(expect.any(String)) + const emailAddress = await database.neode.first( + 'EmailAddress', + { email: 'someuser@example.org' }, + undefined, + ) + const emailAddressJson = await emailAddress.toJson() + expect(emailAddressJson.nonce).toEqual(expect.any(String)) }) describe('if the email already exists', () => { @@ -136,12 +152,12 @@ describe('Signup', () => { it('creates no additional `EmailAddress` node', async () => { // admin account and the already existing user - await expect(neode.all('EmailAddress')).resolves.toHaveLength(2) + await expect(database.neode.all('EmailAddress')).resolves.toHaveLength(2) await expect(mutate({ mutation, variables })).resolves.toMatchObject({ data: { Signup: { email: 'someuser@example.org' } }, errors: undefined, }) - await expect(neode.all('EmailAddress')).resolves.toHaveLength(2) + await expect(database.neode.all('EmailAddress')).resolves.toHaveLength(2) }) }) }) @@ -177,7 +193,7 @@ describe('SignupVerification', () => { } ` describe('given valid password and email', () => { - beforeEach(async () => { + beforeEach(() => { variables = { ...variables, nonce: '12345', @@ -190,7 +206,7 @@ describe('SignupVerification', () => { }) describe('unauthenticated', () => { - beforeEach(async () => { + beforeEach(() => { authenticatedUser = null }) @@ -198,8 +214,8 @@ describe('SignupVerification', () => { beforeEach(async () => { const { email, nonce } = variables const [emailAddress, user] = await Promise.all([ - neode.model('EmailAddress').create({ email, nonce }), - neode + database.neode.model('EmailAddress').create({ email, nonce }), + database.neode .model('User') .create({ name: 'Somebody', password: '1234', email: 'john@example.org' }), ]) @@ -225,7 +241,7 @@ describe('SignupVerification', () => { email: 'john@example.org', nonce: '12345', } - await neode.model('EmailAddress').create(args) + await database.neode.model('EmailAddress').create(args) }) describe('sending a valid nonce', () => { @@ -241,7 +257,11 @@ describe('SignupVerification', () => { it('sets `verifiedAt` attribute of EmailAddress', async () => { await mutate({ mutation, variables }) - const email = await neode.first('EmailAddress', { email: 'john@example.org' }) + const email = await database.neode.first( + 'EmailAddress', + { email: 'john@example.org' }, + undefined, + ) await expect(email.toJson()).resolves.toEqual( expect.objectContaining({ verifiedAt: expect.any(String), @@ -255,14 +275,18 @@ describe('SignupVerification', () => { RETURN email ` await mutate({ mutation, variables }) - const { records: emails } = await neode.cypher(cypher, { name: 'John Doe' }) + const { records: emails } = await database.neode.cypher(cypher, { name: 'John Doe' }) expect(emails).toHaveLength(1) }) it('sets `about` attribute of User', async () => { variables = { ...variables, about: 'Find this description in the user profile' } await mutate({ mutation, variables }) - const user = await neode.first('User', { name: 'John Doe' }) + const user = await database.neode.first( + 'User', + { name: 'John Doe' }, + undefined, + ) await expect(user.toJson()).resolves.toMatchObject({ about: 'Find this description in the user profile', }) @@ -285,7 +309,7 @@ describe('SignupVerification', () => { RETURN email ` await mutate({ mutation, variables }) - const { records: emails } = await neode.cypher(cypher, { name: 'John Doe' }) + const { records: emails } = await database.neode.cypher(cypher, { name: 'John Doe' }) expect(emails).toHaveLength(1) }) diff --git a/backend/src/graphql/resolvers/registration.ts b/backend/src/graphql/resolvers/registration.ts new file mode 100644 index 000000000..db24ed7d0 --- /dev/null +++ b/backend/src/graphql/resolvers/registration.ts @@ -0,0 +1,120 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { UserInputError } from 'apollo-server' +import { hash } from 'bcryptjs' + +import { getNeode } from '@db/neo4j' +import { Context } from '@src/server' + +import existingEmailAddress from './helpers/existingEmailAddress' +import generateNonce from './helpers/generateNonce' +import normalizeEmail from './helpers/normalizeEmail' +import { redeemInviteCode } from './inviteCodes' +import { createOrUpdateLocations } from './users/location' + +const neode = getNeode() + +export default { + Mutation: { + Signup: async (_parent, args, context) => { + args.nonce = generateNonce() + args.email = normalizeEmail(args.email) + let emailAddress = await existingEmailAddress({ args, context }) + /* + if (emailAddress.user) { + // what to do? + } + */ + if (emailAddress.alreadyExistingEmail) return emailAddress.alreadyExistingEmail + try { + emailAddress = await neode.create('EmailAddress', args) + return emailAddress.toJson() + } catch (e) { + throw new UserInputError(e.message) + } + }, + SignupVerification: async (_parent, args, context: Context) => { + const { termsAndConditionsAgreedVersion } = args + const regEx = /^[0-9]+\.[0-9]+\.[0-9]+$/g + if (!regEx.test(termsAndConditionsAgreedVersion)) { + throw new UserInputError('Invalid version format!') + } + args.termsAndConditionsAgreedAt = new Date().toISOString() + + let { nonce, email, inviteCode, locationName } = args + email = normalizeEmail(email) + delete args.nonce + delete args.email + delete args.inviteCode + args.encryptedPassword = await hash(args.password, 10) + delete args.password + delete args.locationName + + if (locationName === '') locationName = null + + const { driver } = context + const session = driver.session() + const writeTxResultPromise = session.writeTransaction(async (transaction) => { + const createUserTransactionResponse = await transaction.run( + ` + MATCH (email:EmailAddress {nonce: $nonce, email: $email}) + WHERE NOT (email)-[:BELONGS_TO]->() + CREATE (user:User) + MERGE (user)-[:PRIMARY_EMAIL]->(email) + MERGE (user)<-[:BELONGS_TO]-(email) + SET user += $args + SET user.id = randomUUID() + SET user.role = 'user' + SET user.createdAt = toString(datetime()) + SET user.updatedAt = toString(datetime()) + SET user.allowEmbedIframes = false + SET user.showShoutsPublicly = false + SET user.locationName = $locationName + SET email.verifiedAt = toString(datetime()) + WITH user + OPTIONAL MATCH (post:Post)-[:IN]->(group:Group) + WHERE NOT group.groupType = 'public' + WITH user, collect(post) AS invisiblePosts + FOREACH (invisiblePost IN invisiblePosts | + MERGE (user)-[:CANNOT_SEE]->(invisiblePost) + ) + RETURN user {.*} + `, + { + args, + nonce, + email, + inviteCode, + locationName, + }, + ) + const [user] = createUserTransactionResponse.records.map((record) => record.get('user')) + if (!user) throw new UserInputError('Invalid email or nonce') + + return user + }) + try { + const user = await writeTxResultPromise + + // To allow redeeming and return an User object we require a User in the context + context.user = user + + if (inviteCode) { + await redeemInviteCode(context, inviteCode, true) + } + + await createOrUpdateLocations('User', user.id, locationName, session) + return user + } catch (e) { + if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed') + throw new UserInputError('User with this slug already exists!') + throw new UserInputError(e.message) + } finally { + await session.close() + } + }, + }, +} diff --git a/backend/src/schema/resolvers/reports.spec.ts b/backend/src/graphql/resolvers/reports.spec.ts similarity index 98% rename from backend/src/schema/resolvers/reports.spec.ts rename to backend/src/graphql/resolvers/reports.spec.ts index bc47778c1..bcbe1df4e 100644 --- a/backend/src/schema/resolvers/reports.spec.ts +++ b/backend/src/graphql/resolvers/reports.spec.ts @@ -1,8 +1,13 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' -import createServer from '../.././server' -import Factory, { cleanDatabase } from '../../db/factories' import gql from 'graphql-tag' -import { getDriver, getNeode } from '../../db/neo4j' + +import Factory, { cleanDatabase } from '@db/factories' +import { getDriver, getNeode } from '@db/neo4j' +import createServer from '@src/server' const instance = getNeode() const driver = getDriver() @@ -117,7 +122,7 @@ describe('file a report on a resource', () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) // TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 diff --git a/backend/src/schema/resolvers/reports.ts b/backend/src/graphql/resolvers/reports.ts similarity index 95% rename from backend/src/schema/resolvers/reports.ts rename to backend/src/graphql/resolvers/reports.ts index f7945e060..b8886c48f 100644 --- a/backend/src/schema/resolvers/reports.ts +++ b/backend/src/graphql/resolvers/reports.ts @@ -1,5 +1,8 @@ -import log from './helpers/databaseLogger' - +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ export default { Mutation: { fileReport: async (_parent, params, context, _resolveInfo) => { @@ -28,7 +31,6 @@ export default { reasonDescription, }, ) - log(fileReportTransactionResponse) return fileReportTransactionResponse.records.map((record) => record.get('filedReport')) }) try { @@ -101,7 +103,6 @@ export default { ${offset} ${limit} `, ) - log(reportsTransactionResponse) return reportsTransactionResponse.records.map((record) => record.get('report')) }) try { @@ -126,7 +127,6 @@ export default { `, { id }, ) - log(filedReportsTransactionResponse) return filedReportsTransactionResponse.records.map((record) => ({ submitter: record.get('submitter').properties, filed: record.get('filed').properties, @@ -161,7 +161,6 @@ export default { `, { id }, ) - log(reviewedReportsTransactionResponse) return reviewedReportsTransactionResponse.records.map((record) => ({ review: record.get('review').properties, moderator: record.get('moderator').properties, diff --git a/backend/src/graphql/resolvers/roles.ts b/backend/src/graphql/resolvers/roles.ts new file mode 100644 index 000000000..006d4f5ba --- /dev/null +++ b/backend/src/graphql/resolvers/roles.ts @@ -0,0 +1,8 @@ +/* eslint-disable @typescript-eslint/require-await */ +export default { + Query: { + availableRoles: async (_parent, _args, _context, _resolveInfo) => { + return ['admin', 'moderator', 'user'] + }, + }, +} diff --git a/backend/src/schema/resolvers/rooms.spec.ts b/backend/src/graphql/resolvers/rooms.spec.ts similarity index 91% rename from backend/src/schema/resolvers/rooms.spec.ts rename to backend/src/graphql/resolvers/rooms.spec.ts index 2e26dc1e3..9a226a2f8 100644 --- a/backend/src/schema/resolvers/rooms.spec.ts +++ b/backend/src/graphql/resolvers/rooms.spec.ts @@ -1,9 +1,15 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../../db/factories' -import { getNeode, getDriver } from '../../db/neo4j' -import { createRoomMutation, roomQuery, unreadRoomsQuery } from '../../graphql/rooms' -import { createMessageMutation } from '../../graphql/messages' -import createServer from '../../server' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import { createMessageMutation } from '@graphql/queries/createMessageMutation' +import { createRoomMutation } from '@graphql/queries/createRoomMutation' +import { roomQuery } from '@graphql/queries/roomQuery' +import { unreadRoomsQuery } from '@graphql/queries/unreadRoomsQuery' +import createServer from '@src/server' const driver = getDriver() const neode = getNeode() @@ -34,7 +40,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('Room', () => { @@ -386,6 +392,34 @@ describe('Room', () => { }, }) }) + + it('when chattingUser is blocked has 0 unread rooms', async () => { + authenticatedUser = await otherChattingUser.toJson() + await otherChattingUser.relateTo(chattingUser, 'blocked') + await expect( + query({ + query: unreadRoomsQuery(), + }), + ).resolves.toMatchObject({ + data: { + UnreadRooms: 0, + }, + }) + }) + + it('when chattingUser is muted has 0 unread rooms', async () => { + authenticatedUser = await otherChattingUser.toJson() + await otherChattingUser.relateTo(chattingUser, 'muted') + await expect( + query({ + query: unreadRoomsQuery(), + }), + ).resolves.toMatchObject({ + data: { + UnreadRooms: 0, + }, + }) + }) }) describe('as not chatting user', () => { @@ -557,6 +591,7 @@ describe('Room', () => { }) describe('query single room', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any let result: any = null beforeAll(async () => { diff --git a/backend/src/schema/resolvers/rooms.ts b/backend/src/graphql/resolvers/rooms.ts similarity index 81% rename from backend/src/schema/resolvers/rooms.ts rename to backend/src/graphql/resolvers/rooms.ts index 5382c5ee7..e3422a5ce 100644 --- a/backend/src/schema/resolvers/rooms.ts +++ b/backend/src/graphql/resolvers/rooms.ts @@ -1,13 +1,23 @@ -import { neo4jgraphql } from 'neo4j-graphql-js' -import Resolver from './helpers/Resolver' -import { pubsub, ROOM_COUNT_UPDATED } from '../../server' +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { withFilter } from 'graphql-subscriptions' +import { neo4jgraphql } from 'neo4j-graphql-js' + +import { ROOM_COUNT_UPDATED } from '@constants/subscriptions' + +import Resolver from './helpers/Resolver' export const getUnreadRoomsCount = async (userId, session) => { return session.readTransaction(async (transaction) => { const unreadRoomsCypher = ` - MATCH (:User { id: $userId })-[:CHATS_IN]->(room:Room)<-[:INSIDE]-(message:Message)<-[:CREATED]-(sender:User) + MATCH (user:User { id: $userId })-[:CHATS_IN]->(room:Room)<-[:INSIDE]-(message:Message)<-[:CREATED]-(sender:User) WHERE NOT sender.id = $userId AND NOT message.seen + AND NOT (user)-[:BLOCKED]->(sender) + AND NOT (user)-[:MUTED]->(sender) RETURN toString(COUNT(DISTINCT room)) AS count ` const unreadRoomsTxResponse = await transaction.run(unreadRoomsCypher, { userId }) @@ -19,7 +29,7 @@ export default { Subscription: { roomCountUpdated: { subscribe: withFilter( - () => pubsub.asyncIterator(ROOM_COUNT_UPDATED), + (_, __, context) => context.pubsub.asyncIterator(ROOM_COUNT_UPDATED), (payload, variables, context) => { return payload.userId === context.user?.id }, @@ -34,7 +44,7 @@ export default { } return neo4jgraphql(object, params, context, resolveInfo) }, - UnreadRooms: async (object, params, context, resolveInfo) => { + UnreadRooms: async (_object, _params, context, _resolveInfo) => { const { user: { id: currentUserId }, } = context diff --git a/backend/src/schema/resolvers/searches.spec.ts b/backend/src/graphql/resolvers/searches.spec.ts similarity index 98% rename from backend/src/schema/resolvers/searches.spec.ts rename to backend/src/graphql/resolvers/searches.spec.ts index f889c2ac8..8a94fbf21 100644 --- a/backend/src/schema/resolvers/searches.spec.ts +++ b/backend/src/graphql/resolvers/searches.spec.ts @@ -1,8 +1,12 @@ -import Factory, { cleanDatabase } from '../../db/factories' -import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' +import gql from 'graphql-tag' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' let query, authenticatedUser, user @@ -26,7 +30,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() neode.close() }) diff --git a/backend/src/schema/resolvers/searches.ts b/backend/src/graphql/resolvers/searches.ts similarity index 94% rename from backend/src/schema/resolvers/searches.ts rename to backend/src/graphql/resolvers/searches.ts index 5f4097c17..34fc11709 100644 --- a/backend/src/schema/resolvers/searches.ts +++ b/backend/src/graphql/resolvers/searches.ts @@ -1,4 +1,9 @@ -import log from './helpers/databaseLogger' +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { queryString } from './searches/queryString' // see http://lucene.apache.org/core/8_3_1/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package.description @@ -127,7 +132,6 @@ const getSearchResults = async (context, setup, params, resultCallback = searchR const session = context.driver.session() try { const results = await searchResultPromise(session, setup, params) - log(results) return resultCallback(results) } finally { session.close() @@ -250,6 +254,7 @@ export default { ] params.limit = 15 + // eslint-disable-next-line @typescript-eslint/no-explicit-any const type: any = multiSearchMap.find((obj) => obj.symbol === searchType) return getSearchResults(context, type.setup, params) }, diff --git a/backend/src/schema/resolvers/searches/queryString.spec.ts b/backend/src/graphql/resolvers/searches/queryString.spec.ts similarity index 100% rename from backend/src/schema/resolvers/searches/queryString.spec.ts rename to backend/src/graphql/resolvers/searches/queryString.spec.ts diff --git a/backend/src/schema/resolvers/searches/queryString.ts b/backend/src/graphql/resolvers/searches/queryString.ts similarity index 76% rename from backend/src/schema/resolvers/searches/queryString.ts rename to backend/src/graphql/resolvers/searches/queryString.ts index 8f415c5e6..da8e7bffb 100644 --- a/backend/src/schema/resolvers/searches/queryString.ts +++ b/backend/src/graphql/resolvers/searches/queryString.ts @@ -1,3 +1,9 @@ +/* eslint-disable @typescript-eslint/restrict-plus-operands */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ export function queryString(str) { const normalizedString = normalizeWhitespace(str) const escapedString = escapeSpecialCharacters(normalizedString) diff --git a/backend/src/schema/resolvers/shout.spec.ts b/backend/src/graphql/resolvers/shout.spec.ts similarity index 93% rename from backend/src/schema/resolvers/shout.spec.ts rename to backend/src/graphql/resolvers/shout.spec.ts index 294a28a76..9023284c6 100644 --- a/backend/src/schema/resolvers/shout.spec.ts +++ b/backend/src/graphql/resolvers/shout.spec.ts @@ -1,8 +1,13 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../../db/factories' import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' let mutate, query, authenticatedUser, variables const instance = getNeode() @@ -51,7 +56,7 @@ describe('shout and unshout posts', () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { diff --git a/backend/src/schema/resolvers/shout.ts b/backend/src/graphql/resolvers/shout.ts similarity index 89% rename from backend/src/schema/resolvers/shout.ts rename to backend/src/graphql/resolvers/shout.ts index 8c330cd67..f0b5885eb 100644 --- a/backend/src/schema/resolvers/shout.ts +++ b/backend/src/graphql/resolvers/shout.ts @@ -1,5 +1,7 @@ -import log from './helpers/databaseLogger' - +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ export default { Mutation: { shout: async (_object, params, context, _resolveInfo) => { @@ -21,7 +23,6 @@ export default { userId: context.user.id, }, ) - log(shoutTransactionResponse) return shoutTransactionResponse.records.map((record) => record.get('isShouted')) }) const [isShouted] = await shoutWriteTxResultPromise @@ -49,7 +50,6 @@ export default { userId: context.user.id, }, ) - log(unshoutTransactionResponse) return unshoutTransactionResponse.records.map((record) => record.get('isShouted')) }) const [isShouted] = await unshoutWriteTxResultPromise diff --git a/backend/src/schema/resolvers/socialMedia.spec.ts b/backend/src/graphql/resolvers/socialMedia.spec.ts similarity index 94% rename from backend/src/schema/resolvers/socialMedia.spec.ts rename to backend/src/graphql/resolvers/socialMedia.spec.ts index 8265e8376..168360a3b 100644 --- a/backend/src/schema/resolvers/socialMedia.spec.ts +++ b/backend/src/graphql/resolvers/socialMedia.spec.ts @@ -1,8 +1,14 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ import { createTestClient } from 'apollo-server-testing' -import createServer from '../../server' -import Factory, { cleanDatabase } from '../../db/factories' import gql from 'graphql-tag' -import { getDriver } from '../../db/neo4j' + +import Factory, { cleanDatabase } from '@db/factories' +import { getDriver } from '@db/neo4j' +import createServer from '@src/server' const driver = getDriver() @@ -12,7 +18,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('SocialMedia', () => { diff --git a/backend/src/schema/resolvers/socialMedia.ts b/backend/src/graphql/resolvers/socialMedia.ts similarity index 66% rename from backend/src/schema/resolvers/socialMedia.ts rename to backend/src/graphql/resolvers/socialMedia.ts index c5b9dcd91..2c0cd4c94 100644 --- a/backend/src/schema/resolvers/socialMedia.ts +++ b/backend/src/graphql/resolvers/socialMedia.ts @@ -1,11 +1,15 @@ -import { getNeode } from '../../db/neo4j' +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { getNeode } from '@db/neo4j' + import Resolver from './helpers/Resolver' const neode = getNeode() export default { Mutation: { - CreateSocialMedia: async (object, params, context, resolveInfo) => { + CreateSocialMedia: async (_object, params, context, _resolveInfo) => { const [user, socialMedia] = await Promise.all([ neode.find('User', context.user.id), neode.create('SocialMedia', params), @@ -15,14 +19,14 @@ export default { return response }, - UpdateSocialMedia: async (object, params, context, resolveInfo) => { + UpdateSocialMedia: async (_object, params, _context, _resolveInfo) => { const socialMedia = await neode.find('SocialMedia', params.id) await socialMedia.update({ url: params.url }) const response = await socialMedia.toJson() return response }, - DeleteSocialMedia: async (object, { id }, context, resolveInfo) => { + DeleteSocialMedia: async (_object, { id }, _context, _resolveInfo) => { const socialMedia = await neode.find('SocialMedia', id) if (!socialMedia) return null await socialMedia.delete() diff --git a/backend/src/schema/resolvers/statistics.spec.ts b/backend/src/graphql/resolvers/statistics.spec.ts similarity index 57% rename from backend/src/schema/resolvers/statistics.spec.ts rename to backend/src/graphql/resolvers/statistics.spec.ts index 15aa2d449..f67552f39 100644 --- a/backend/src/schema/resolvers/statistics.spec.ts +++ b/backend/src/graphql/resolvers/statistics.spec.ts @@ -1,48 +1,40 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { ApolloServer } from 'apollo-server-express' import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../../db/factories' -import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' +import databaseContext from '@context/database' +import Factory, { cleanDatabase } from '@db/factories' +import { statistics } from '@graphql/queries/statistics' +import createServer, { getContext } from '@src/server' + +const database = databaseContext() + +let server: ApolloServer let query, authenticatedUser -const instance = getNeode() -const driver = getDriver() -const statisticsQuery = gql` - query { - statistics { - countUsers - countPosts - countComments - countNotifications - countInvites - countFollows - countShouts - } - } -` beforeAll(async () => { await cleanDatabase() - authenticatedUser = undefined - const { server } = createServer({ - context: () => { - return { - driver, - neode: instance, - user: authenticatedUser, - } - }, - }) - query = createTestClient(server).query + // eslint-disable-next-line @typescript-eslint/require-await + const contextUser = async (_req) => authenticatedUser + const context = getContext({ user: contextUser, database }) + + server = createServer({ context }).server + + const createTestClientResult = createTestClient(server) + query = createTestClientResult.query }) afterAll(async () => { await cleanDatabase() - driver.close() + void server.stop() + void database.driver.close() + database.neode.close() }) -// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 afterEach(async () => { await cleanDatabase() }) @@ -58,8 +50,8 @@ describe('statistics', () => { }) it('returns the count of all users', async () => { - await expect(query({ query: statisticsQuery })).resolves.toMatchObject({ - data: { statistics: { countUsers: 6 } }, + await expect(query({ query: statistics })).resolves.toMatchObject({ + data: { statistics: { users: 6 } }, errors: undefined, }) }) @@ -75,8 +67,8 @@ describe('statistics', () => { }) it('returns the count of all posts', async () => { - await expect(query({ query: statisticsQuery })).resolves.toMatchObject({ - data: { statistics: { countPosts: 3 } }, + await expect(query({ query: statistics })).resolves.toMatchObject({ + data: { statistics: { posts: 3 } }, errors: undefined, }) }) @@ -92,8 +84,8 @@ describe('statistics', () => { }) it('returns the count of all comments', async () => { - await expect(query({ query: statisticsQuery })).resolves.toMatchObject({ - data: { statistics: { countComments: 2 } }, + await expect(query({ query: statistics })).resolves.toMatchObject({ + data: { statistics: { comments: 2 } }, errors: undefined, }) }) @@ -111,8 +103,8 @@ describe('statistics', () => { }) it('returns the count of all follows', async () => { - await expect(query({ query: statisticsQuery })).resolves.toMatchObject({ - data: { statistics: { countFollows: 1 } }, + await expect(query({ query: statistics })).resolves.toMatchObject({ + data: { statistics: { follows: 1 } }, errors: undefined, }) }) @@ -138,8 +130,8 @@ describe('statistics', () => { }) it('returns the count of all shouts', async () => { - await expect(query({ query: statisticsQuery })).resolves.toMatchObject({ - data: { statistics: { countShouts: 2 } }, + await expect(query({ query: statistics })).resolves.toMatchObject({ + data: { statistics: { shouts: 2 } }, errors: undefined, }) }) diff --git a/backend/src/graphql/resolvers/statistics.ts b/backend/src/graphql/resolvers/statistics.ts new file mode 100644 index 000000000..00ead1eb2 --- /dev/null +++ b/backend/src/graphql/resolvers/statistics.ts @@ -0,0 +1,83 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/dot-notation */ +import { Context } from '@src/server' + +export default { + Query: { + statistics: async (_parent, _args, context: Context) => { + const statistics = { + users: 0, + usersDeleted: 0, + posts: 0, + comments: 0, + notifications: 0, + emails: 0, + follows: 0, + shouts: 0, + invites: 0, + chatMessages: 0, + chatRooms: 0, + tags: 0, + locations: 0, + groups: 0, + inviteCodes: 0, + inviteCodesExpired: 0, + inviteCodesRedeemed: 0, + badgesRewarded: 0, + badgesDisplayed: 0, + usersVerified: 0, + reports: 0, + } + const [metaStats] = ( + await context.database.query({ + query: `CALL apoc.meta.stats() YIELD labels, relTypesCount + RETURN labels, relTypesCount`, + }) + ).records.map((record) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { ...record.get('labels'), ...record.get('relTypesCount') } + }) + + const deletedUsers = parseInt( + ( + await context.database.query({ + query: `MATCH (u:User) WHERE NOT (u)-[:PRIMARY_EMAIL]->(:EmailAddress) RETURN toString(count(u)) AS count`, + }) + ).records[0].get('count') as string, + ) + + const invalidInviteCodes = parseInt( + ( + await context.database.query({ + query: `MATCH (i:InviteCode) WHERE NOT i.expiresAt IS NULL OR i.expiresAt >= datetime() RETURN toString(count(i)) AS count`, + }) + ).records[0].get('count') as string, + ) + + statistics.users = (metaStats['User']?.toNumber() ?? 0) - deletedUsers + statistics.usersDeleted = deletedUsers + statistics.posts = metaStats['Post']?.toNumber() ?? 0 + statistics.comments = metaStats['Comment']?.toNumber() ?? 0 + statistics.notifications = metaStats['NOTIFIED']?.toNumber() ?? 0 + statistics.emails = metaStats['EmailAddress']?.toNumber() ?? 0 + statistics.follows = metaStats['FOLLOWS']?.toNumber() ?? 0 + statistics.shouts = metaStats['SHOUTED']?.toNumber() ?? 0 + statistics.invites = statistics.emails - statistics.users + statistics.chatMessages = metaStats['Message']?.toNumber() ?? 0 + statistics.chatRooms = metaStats['Room']?.toNumber() ?? 0 + statistics.tags = metaStats['Tag']?.toNumber() ?? 0 + statistics.locations = metaStats['Location']?.toNumber() ?? 0 + statistics.groups = metaStats['Group']?.toNumber() ?? 0 + statistics.inviteCodes = (metaStats['InviteCode']?.toNumber() ?? 0) - invalidInviteCodes + statistics.inviteCodesExpired = invalidInviteCodes + statistics.inviteCodesRedeemed = metaStats['REDEEMED']?.toNumber() ?? 0 + statistics.badgesRewarded = metaStats['REWARDED']?.toNumber() ?? 0 + statistics.badgesDisplayed = metaStats['SELECTED']?.toNumber() ?? 0 + statistics.usersVerified = metaStats['VERIFIES']?.toNumber() ?? 0 + statistics.reports = metaStats['Report']?.toNumber() ?? 0 + return statistics + }, + }, +} diff --git a/backend/src/schema/resolvers/userData.spec.ts b/backend/src/graphql/resolvers/userData.spec.ts similarity index 90% rename from backend/src/schema/resolvers/userData.spec.ts rename to backend/src/graphql/resolvers/userData.spec.ts index 3c521a4f1..17f1f4446 100644 --- a/backend/src/schema/resolvers/userData.spec.ts +++ b/backend/src/graphql/resolvers/userData.spec.ts @@ -1,8 +1,12 @@ -import Factory, { cleanDatabase } from '../../db/factories' -import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' +import gql from 'graphql-tag' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' let query, authenticatedUser @@ -60,7 +64,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('resolvers/userData', () => { diff --git a/backend/src/schema/resolvers/userData.ts b/backend/src/graphql/resolvers/userData.ts similarity index 83% rename from backend/src/schema/resolvers/userData.ts rename to backend/src/graphql/resolvers/userData.ts index 3cd5f1c01..15c65b59b 100644 --- a/backend/src/schema/resolvers/userData.ts +++ b/backend/src/graphql/resolvers/userData.ts @@ -1,6 +1,11 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ export default { Query: { - userData: async (object, args, context, resolveInfo) => { + userData: async (_object, _args, context, _resolveInfo) => { const id = context.user.id const cypher = ` MATCH (user:User { id: $id }) diff --git a/backend/src/schema/resolvers/user_management.spec.ts b/backend/src/graphql/resolvers/user_management.spec.ts similarity index 84% rename from backend/src/schema/resolvers/user_management.spec.ts rename to backend/src/graphql/resolvers/user_management.spec.ts index 546c7a748..1029ab2b1 100644 --- a/backend/src/schema/resolvers/user_management.spec.ts +++ b/backend/src/graphql/resolvers/user_management.spec.ts @@ -1,13 +1,22 @@ -import jwt from 'jsonwebtoken' -import CONFIG from './../../config' -import Factory, { cleanDatabase } from '../../db/factories' -import gql from 'graphql-tag' -import { loginMutation } from '../../graphql/userManagement' +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable promise/prefer-await-to-callbacks */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable jest/unbound-method */ import { createTestClient } from 'apollo-server-testing' -import createServer, { context } from '../../server' -import encode from '../../jwt/encode' -import { getNeode, getDriver } from '../../db/neo4j' -import { categories } from '../../constants/categories' +import gql from 'graphql-tag' +import jwt from 'jsonwebtoken' + +import CONFIG from '@config/index' +import { categories } from '@constants/categories' +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import { loginMutation } from '@graphql/queries/loginMutation' +import encode from '@jwt/encode' +import createServer, { context } from '@src/server' const neode = getNeode() const driver = getDriver() @@ -49,7 +58,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(() => { @@ -62,55 +71,6 @@ afterEach(async () => { await cleanDatabase() }) -describe('isLoggedIn', () => { - const isLoggedInQuery = gql` - { - isLoggedIn - } - ` - const respondsWith = async (expected) => { - await expect(query({ query: isLoggedInQuery })).resolves.toMatchObject(expected) - } - - describe('unauthenticated', () => { - it('returns false', async () => { - await respondsWith({ data: { isLoggedIn: false } }) - }) - }) - - describe('authenticated', () => { - beforeEach(async () => { - user = await Factory.build('user', { id: 'u3' }) - const userBearerToken = encode({ id: 'u3' }) - req = { headers: { authorization: `Bearer ${userBearerToken}` } } - }) - - it('returns true', async () => { - await respondsWith({ data: { isLoggedIn: true } }) - }) - - describe('but user is disabled', () => { - beforeEach(async () => { - await disable('u3') - }) - - it('returns false', async () => { - await respondsWith({ data: { isLoggedIn: false } }) - }) - }) - - describe('but user is deleted', () => { - beforeEach(async () => { - await user.update({ updatedAt: new Date().toISOString(), deleted: true }) - }) - - it('returns false', async () => { - await respondsWith({ data: { isLoggedIn: false } }) - }) - }) - }) -}) - describe('currentUser', () => { const currentUserQuery = gql` { @@ -133,8 +93,8 @@ describe('currentUser', () => { } describe('unauthenticated', () => { - it('returns null', async () => { - await respondsWith({ data: { currentUser: null } }) + it('throws "Not Authorized!"', async () => { + await respondsWith({ errors: [{ message: 'Not Authorized!' }] }) }) }) @@ -198,10 +158,32 @@ describe('currentUser', () => { ) }) - it('returns empty array for all categories', async () => { + it('returns all categories by default', async () => { await respondsWith({ data: { - currentUser: expect.objectContaining({ activeCategories: [] }), + currentUser: expect.objectContaining({ + activeCategories: expect.arrayContaining([ + 'cat1', + 'cat2', + 'cat3', + 'cat4', + 'cat5', + 'cat6', + 'cat7', + 'cat8', + 'cat9', + 'cat10', + 'cat11', + 'cat12', + 'cat13', + 'cat14', + 'cat15', + 'cat16', + 'cat17', + 'cat18', + 'cat19', + ]), + }), }, }) }) @@ -292,7 +274,11 @@ describe('login', () => { describe('normalization', () => { describe('email address is a gmail address ', () => { beforeEach(async () => { - const email = await neode.first('EmailAddress', { email: 'test@example.org' }) + const email = await neode.first( + 'EmailAddress', + { email: 'test@example.org' }, + undefined, + ) await email.update({ email: 'someuser@gmail.com' }) }) diff --git a/backend/src/graphql/resolvers/user_management.ts b/backend/src/graphql/resolvers/user_management.ts new file mode 100644 index 000000000..140a8d53c --- /dev/null +++ b/backend/src/graphql/resolvers/user_management.ts @@ -0,0 +1,79 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import { AuthenticationError } from 'apollo-server' +import bcrypt from 'bcryptjs' +import { neo4jgraphql } from 'neo4j-graphql-js' + +import { getNeode } from '@db/neo4j' +import encode from '@jwt/encode' + +import normalizeEmail from './helpers/normalizeEmail' + +const neode = getNeode() + +export default { + Query: { + currentUser: async (object, params, context, resolveInfo) => + neo4jgraphql(object, { id: context.user.id }, context, resolveInfo), + }, + Mutation: { + login: async (_, { email, password }, { driver }) => { + // if (user && user.id) { + // throw new Error('Already logged in.') + // } + email = normalizeEmail(email) + const session = driver.session() + try { + const loginReadTxResultPromise = session.readTransaction(async (transaction) => { + const loginTransactionResponse = await transaction.run( + ` + MATCH (user:User {deleted: false})-[:PRIMARY_EMAIL]->(e:EmailAddress {email: $userEmail}) + RETURN user {.id, .slug, .name, .encryptedPassword, .role, .disabled, email:e.email} as user LIMIT 1 + `, + { userEmail: email }, + ) + return loginTransactionResponse.records.map((record) => record.get('user')) + }) + const [currentUser] = await loginReadTxResultPromise + if ( + currentUser && + (await bcrypt.compare(password, currentUser.encryptedPassword)) && + !currentUser.disabled + ) { + delete currentUser.encryptedPassword + return encode(currentUser) + } else if (currentUser?.disabled) { + throw new AuthenticationError('Your account has been disabled.') + } else { + throw new AuthenticationError('Incorrect email address or password.') + } + } finally { + session.close() + } + }, + changePassword: async (_, { oldPassword, newPassword }, { user }) => { + const currentUser = await neode.find('User', user.id) + + const encryptedPassword = currentUser.get('encryptedPassword') + if (!(await bcrypt.compare(oldPassword, encryptedPassword))) { + throw new AuthenticationError('Old password is not correct') + } + + if (await bcrypt.compare(newPassword, encryptedPassword)) { + throw new AuthenticationError('Old password and new password should be different') + } + + const newEncryptedPassword = await bcrypt.hash(newPassword, 10) + await currentUser.update({ + encryptedPassword: newEncryptedPassword, + updatedAt: new Date().toISOString(), + }) + + return encode(await currentUser.toJson()) + }, + }, +} diff --git a/backend/src/graphql/resolvers/users.spec.ts b/backend/src/graphql/resolvers/users.spec.ts new file mode 100644 index 000000000..2576c1f15 --- /dev/null +++ b/backend/src/graphql/resolvers/users.spec.ts @@ -0,0 +1,1599 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +import { ApolloServer } from 'apollo-server-express' +import { createTestClient } from 'apollo-server-testing' +import gql from 'graphql-tag' + +import { categories } from '@constants/categories' +import databaseContext from '@context/database' +import pubsubContext from '@context/pubsub' +import Factory, { cleanDatabase } from '@db/factories' +import User from '@db/models/User' +import { setTrophyBadgeSelected } from '@graphql/queries/setTrophyBadgeSelected' +import createServer, { getContext } from '@src/server' + +const categoryIds = ['cat9'] +let user +let admin +let authenticatedUser + +let query +let mutate +let variables + +const pubsub = pubsubContext() + +const deleteUserMutation = gql` + mutation ($id: ID!, $resource: [Deletable]) { + DeleteUser(id: $id, resource: $resource) { + id + name + about + deleted + contributions { + id + content + contentExcerpt + deleted + comments { + id + content + contentExcerpt + deleted + } + } + comments { + id + content + contentExcerpt + deleted + } + } + } +` +const switchUserRoleMutation = gql` + mutation ($role: UserRole!, $id: ID!) { + switchUserRole(role: $role, id: $id) { + name + role + id + updatedAt + email + } + } +` + +const saveCategorySettings = gql` + mutation ($activeCategories: [String]) { + saveCategorySettings(activeCategories: $activeCategories) + } +` + +const updateOnlineStatus = gql` + mutation ($status: OnlineStatus!) { + updateOnlineStatus(status: $status) + } +` + +const resetTrophyBadgesSelected = gql` + mutation { + resetTrophyBadgesSelected { + badgeTrophiesCount + badgeTrophiesSelected { + id + isDefault + } + badgeTrophiesUnused { + id + } + badgeTrophiesUnusedCount + } + } +` + +const database = databaseContext() + +let server: ApolloServer + +beforeAll(async () => { + await cleanDatabase() + + const contextUser = async (_req) => authenticatedUser + const context = getContext({ user: contextUser, database, pubsub }) + + server = createServer({ context }).server + + const createTestClientResult = createTestClient(server) + query = createTestClientResult.query + mutate = createTestClientResult.mutate +}) + +afterAll(async () => { + await cleanDatabase() + void server.stop() + void database.driver.close() + database.neode.close() +}) + +// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 +afterEach(async () => { + await cleanDatabase() +}) + +describe('User', () => { + describe('query by email address', () => { + let userQuery + + beforeEach(async () => { + userQuery = gql` + query ($email: String) { + User(email: $email) { + name + } + } + ` + variables = { + email: 'any-email-address@example.org', + } + await Factory.build('user', { name: 'Johnny' }, { email: 'any-email-address@example.org' }) + }) + + it('is forbidden', async () => { + await expect(query({ query: userQuery, variables })).resolves.toMatchObject({ + errors: [{ message: 'Not Authorized!' }], + }) + }) + + describe('as admin', () => { + beforeEach(async () => { + const admin = await Factory.build( + 'user', + { + role: 'admin', + }, + { + email: 'admin@example.org', + password: '1234', + }, + ) + authenticatedUser = await admin.toJson() + }) + + it('is permitted', async () => { + await expect(query({ query: userQuery, variables })).resolves.toMatchObject({ + data: { User: [{ name: 'Johnny' }] }, + errors: undefined, + }) + }) + + it('non-existing email address, issue #2294', async () => { + // see: https://github.com/Human-Connection/Human-Connection/issues/2294 + await expect( + query({ + query: userQuery, + variables: { + email: 'this-email-does-not-exist@example.org', + }, + }), + ).resolves.toMatchObject({ + data: { User: [] }, + errors: undefined, + }) + }) + }) + }) +}) + +describe('UpdateUser', () => { + let updateUserMutation + + beforeEach(async () => { + updateUserMutation = gql` + mutation ( + $id: ID! + $name: String + $termsAndConditionsAgreedVersion: String + $locationName: String # empty string '' sets it to null + ) { + UpdateUser( + id: $id + name: $name + termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion + locationName: $locationName + ) { + id + name + termsAndConditionsAgreedVersion + termsAndConditionsAgreedAt + locationName + location { + name + nameDE + nameEN + } + } + } + ` + variables = { + id: 'u47', + name: 'John Doughnut', + } + + user = await Factory.build( + 'user', + { + id: 'u47', + name: 'John Doe', + termsAndConditionsAgreedVersion: null, + termsAndConditionsAgreedAt: null, + allowEmbedIframes: false, + }, + { + email: 'user@example.org', + }, + ) + }) + + describe('as another user', () => { + beforeEach(async () => { + const someoneElse = await Factory.build( + 'user', + { + name: 'James Doe', + }, + { + email: 'someone-else@example.org', + }, + ) + + authenticatedUser = await someoneElse.toJson() + }) + + it('is not allowed to change other user accounts', async () => { + const { errors } = await mutate({ mutation: updateUserMutation, variables }) + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') + }) + }) + + describe('as the same user', () => { + beforeEach(async () => { + authenticatedUser = await user.toJson() + }) + + it('updates the name', async () => { + const expected = { + data: { + UpdateUser: { + id: 'u47', + name: 'John Doughnut', + }, + }, + errors: undefined, + } + await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject( + expected, + ) + }) + + describe('given a new agreed version of terms and conditions', () => { + beforeEach(async () => { + variables = { ...variables, termsAndConditionsAgreedVersion: '0.0.2' } + }) + it('update termsAndConditionsAgreedVersion', async () => { + const expected = { + data: { + UpdateUser: expect.objectContaining({ + termsAndConditionsAgreedVersion: '0.0.2', + termsAndConditionsAgreedAt: expect.any(String), + }), + }, + errors: undefined, + } + + await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject( + expected, + ) + }) + }) + + describe('given any attribute other than termsAndConditionsAgreedVersion', () => { + beforeEach(async () => { + variables = { ...variables, name: 'any name' } + }) + it('update termsAndConditionsAgreedVersion', async () => { + const expected = { + data: { + UpdateUser: expect.objectContaining({ + termsAndConditionsAgreedVersion: null, + termsAndConditionsAgreedAt: null, + }), + }, + errors: undefined, + } + + await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject( + expected, + ) + }) + }) + + it('rejects if version of terms and conditions has wrong format', async () => { + variables = { + ...variables, + termsAndConditionsAgreedVersion: 'invalid version format', + } + const { errors } = await mutate({ mutation: updateUserMutation, variables }) + expect(errors[0]).toHaveProperty('message', 'Invalid version format!') + }) + + describe('supports updating location', () => { + describe('change location to "Hamburg, New Jersey, United States"', () => { + it('has updated location to "Hamburg, New Jersey, United States"', async () => { + variables = { ...variables, locationName: 'Hamburg, New Jersey, United States' } + await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject({ + data: { + UpdateUser: { + locationName: 'Hamburg, New Jersey, United States', + location: expect.objectContaining({ + name: 'Hamburg', + nameDE: 'Hamburg', + nameEN: 'Hamburg', + }), + }, + }, + errors: undefined, + }) + }) + }) + + describe('change location to unset location', () => { + it('has updated location to unset location', async () => { + variables = { ...variables, locationName: '' } + await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject({ + data: { + UpdateUser: { + locationName: null, + location: null, + }, + }, + errors: undefined, + }) + }) + }) + }) + }) +}) + +describe('Delete a User as admin', () => { + beforeEach(async () => { + variables = { id: ' u343', resource: [] } + + user = await Factory.build('user', { + name: 'My name should be deleted', + about: 'along with my about', + id: 'u343', + }) + }) + + describe('authenticated as Admin', () => { + beforeEach(async () => { + admin = await Factory.build( + 'user', + { + role: 'admin', + }, + { + email: 'admin@example.org', + password: '1234', + }, + ) + authenticatedUser = await admin.toJson() + }) + + describe('deleting a user account', () => { + beforeEach(() => { + variables = { ...variables, id: 'u343' } + }) + + describe('given posts and comments', () => { + beforeEach(async () => { + await Factory.build('category', { + id: 'cat9', + name: 'Democracy & Politics', + icon: 'university', + }) + await Factory.build( + 'post', + { + id: 'p139', + content: 'Post by user u343', + }, + { + author: user, + categoryIds, + }, + ) + await Factory.build( + 'comment', + { + id: 'c155', + content: 'Comment by user u343', + }, + { + author: user, + }, + ) + await Factory.build( + 'comment', + { + id: 'c156', + content: "A comment by someone else on user u343's post", + }, + { + postId: 'p139', + }, + ) + }) + + it("deletes account, but doesn't delete posts or comments by default", async () => { + const expectedResponse = { + data: { + DeleteUser: { + id: 'u343', + name: 'UNAVAILABLE', + about: 'UNAVAILABLE', + deleted: true, + contributions: [ + { + id: 'p139', + content: 'Post by user u343', + contentExcerpt: 'Post by user u343', + deleted: false, + comments: [ + { + id: 'c156', + content: "A comment by someone else on user u343's post", + contentExcerpt: "A comment by someone else on user u343's post", + deleted: false, + }, + ], + }, + ], + comments: [ + { + id: 'c155', + content: 'Comment by user u343', + contentExcerpt: 'Comment by user u343', + deleted: false, + }, + ], + }, + }, + errors: undefined, + } + await expect(mutate({ mutation: deleteUserMutation, variables })).resolves.toMatchObject( + expectedResponse, + ) + }) + + describe('deletion of all posts and comments requested', () => { + beforeEach(() => { + variables = { ...variables, resource: ['Comment', 'Post'] } + }) + + it('marks posts and comments as deleted', async () => { + const expectedResponse = { + data: { + DeleteUser: { + id: 'u343', + name: 'UNAVAILABLE', + about: 'UNAVAILABLE', + deleted: true, + contributions: [ + { + id: 'p139', + content: 'UNAVAILABLE', + contentExcerpt: 'UNAVAILABLE', + deleted: true, + comments: [ + { + id: 'c156', + content: 'UNAVAILABLE', + contentExcerpt: 'UNAVAILABLE', + deleted: true, + }, + ], + }, + ], + comments: [ + { + id: 'c155', + content: 'UNAVAILABLE', + contentExcerpt: 'UNAVAILABLE', + deleted: true, + }, + ], + }, + }, + errors: undefined, + } + await expect( + mutate({ mutation: deleteUserMutation, variables }), + ).resolves.toMatchObject(expectedResponse) + }) + }) + }) + + describe('connected `EmailAddress` nodes', () => { + it('will be removed completely', async () => { + await expect(database.neode.all('EmailAddress')).resolves.toHaveLength(2) + await mutate({ mutation: deleteUserMutation, variables }) + + await expect(database.neode.all('EmailAddress')).resolves.toHaveLength(1) + }) + }) + + describe('connected `SocialMedia` nodes', () => { + beforeEach(async () => { + const socialMedia = await Factory.build('socialMedia') + await socialMedia.relateTo(user, 'ownedBy') + }) + + it('will be removed completely', async () => { + await expect(database.neode.all('SocialMedia')).resolves.toHaveLength(1) + await mutate({ mutation: deleteUserMutation, variables }) + await expect(database.neode.all('SocialMedia')).resolves.toHaveLength(0) + }) + }) + }) + }) +}) + +describe('switch user role', () => { + beforeEach(async () => { + user = await Factory.build('user', { + id: 'user', + role: 'user', + }) + admin = await Factory.build('user', { + role: 'admin', + id: 'admin', + }) + }) + + describe('as simple user', () => { + it('cannot change the role', async () => { + authenticatedUser = await user.toJson() + variables = { + id: 'user', + role: 'admin', + } + await expect(mutate({ mutation: switchUserRoleMutation, variables })).resolves.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ], + }), + ) + }) + }) + + describe('as admin', () => { + it('changes the role of other user', async () => { + authenticatedUser = await admin.toJson() + variables = { + id: 'user', + role: 'moderator', + } + await expect(mutate({ mutation: switchUserRoleMutation, variables })).resolves.toEqual( + expect.objectContaining({ + data: { + switchUserRole: expect.objectContaining({ + role: 'moderator', + }), + }, + }), + ) + }) + + it('cannot change own role', async () => { + authenticatedUser = await admin.toJson() + variables = { + id: 'admin', + role: 'moderator', + } + await expect(mutate({ mutation: switchUserRoleMutation, variables })).resolves.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'you-cannot-change-your-own-role', + }), + ], + }), + ) + }) + }) +}) + +let anotherUser +const emailNotificationSettingsQuery = gql` + query ($id: ID!) { + User(id: $id) { + emailNotificationSettings { + type + settings { + name + value + } + } + } + } +` + +const emailNotificationSettingsMutation = gql` + mutation ($id: ID!, $emailNotificationSettings: [EmailNotificationSettingsInput]!) { + UpdateUser(id: $id, emailNotificationSettings: $emailNotificationSettings) { + emailNotificationSettings { + type + settings { + name + value + } + } + } + } +` + +describe('emailNotificationSettings', () => { + beforeEach(async () => { + user = await Factory.build('user', { + id: 'user', + role: 'user', + }) + anotherUser = await Factory.build('user', { + id: 'anotherUser', + role: 'anotherUser', + }) + }) + + describe('query the field', () => { + describe('as another user', () => { + it('throws an error', async () => { + authenticatedUser = await anotherUser.toJson() + const targetUser = await user.toJson() + await expect( + query({ query: emailNotificationSettingsQuery, variables: { id: targetUser.id } }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ], + }), + ) + }) + }) + + describe('as self', () => { + it('returns the emailNotificationSettings', async () => { + authenticatedUser = await user.toJson() + await expect( + query({ query: emailNotificationSettingsQuery, variables: { id: authenticatedUser.id } }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + User: [ + { + emailNotificationSettings: [ + { + type: 'post', + settings: [ + { + name: 'commentOnObservedPost', + value: true, + }, + { + name: 'mention', + value: true, + }, + { + name: 'followingUsers', + value: true, + }, + { + name: 'postInGroup', + value: true, + }, + ], + }, + { + type: 'chat', + settings: [ + { + name: 'chatMessage', + value: true, + }, + ], + }, + { + type: 'group', + settings: [ + { + name: 'groupMemberJoined', + value: true, + }, + { + name: 'groupMemberLeft', + value: true, + }, + { + name: 'groupMemberRemoved', + value: true, + }, + { + name: 'groupMemberRoleChanged', + value: true, + }, + ], + }, + ], + }, + ], + }, + }), + ) + }) + }) + }) + + describe('mutate the field', () => { + const emailNotificationSettings = [{ name: 'mention', value: false }] + + describe('as another user', () => { + it('throws an error', async () => { + authenticatedUser = await anotherUser.toJson() + const targetUser = await user.toJson() + await expect( + mutate({ + mutation: emailNotificationSettingsMutation, + variables: { id: targetUser.id, emailNotificationSettings }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ], + }), + ) + }) + }) + + describe('as self', () => { + it('updates the emailNotificationSettings', async () => { + authenticatedUser = await user.toJson() + await expect( + mutate({ + mutation: emailNotificationSettingsMutation, + variables: { id: authenticatedUser.id, emailNotificationSettings }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + UpdateUser: { + emailNotificationSettings: [ + { + type: 'post', + settings: [ + { + name: 'commentOnObservedPost', + value: true, + }, + { + name: 'mention', + value: false, + }, + { + name: 'followingUsers', + value: true, + }, + { + name: 'postInGroup', + value: true, + }, + ], + }, + { + type: 'chat', + settings: [ + { + name: 'chatMessage', + value: true, + }, + ], + }, + { + type: 'group', + settings: [ + { + name: 'groupMemberJoined', + value: true, + }, + { + name: 'groupMemberLeft', + value: true, + }, + { + name: 'groupMemberRemoved', + value: true, + }, + { + name: 'groupMemberRoleChanged', + value: true, + }, + ], + }, + ], + }, + }, + }), + ) + }) + }) + }) +}) + +describe('save category settings', () => { + beforeEach(async () => { + await Promise.all( + categories.map(({ icon, name }, index) => { + return Factory.build('category', { + id: `cat${index + 1}`, + slug: name, + name, + icon, + }) + }), + ) + }) + + beforeEach(async () => { + user = await Factory.build('user', { + id: 'user', + role: 'user', + }) + variables = { + activeCategories: ['cat1', 'cat3', 'cat5'], + } + }) + + describe('not authenticated', () => { + beforeEach(async () => { + authenticatedUser = undefined + }) + + it('throws an error', async () => { + await expect(mutate({ mutation: saveCategorySettings, variables })).resolves.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ], + }), + ) + }) + }) + + describe('authenticated', () => { + beforeEach(async () => { + authenticatedUser = await user.toJson() + }) + + const userQuery = gql` + query ($id: ID) { + User(id: $id) { + activeCategories + } + } + ` + + describe('no categories saved', () => { + it('returns true for active categories mutation', async () => { + await expect(mutate({ mutation: saveCategorySettings, variables })).resolves.toEqual( + expect.objectContaining({ + data: { saveCategorySettings: true }, + }), + ) + }) + + describe('query for user', () => { + beforeEach(async () => { + await mutate({ mutation: saveCategorySettings, variables }) + }) + + it('returns the active categories when user is queried', async () => { + await expect( + query({ query: userQuery, variables: { id: authenticatedUser.id } }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + User: [ + { + activeCategories: expect.arrayContaining(['cat1', 'cat3', 'cat5']), + }, + ], + }, + }), + ) + }) + }) + }) + + describe('categories already saved', () => { + beforeEach(async () => { + variables = { + activeCategories: ['cat1', 'cat3', 'cat5'], + } + await mutate({ mutation: saveCategorySettings, variables }) + variables = { + activeCategories: ['cat10', 'cat11', 'cat12', 'cat8', 'cat9'], + } + }) + + it('returns true', async () => { + await expect(mutate({ mutation: saveCategorySettings, variables })).resolves.toEqual( + expect.objectContaining({ + data: { saveCategorySettings: true }, + }), + ) + }) + + describe('query for user', () => { + beforeEach(async () => { + await mutate({ mutation: saveCategorySettings, variables }) + }) + + it('returns the new active categories when user is queried', async () => { + await expect( + query({ query: userQuery, variables: { id: authenticatedUser.id } }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + User: [ + { + activeCategories: expect.arrayContaining([ + 'cat10', + 'cat11', + 'cat12', + 'cat8', + 'cat9', + ]), + }, + ], + }, + }), + ) + }) + }) + }) + }) +}) + +describe('updateOnlineStatus', () => { + beforeEach(async () => { + user = await Factory.build('user', { + id: 'user', + role: 'user', + }) + variables = { + status: 'online', + } + }) + + describe('not authenticated', () => { + beforeEach(async () => { + authenticatedUser = undefined + }) + + it('throws an error', async () => { + await expect(mutate({ mutation: updateOnlineStatus, variables })).resolves.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ], + }), + ) + }) + }) + + describe('authenticated', () => { + beforeEach(async () => { + authenticatedUser = await user.toJson() + }) + + describe('set online', () => { + it('returns true and saves the user in the database as online', async () => { + await expect(mutate({ mutation: updateOnlineStatus, variables })).resolves.toEqual( + expect.objectContaining({ + data: { updateOnlineStatus: true }, + }), + ) + + const cypher = 'MATCH (u:User {id: $id}) RETURN u' + const result = await database.neode.cypher(cypher, { id: authenticatedUser.id }) + const dbUser = database.neode.hydrateFirst(result, 'u', database.neode.model('User')) + await expect(dbUser.toJson()).resolves.toMatchObject({ + lastOnlineStatus: 'online', + }) + await expect(dbUser.toJson()).resolves.not.toMatchObject({ + awaySince: expect.any(String), + }) + }) + }) + + describe('set away', () => { + beforeEach(() => { + variables = { + status: 'away', + } + }) + + it('returns true and saves the user in the database as away', async () => { + await expect(mutate({ mutation: updateOnlineStatus, variables })).resolves.toEqual( + expect.objectContaining({ + data: { updateOnlineStatus: true }, + }), + ) + + const cypher = 'MATCH (u:User {id: $id}) RETURN u' + const result = await database.neode.cypher(cypher, { id: authenticatedUser.id }) + const dbUser = database.neode.hydrateFirst(result, 'u', database.neode.model('User')) + await expect(dbUser.toJson()).resolves.toMatchObject({ + lastOnlineStatus: 'away', + awaySince: expect.any(String), + }) + }) + + it('stores the timestamp of the first away call', async () => { + await expect(mutate({ mutation: updateOnlineStatus, variables })).resolves.toEqual( + expect.objectContaining({ + data: { updateOnlineStatus: true }, + }), + ) + + const cypher = 'MATCH (u:User {id: $id}) RETURN u' + const result = await database.neode.cypher(cypher, { id: authenticatedUser.id }) + const dbUser = database.neode.hydrateFirst( + result, + 'u', + database.neode.model('User'), + ) + await expect(dbUser.toJson()).resolves.toMatchObject({ + lastOnlineStatus: 'away', + awaySince: expect.any(String), + }) + + const awaySince = (await dbUser.toJson()).awaySince + + await expect(mutate({ mutation: updateOnlineStatus, variables })).resolves.toEqual( + expect.objectContaining({ + data: { updateOnlineStatus: true }, + }), + ) + + const result2 = await database.neode.cypher(cypher, { id: authenticatedUser.id }) + const dbUser2 = database.neode.hydrateFirst(result2, 'u', database.neode.model('User')) + await expect(dbUser2.toJson()).resolves.toMatchObject({ + lastOnlineStatus: 'away', + awaySince, + }) + }) + }) + }) +}) + +describe('setTrophyBadgeSelected', () => { + beforeEach(async () => { + user = await Factory.build('user', { + id: 'user', + role: 'user', + }) + const badgeBear = await Factory.build('badge', { + id: 'trophy_bear', + type: 'trophy', + description: 'You earned a Bear', + icon: '/img/badges/trophy_blue_bear.svg', + }) + const badgePanda = await Factory.build('badge', { + id: 'trophy_panda', + type: 'trophy', + description: 'You earned a Panda', + icon: '/img/badges/trophy_blue_panda.svg', + }) + await Factory.build('badge', { + id: 'trophy_rabbit', + type: 'trophy', + description: 'You earned a Rabbit', + icon: '/img/badges/trophy_blue_rabbit.svg', + }) + + await user.relateTo(badgeBear, 'rewarded') + await user.relateTo(badgePanda, 'rewarded') + }) + + describe('not authenticated', () => { + beforeEach(async () => { + authenticatedUser = undefined + }) + + it('throws an error', async () => { + await expect( + mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 0, badgeId: 'trophy_bear' }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ], + }), + ) + }) + }) + + describe('authenticated', () => { + beforeEach(async () => { + authenticatedUser = await user.toJson() + }) + + it('throws Error when slot is out of bound', async () => { + await expect( + mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: -1, badgeId: 'trophy_bear' }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'Invalid slot! There is only 9 badge-slots to fill', + }), + ], + }), + ) + await expect( + mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 9, badgeId: 'trophy_bear' }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'Invalid slot! There is only 9 badge-slots to fill', + }), + ], + }), + ) + }) + + it('throws Error when badge was not rewarded to user', async () => { + await expect( + mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 0, badgeId: 'trophy_rabbit' }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'Error: You cannot set badges not rewarded to you.', + }), + ], + }), + ) + }) + + it('throws Error when badge is unknown', async () => { + await expect( + mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 0, badgeId: 'trophy_unknown' }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'Error: You cannot set badges not rewarded to you.', + }), + ], + }), + ) + }) + + it('returns the user with badges set on slots', async () => { + await expect( + mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 0, badgeId: 'trophy_bear' }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + setTrophyBadgeSelected: { + badgeTrophiesCount: 2, + badgeTrophiesSelected: [ + { + id: 'trophy_bear', + isDefault: false, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + ], + badgeTrophiesUnused: [ + { + id: 'trophy_panda', + }, + ], + badgeTrophiesUnusedCount: 1, + }, + }, + }), + ) + await expect( + mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 5, badgeId: 'trophy_panda' }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + setTrophyBadgeSelected: { + badgeTrophiesCount: 2, + badgeTrophiesSelected: [ + { + id: 'trophy_bear', + isDefault: false, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'trophy_panda', + isDefault: false, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + ], + badgeTrophiesUnused: [], + badgeTrophiesUnusedCount: 0, + }, + }, + }), + ) + }) + + describe('set badge to null or default', () => { + beforeEach(async () => { + await mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 0, badgeId: 'trophy_bear' }, + }) + await mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 5, badgeId: 'trophy_panda' }, + }) + }) + + it('returns the user with no badge set on the selected slot when sending null', async () => { + await expect( + mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 5, badgeId: null }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + setTrophyBadgeSelected: { + badgeTrophiesCount: 2, + badgeTrophiesSelected: [ + { + id: 'trophy_bear', + isDefault: false, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + ], + badgeTrophiesUnused: [ + { + id: 'trophy_panda', + }, + ], + badgeTrophiesUnusedCount: 1, + }, + }, + }), + ) + }) + + it('returns the user with no badge set on the selected slot when sending default_trophy', async () => { + await expect( + mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 5, badgeId: 'default_trophy' }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + setTrophyBadgeSelected: { + badgeTrophiesCount: 2, + badgeTrophiesSelected: [ + { + id: 'trophy_bear', + isDefault: false, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + ], + badgeTrophiesUnused: [ + { + id: 'trophy_panda', + }, + ], + badgeTrophiesUnusedCount: 1, + }, + }, + }), + ) + }) + }) + }) +}) + +describe('resetTrophyBadgesSelected', () => { + beforeEach(async () => { + user = await Factory.build('user', { + id: 'user', + role: 'user', + }) + const badgeBear = await Factory.build('badge', { + id: 'trophy_bear', + type: 'trophy', + description: 'You earned a Bear', + icon: '/img/badges/trophy_blue_bear.svg', + }) + const badgePanda = await Factory.build('badge', { + id: 'trophy_panda', + type: 'trophy', + description: 'You earned a Panda', + icon: '/img/badges/trophy_blue_panda.svg', + }) + await Factory.build('badge', { + id: 'trophy_rabbit', + type: 'trophy', + description: 'You earned a Rabbit', + icon: '/img/badges/trophy_blue_rabbit.svg', + }) + + await user.relateTo(badgeBear, 'rewarded') + await user.relateTo(badgePanda, 'rewarded') + + await mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 0, badgeId: 'trophy_bear' }, + }) + await mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 5, badgeId: 'trophy_panda' }, + }) + }) + + describe('not authenticated', () => { + beforeEach(async () => { + authenticatedUser = undefined + }) + + it('throws an error', async () => { + await expect(mutate({ mutation: resetTrophyBadgesSelected })).resolves.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ], + }), + ) + }) + }) + + describe('authenticated', () => { + beforeEach(async () => { + authenticatedUser = await user.toJson() + }) + + it('returns the user with no profile badges badges set', async () => { + await expect(mutate({ mutation: resetTrophyBadgesSelected })).resolves.toEqual( + expect.objectContaining({ + data: { + resetTrophyBadgesSelected: { + badgeTrophiesCount: 2, + badgeTrophiesSelected: [ + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + ], + badgeTrophiesUnused: expect.arrayContaining([ + { + id: 'trophy_panda', + }, + { + id: 'trophy_bear', + }, + ]), + badgeTrophiesUnusedCount: 2, + }, + }, + }), + ) + }) + }) +}) diff --git a/backend/src/schema/resolvers/users.ts b/backend/src/graphql/resolvers/users.ts similarity index 51% rename from backend/src/schema/resolvers/users.ts rename to backend/src/graphql/resolvers/users.ts index cab0bc8a3..9418ef3e6 100644 --- a/backend/src/schema/resolvers/users.ts +++ b/backend/src/graphql/resolvers/users.ts @@ -1,9 +1,21 @@ -import { neo4jgraphql } from 'neo4j-graphql-js' -import { getNeode } from '../../db/neo4j' +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { UserInputError, ForbiddenError } from 'apollo-server' -import { mergeImage, deleteImage } from './images/images' +import { neo4jgraphql } from 'neo4j-graphql-js' + +import { TROPHY_BADGES_SELECTED_MAX } from '@constants/badges' +import { getNeode } from '@db/neo4j' +import { Context } from '@src/server' + +import { defaultTrophyBadge, defaultVerificationBadge } from './badges' +import normalizeEmail from './helpers/normalizeEmail' import Resolver from './helpers/Resolver' -import log from './helpers/databaseLogger' +import { images } from './images/images' import { createOrUpdateLocations } from './users/location' const neode = getNeode() @@ -40,14 +52,14 @@ export const getBlockedUsers = async (context) => { export default { Query: { - mutedUsers: async (object, args, context, resolveInfo) => { + mutedUsers: async (_object, _args, context, _resolveInfo) => { try { return getMutedUsers(context) } catch (e) { throw new UserInputError(e.message) } }, - blockedUsers: async (object, args, context, resolveInfo) => { + blockedUsers: async (_object, _args, context, _resolveInfo) => { try { return getBlockedUsers(context) } catch (e) { @@ -55,8 +67,8 @@ export default { } }, User: async (object, args, context, resolveInfo) => { - const { email } = args - if (email) { + if (args.email) { + args.email = normalizeEmail(args.email) let session try { session = context.driver.session() @@ -64,12 +76,12 @@ export default { const result = txc.run( ` MATCH (user:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email: $args.email}) - RETURN user`, + RETURN user {.*, email: e.email}`, { args }, ) return result }) - return readTxResult.records.map((r) => r.get('user').properties) + return readTxResult.records.map((r) => r.get('user')) } finally { session.close() } @@ -108,35 +120,56 @@ export default { const unmutedUser = await neode.find('User', params.id) return unmutedUser.toJson() }, - blockUser: async (object, args, context, resolveInfo) => { + blockUser: async (_object, args, context, _resolveInfo) => { const { user: currentUser } = context if (currentUser.id === args.id) return null - await neode.cypher( - ` - MATCH(u:User {id: $currentUser.id})-[r:FOLLOWS]->(b:User {id: $args.id}) - DELETE r - `, - { currentUser, args }, - ) - const [user, blockedUser] = await Promise.all([ - neode.find('User', currentUser.id), - neode.find('User', args.id), - ]) - await user.relateTo(blockedUser, 'blocked') - return blockedUser.toJson() + + const session = context.driver.session() + const writeTxResultPromise = session.writeTransaction(async (transaction) => { + const unBlockUserTransactionResponse = await transaction.run( + ` + MATCH (blockedUser:User {id: $args.id}) + MATCH (currentUser:User {id: $currentUser.id}) + OPTIONAL MATCH (currentUser)-[r:FOLLOWS]->(blockedUser) + DELETE r + CREATE (currentUser)-[:BLOCKED]->(blockedUser) + RETURN blockedUser {.*} + `, + { currentUser, args }, + ) + return unBlockUserTransactionResponse.records.map((record) => record.get('blockedUser'))[0] + }) + try { + return await writeTxResultPromise + } catch (error) { + throw new UserInputError(error.message) + } finally { + session.close() + } }, - unblockUser: async (object, args, context, resolveInfo) => { + unblockUser: async (_object, args, context, _resolveInfo) => { const { user: currentUser } = context if (currentUser.id === args.id) return null - await neode.cypher( - ` - MATCH(u:User {id: $currentUser.id})-[r:BLOCKED]->(b:User {id: $args.id}) - DELETE r - `, - { currentUser, args }, - ) - const blockedUser = await neode.find('User', args.id) - return blockedUser.toJson() + + const session = context.driver.session() + const writeTxResultPromise = session.writeTransaction(async (transaction) => { + const unBlockUserTransactionResponse = await transaction.run( + ` + MATCH(u:User {id: $currentUser.id})-[r:BLOCKED]->(blockedUser:User {id: $args.id}) + DELETE r + RETURN blockedUser {.*} + `, + { currentUser, args }, + ) + return unBlockUserTransactionResponse.records.map((record) => record.get('blockedUser'))[0] + }) + try { + return await writeTxResultPromise + } catch (error) { + throw new UserInputError(error.message) + } finally { + session.close() + } }, UpdateUser: async (_parent, params, context, _resolveInfo) => { const { avatar: avatarInput } = params @@ -150,6 +183,19 @@ export default { } params.termsAndConditionsAgreedAt = new Date().toISOString() } + + const { + emailNotificationSettings, + }: { emailNotificationSettings: { name: string; value: boolean }[] | undefined } = params + delete params.emailNotificationSettings + if (emailNotificationSettings) { + emailNotificationSettings.forEach((setting) => { + params[ + 'emailNotifications' + setting.name.charAt(0).toUpperCase() + setting.name.slice(1) + ] = setting.value + }) + } + const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { @@ -164,7 +210,7 @@ export default { ) const [user] = updateUserTransactionResponse.records.map((record) => record.get('user')) if (avatarInput) { - await mergeImage(user, 'AVATAR_IMAGE', avatarInput, { transaction }) + await images.mergeImage(user, 'AVATAR_IMAGE', avatarInput, { transaction }) } return user }) @@ -179,12 +225,12 @@ export default { session.close() } }, - DeleteUser: async (object, params, context, resolveInfo) => { + DeleteUser: async (_object, params, context, _resolveInfo) => { const { resource, id: userId } = params const session = context.driver.session() const deleteUserTxResultPromise = session.writeTransaction(async (transaction) => { - if (resource && resource.length) { + if (resource?.length) { await Promise.all( resource.map(async (node) => { const txResult = await transaction.run( @@ -207,7 +253,7 @@ export default { return Promise.all( txResult.records .map((record) => record.get('resource')) - .map((resource) => deleteImage(resource, 'HERO_IMAGE', { transaction })), + .map((resource) => images.deleteImage(resource, 'HERO_IMAGE', { transaction })), ) }), ) @@ -234,9 +280,8 @@ export default { `, { userId }, ) - log(deleteUserTransactionResponse) const [user] = deleteUserTransactionResponse.records.map((record) => record.get('user')) - await deleteImage(user, 'AVATAR_IMAGE', { transaction }) + await images.deleteImage(user, 'AVATAR_IMAGE', { transaction }) return user }) try { @@ -246,7 +291,7 @@ export default { session.close() } }, - switchUserRole: async (object, args, context, resolveInfo) => { + switchUserRole: async (_object, args, context, _resolveInfo) => { const { role, id } = args if (context.user.id === id) throw new Error('you-cannot-change-your-own-role') @@ -255,14 +300,14 @@ export default { const switchUserRoleResponse = await transaction.run( ` MATCH (user:User {id: $id}) + OPTIONAL MATCH (user)-[:PRIMARY_EMAIL]->(e:EmailAddress) SET user.role = $role SET user.updatedAt = toString(datetime()) - RETURN user {.*} + RETURN user {.*, email: e.email} `, { id, role }, ) - const [user] = switchUserRoleResponse.records.map((record) => record.get('user')) - return user + return switchUserRoleResponse.records.map((record) => record.get('user'))[0] }) try { const user = await writeTxResultPromise @@ -271,7 +316,7 @@ export default { session.close() } }, - saveCategorySettings: async (object, args, context, resolveInfo) => { + saveCategorySettings: async (_object, args, context, _resolveInfo) => { const { activeCategories } = args const { user: { id }, @@ -314,7 +359,7 @@ export default { session.close() } }, - updateOnlineStatus: async (object, args, context, resolveInfo) => { + updateOnlineStatus: async (_object, args, context, _resolveInfo) => { const { status } = args const { user: { id }, @@ -345,15 +390,247 @@ export default { return true }, + setTrophyBadgeSelected: async (_object, args, context, _resolveInfo) => { + const { slot, badgeId } = args + const { + user: { id: userId }, + } = context + + if (slot >= TROPHY_BADGES_SELECTED_MAX || slot < 0) { + throw new Error( + `Invalid slot! There is only ${TROPHY_BADGES_SELECTED_MAX} badge-slots to fill`, + ) + } + + const session = context.driver.session() + + const query = session.writeTransaction(async (transaction) => { + const queryBadge = ` + MATCH (user:User {id: $userId})<-[:REWARDED]-(badge:Badge {id: $badgeId}) + OPTIONAL MATCH (user)-[badgeRelation:SELECTED]->(badge) + OPTIONAL MATCH (user)-[slotRelation:SELECTED{slot: $slot}]->(:Badge) + DELETE badgeRelation, slotRelation + MERGE (user)-[:SELECTED{slot: toInteger($slot)}]->(badge) + RETURN user {.*} + ` + const queryEmpty = ` + MATCH (user:User {id: $userId}) + OPTIONAL MATCH (user)-[slotRelation:SELECTED {slot: $slot}]->(:Badge) + DELETE slotRelation + RETURN user {.*} + ` + const isDefault = !badgeId || badgeId === defaultTrophyBadge.id + + const result = await transaction.run(isDefault ? queryEmpty : queryBadge, { + userId, + badgeId, + slot, + }) + return result.records.map((record) => record.get('user'))[0] + }) + try { + const user = await query + if (!user) { + throw new Error('You cannot set badges not rewarded to you.') + } + return user + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, + resetTrophyBadgesSelected: async (_object, _args, context, _resolveInfo) => { + const { + user: { id: userId }, + } = context + + const session = context.driver.session() + + const query = session.writeTransaction(async (transaction) => { + const result = await transaction.run( + ` + MATCH (user:User {id: $userId}) + OPTIONAL MATCH (user)-[relation:SELECTED]->(:Badge) + DELETE relation + RETURN user {.*} + `, + { userId }, + ) + return result.records.map((record) => record.get('user'))[0] + }) + try { + return await query + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, }, User: { - email: async (parent, params, context, resolveInfo) => { - if (typeof parent.email !== 'undefined') return parent.email - const { id } = parent - const statement = `MATCH(u:User {id: $id})-[:PRIMARY_EMAIL]->(e:EmailAddress) RETURN e` - const result = await neode.cypher(statement, { id }) - const [{ email }] = result.records.map((r) => r.get('e').properties) - return email + inviteCodes: async (_parent, _args, context: Context, _resolveInfo) => { + return ( + await context.database.query({ + query: ` + MATCH (user:User {id: $user.id})-[:GENERATED]->(inviteCodes:InviteCode) + WHERE NOT (inviteCodes)-[:INVITES_TO]->(:Group) + RETURN inviteCodes {.*} + ORDER BY inviteCodes.createdAt ASC + `, + variables: { user: context.user }, + }) + ).records.map((record) => record.get('inviteCodes')) + }, + emailNotificationSettings: async (parent, _params, _context, _resolveInfo) => { + return [ + { + type: 'post', + settings: [ + { + name: 'commentOnObservedPost', + value: parent.emailNotificationsCommentOnObservedPost ?? true, + }, + { + name: 'mention', + value: parent.emailNotificationsMention ?? true, + }, + { + name: 'followingUsers', + value: parent.emailNotificationsFollowingUsers ?? true, + }, + { + name: 'postInGroup', + value: parent.emailNotificationsPostInGroup ?? true, + }, + ], + }, + { + type: 'chat', + settings: [ + { + name: 'chatMessage', + value: parent.emailNotificationsChatMessage ?? true, + }, + ], + }, + { + type: 'group', + settings: [ + { + name: 'groupMemberJoined', + value: parent.emailNotificationsGroupMemberJoined ?? true, + }, + { + name: 'groupMemberLeft', + value: parent.emailNotificationsGroupMemberLeft ?? true, + }, + { + name: 'groupMemberRemoved', + value: parent.emailNotificationsGroupMemberRemoved ?? true, + }, + { + name: 'groupMemberRoleChanged', + value: parent.emailNotificationsGroupMemberRoleChanged ?? true, + }, + ], + }, + ] + }, + badgeTrophiesSelected: async (parent, _params, context, _resolveInfo) => { + const session = context.driver.session() + + const query = session.readTransaction(async (transaction) => { + const result = await transaction.run( + ` + MATCH (user:User {id: $parent.id})-[relation:SELECTED]->(badge:Badge) + WITH relation, badge + ORDER BY relation.slot ASC + RETURN relation.slot as slot, badge {.*} + `, + { parent }, + ) + return result.records + }) + try { + const badgesSelected = await query + const result = Array(TROPHY_BADGES_SELECTED_MAX).fill(defaultTrophyBadge) + badgesSelected.map((record) => { + result[record.get('slot')] = record.get('badge') + return true + }) + return result + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, + badgeTrophiesUnused: async (parent, _params, context, _resolveInfo) => { + const session = context.driver.session() + + const query = session.readTransaction(async (transaction) => { + const result = await transaction.run( + ` + MATCH (user:User {id: $parent.id})<-[:REWARDED]-(badge:Badge) + WHERE NOT (user)-[:SELECTED]-(badge) + RETURN badge {.*} + `, + { parent }, + ) + return result.records.map((record) => record.get('badge')) + }) + try { + return await query + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, + badgeTrophiesUnusedCount: async (parent, _params, context, _resolveInfo) => { + const session = context.driver.session() + + const query = session.readTransaction(async (transaction) => { + const result = await transaction.run( + ` + MATCH (user:User {id: $parent.id})<-[:REWARDED]-(badge:Badge) + WHERE NOT (user)-[:SELECTED]-(badge) + RETURN toString(COUNT(badge)) as count + `, + { parent }, + ) + return result.records.map((record) => record.get('count'))[0] + }) + try { + return await query + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, + badgeVerification: async (parent, _params, context, _resolveInfo) => { + const session = context.driver.session() + + const query = session.writeTransaction(async (transaction) => { + const result = await transaction.run( + ` + MATCH (user:User {id: $parent.id})<-[:VERIFIES]-(verification:Badge) + RETURN verification {.*} + `, + { parent }, + ) + return result.records.map((record) => record.get('verification'))[0] + }) + try { + const result = await query + return result ?? defaultVerificationBadge + } catch (error) { + throw new Error(error) + } finally { + session.close() + } }, ...Resolver('User', { undefinedToNull: [ @@ -366,7 +643,6 @@ export default { 'termsAndConditionsAgreedAt', 'allowEmbedIframes', 'showShoutsPublicly', - 'sendNotificationEmails', 'locale', ], boolean: { @@ -389,7 +665,7 @@ export default { '-[:WROTE]->(c:Comment)-[:COMMENTS]->(related:Post) WHERE NOT related.disabled = true AND NOT related.deleted = true', shoutedCount: '-[:SHOUTED]->(related:Post) WHERE NOT related.disabled = true AND NOT related.deleted = true', - badgesCount: '<-[:REWARDED]-(related:Badge)', + badgeTrophiesCount: '<-[:REWARDED]-(related:Badge)', }, hasOne: { avatar: '-[:AVATAR_IMAGE]->(related:Image)', @@ -406,8 +682,7 @@ export default { comments: '-[:WROTE]->(related:Comment)', shouted: '-[:SHOUTED]->(related:Post)', categories: '-[:CATEGORIZED]->(related:Category)', - badges: '<-[:REWARDED]-(related:Badge)', - inviteCodes: '-[:GENERATED]->(related:InviteCode)', + badgeTrophies: '<-[:REWARDED]-(related:Badge)', }, }), }, diff --git a/backend/src/schema/resolvers/users/location.spec.ts b/backend/src/graphql/resolvers/users/location.spec.ts similarity index 94% rename from backend/src/schema/resolvers/users/location.spec.ts rename to backend/src/graphql/resolvers/users/location.spec.ts index f77f8d7f0..659c126dd 100644 --- a/backend/src/schema/resolvers/users/location.spec.ts +++ b/backend/src/graphql/resolvers/users/location.spec.ts @@ -1,8 +1,12 @@ -import gql from 'graphql-tag' -import Factory, { cleanDatabase } from '../../../db/factories' -import { getNeode, getDriver } from '../../../db/neo4j' +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ import { createTestClient } from 'apollo-server-testing' -import createServer from '../../../server' +import gql from 'graphql-tag' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' const neode = getNeode() const driver = getDriver() @@ -90,7 +94,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(() => { @@ -209,6 +213,7 @@ describe('userMiddleware', () => { await mutate({ mutation: updateUserMutation, variables }) const locations = await neode.cypher( `MATCH (city:Location)-[:IS_IN]->(district:Location)-[:IS_IN]->(state:Location)-[:IS_IN]->(country:Location) return city {.*}, state {.*}, country {.*}`, + {}, ) expect( locations.records.map((record) => { diff --git a/backend/src/schema/resolvers/users/location.ts b/backend/src/graphql/resolvers/users/location.ts similarity index 81% rename from backend/src/schema/resolvers/users/location.ts rename to backend/src/graphql/resolvers/users/location.ts index 0c3f55595..dc515e70d 100644 --- a/backend/src/schema/resolvers/users/location.ts +++ b/backend/src/graphql/resolvers/users/location.ts @@ -1,10 +1,17 @@ -import request from 'request' +/* eslint-disable @typescript-eslint/restrict-plus-operands */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable promise/avoid-new */ +/* eslint-disable promise/prefer-await-to-callbacks */ import { UserInputError } from 'apollo-server' -import Debug from 'debug' -import asyncForEach from '../../../helpers/asyncForEach' -import CONFIG from '../../../config' +import request from 'request' -const debug = Debug('human-connection:location') +import CONFIG from '@config/index' const fetch = (url) => { return new Promise((resolve, reject) => { @@ -34,8 +41,8 @@ const createLocation = async (session, mapboxData) => { nameRU: mapboxData.text_ru, type: mapboxData.id.split('.')[0].toLowerCase(), address: mapboxData.address, - lng: mapboxData.center && mapboxData.center.length ? mapboxData.center[0] : null, - lat: mapboxData.center && mapboxData.center.length ? mapboxData.center[1] : null, + lng: mapboxData.center?.length ? mapboxData.center[0] : null, + lat: mapboxData.center?.length ? mapboxData.center[1] : null, } let mutation = @@ -80,9 +87,7 @@ export const createOrUpdateLocations = async (nodeLabel, nodeId, locationName, s }&types=region,place,country,address&language=${locales.join(',')}`, ) - debug(res) - - if (!res || !res.features || !res.features[0]) { + if (!res?.features?.[0]) { throw new UserInputError('locationName is invalid') } @@ -97,7 +102,7 @@ export const createOrUpdateLocations = async (nodeLabel, nodeId, locationName, s data = res.features[0] } - if (!data || !data.place_type || !data.place_type.length) { + if (!data?.place_type?.length) { throw new UserInputError('locationName is invalid') } @@ -113,7 +118,7 @@ export const createOrUpdateLocations = async (nodeLabel, nodeId, locationName, s } if (data.context) { - await asyncForEach(data.context, async (ctx) => { + for await (const ctx of data.context) { await createLocation(session, ctx) await session.writeTransaction((transaction) => { return transaction.run( @@ -129,7 +134,7 @@ export const createOrUpdateLocations = async (nodeLabel, nodeId, locationName, s ) }) parent = ctx - }) + } } locationId = data.id @@ -159,7 +164,7 @@ export const queryLocations = async ({ place, lang }) => { `https://api.mapbox.com/geocoding/v5/mapbox.places/${place}.json?access_token=${CONFIG.MAPBOX_TOKEN}&types=region,place,country&language=${lang}`, ) // Return empty array if no location found or error occurred - if (!res || !res.features) { + if (!res?.features) { return [] } return res.features diff --git a/backend/src/schema/resolvers/users/mutedUsers.spec.ts b/backend/src/graphql/resolvers/users/mutedUsers.spec.ts similarity index 96% rename from backend/src/schema/resolvers/users/mutedUsers.spec.ts rename to backend/src/graphql/resolvers/users/mutedUsers.spec.ts index 762893af0..ccb6d2a87 100644 --- a/backend/src/schema/resolvers/users/mutedUsers.spec.ts +++ b/backend/src/graphql/resolvers/users/mutedUsers.spec.ts @@ -1,8 +1,13 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ import { createTestClient } from 'apollo-server-testing' -import createServer from '../../../server' -import { cleanDatabase } from '../../../db/factories' import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../../db/neo4j' + +import { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' const driver = getDriver() const neode = getNeode() @@ -18,7 +23,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(() => { @@ -59,6 +64,7 @@ describe('mutedUsers', () => { it('throws permission error', async () => { const { query } = createTestClient(server) const result = await query({ query: mutedUserQuery }) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion expect(result.errors![0]).toHaveProperty('message', 'Not Authorized!') }) diff --git a/backend/src/schema/resolvers/viewedTeaserCount.spec.ts b/backend/src/graphql/resolvers/viewedTeaserCount.spec.ts similarity index 85% rename from backend/src/schema/resolvers/viewedTeaserCount.spec.ts rename to backend/src/graphql/resolvers/viewedTeaserCount.spec.ts index ee90d1a08..f4fba31f8 100644 --- a/backend/src/schema/resolvers/viewedTeaserCount.spec.ts +++ b/backend/src/graphql/resolvers/viewedTeaserCount.spec.ts @@ -1,8 +1,12 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../../db/factories' import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' const driver = getDriver() const neode = getNeode() @@ -28,7 +32,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('count post teaser views', () => { diff --git a/backend/src/schema/index.ts b/backend/src/graphql/schema.ts similarity index 67% rename from backend/src/schema/index.ts rename to backend/src/graphql/schema.ts index 07721bceb..55eccb4dc 100644 --- a/backend/src/schema/index.ts +++ b/backend/src/graphql/schema.ts @@ -1,5 +1,9 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { makeAugmentedSchema } from 'neo4j-graphql-js' -import typeDefs from './types' + +import typeDefs from '@graphql/types/index' + import resolvers from './resolvers' export default makeAugmentedSchema({ @@ -10,6 +14,8 @@ export default makeAugmentedSchema({ exclude: [ 'Badge', 'Embed', + 'EmailNotificationSettings', + 'EmailNotificationSettingsOption', 'EmailAddress', 'Notification', 'Statistics', diff --git a/backend/src/graphql/types/enum/EmailNotificationSettingsName.gql b/backend/src/graphql/types/enum/EmailNotificationSettingsName.gql new file mode 100644 index 000000000..59989faca --- /dev/null +++ b/backend/src/graphql/types/enum/EmailNotificationSettingsName.gql @@ -0,0 +1,11 @@ +enum EmailNotificationSettingsName { + commentOnObservedPost + mention + followingUsers + postInGroup + chatMessage + groupMemberJoined + groupMemberLeft + groupMemberRemoved + groupMemberRoleChanged +} diff --git a/backend/src/graphql/types/enum/EmailNotificationSettingsType.gql b/backend/src/graphql/types/enum/EmailNotificationSettingsType.gql new file mode 100644 index 000000000..70128a6b2 --- /dev/null +++ b/backend/src/graphql/types/enum/EmailNotificationSettingsType.gql @@ -0,0 +1,5 @@ +enum EmailNotificationSettingsType { + post + chat + group +} \ No newline at end of file diff --git a/backend/src/schema/types/enum/Emotion.gql b/backend/src/graphql/types/enum/Emotion.gql similarity index 100% rename from backend/src/schema/types/enum/Emotion.gql rename to backend/src/graphql/types/enum/Emotion.gql diff --git a/backend/src/schema/types/enum/GroupActionRadius.gql b/backend/src/graphql/types/enum/GroupActionRadius.gql similarity index 100% rename from backend/src/schema/types/enum/GroupActionRadius.gql rename to backend/src/graphql/types/enum/GroupActionRadius.gql diff --git a/backend/src/schema/types/enum/GroupMemberRole.gql b/backend/src/graphql/types/enum/GroupMemberRole.gql similarity index 100% rename from backend/src/schema/types/enum/GroupMemberRole.gql rename to backend/src/graphql/types/enum/GroupMemberRole.gql diff --git a/backend/src/schema/types/enum/GroupType.gql b/backend/src/graphql/types/enum/GroupType.gql similarity index 100% rename from backend/src/schema/types/enum/GroupType.gql rename to backend/src/graphql/types/enum/GroupType.gql diff --git a/backend/src/schema/types/enum/OnlineStatus.gql b/backend/src/graphql/types/enum/OnlineStatus.gql similarity index 100% rename from backend/src/schema/types/enum/OnlineStatus.gql rename to backend/src/graphql/types/enum/OnlineStatus.gql diff --git a/backend/src/schema/types/enum/PostType.gql b/backend/src/graphql/types/enum/PostType.gql similarity index 100% rename from backend/src/schema/types/enum/PostType.gql rename to backend/src/graphql/types/enum/PostType.gql diff --git a/backend/src/graphql/types/enum/ShoutTypeEnum.gql b/backend/src/graphql/types/enum/ShoutTypeEnum.gql new file mode 100644 index 000000000..97c17316f --- /dev/null +++ b/backend/src/graphql/types/enum/ShoutTypeEnum.gql @@ -0,0 +1,4 @@ +enum ShoutTypeEnum { + Post + Comment +} \ No newline at end of file diff --git a/backend/src/schema/types/enum/UserRole.gql b/backend/src/graphql/types/enum/UserRole.gql similarity index 100% rename from backend/src/schema/types/enum/UserRole.gql rename to backend/src/graphql/types/enum/UserRole.gql diff --git a/backend/src/schema/types/enum/Visibility.gql b/backend/src/graphql/types/enum/Visibility.gql similarity index 100% rename from backend/src/schema/types/enum/Visibility.gql rename to backend/src/graphql/types/enum/Visibility.gql diff --git a/backend/src/schema/types/index.ts b/backend/src/graphql/types/index.ts similarity index 85% rename from backend/src/schema/types/index.ts rename to backend/src/graphql/types/index.ts index d49becffc..42d813ae4 100644 --- a/backend/src/schema/types/index.ts +++ b/backend/src/graphql/types/index.ts @@ -1,4 +1,5 @@ -import path from 'path' +import path from 'node:path' + import { mergeTypes, fileLoader } from 'merge-graphql-schemas' const typeDefs = fileLoader(path.join(__dirname, './**/*.gql')) diff --git a/backend/src/schema/types/scalar/Upload.gql b/backend/src/graphql/types/scalar/Upload.gql similarity index 100% rename from backend/src/schema/types/scalar/Upload.gql rename to backend/src/graphql/types/scalar/Upload.gql diff --git a/backend/src/graphql/types/type/Badge.gql b/backend/src/graphql/types/type/Badge.gql new file mode 100644 index 000000000..8cdad2ee7 --- /dev/null +++ b/backend/src/graphql/types/type/Badge.gql @@ -0,0 +1,26 @@ +type Badge { + id: ID! + type: BadgeType! + icon: String! + createdAt: String + description: String! + isDefault: Boolean! + + rewarded: [User]! @relation(name: "REWARDED", direction: "OUT") + verifies: [User]! @relation(name: "VERIFIES", direction: "OUT") +} + +enum BadgeType { + verification + trophy +} + +type Query { + Badge: [Badge] +} + +type Mutation { + setVerificationBadge(badgeId: ID!, userId: ID!): User + rewardTrophyBadge(badgeId: ID!, userId: ID!): User + revokeBadge(badgeId: ID!, userId: ID!): User +} diff --git a/backend/src/schema/types/type/Category.gql b/backend/src/graphql/types/type/Category.gql similarity index 100% rename from backend/src/schema/types/type/Category.gql rename to backend/src/graphql/types/type/Category.gql diff --git a/backend/src/schema/types/type/Comment.gql b/backend/src/graphql/types/type/Comment.gql similarity index 84% rename from backend/src/schema/types/type/Comment.gql rename to backend/src/graphql/types/type/Comment.gql index b1fd7a838..f82a44a79 100644 --- a/backend/src/schema/types/type/Comment.gql +++ b/backend/src/graphql/types/type/Comment.gql @@ -53,6 +53,14 @@ type Comment { ) postObservingUsersCount: Int! @cypher(statement: "MATCH (this)-[:COMMENTS]->(:Post)<-[obs:OBSERVES]-(u:User) WHERE obs.active = true AND NOT u.disabled = true AND NOT u.deleted = true RETURN COUNT(DISTINCT u)") + + shoutedByCurrentUser: Boolean! + @cypher(statement: "MATCH (this) RETURN EXISTS((this)<-[:SHOUTED]-(:User {id: $cypherParams.currentUserId}))") + + shoutedCount: Int! + @cypher( + statement: "MATCH (this)<-[:SHOUTED]-(related:User) WHERE NOT related.deleted = true AND NOT related.disabled = true RETURN COUNT(DISTINCT related)" + ) } type Query { diff --git a/backend/src/schema/types/type/Donations.gql b/backend/src/graphql/types/type/Donations.gql similarity index 100% rename from backend/src/schema/types/type/Donations.gql rename to backend/src/graphql/types/type/Donations.gql diff --git a/backend/src/schema/types/type/EMOTED.gql b/backend/src/graphql/types/type/EMOTED.gql similarity index 100% rename from backend/src/schema/types/type/EMOTED.gql rename to backend/src/graphql/types/type/EMOTED.gql diff --git a/backend/src/schema/types/type/EmailAddress.gql b/backend/src/graphql/types/type/EmailAddress.gql similarity index 80% rename from backend/src/schema/types/type/EmailAddress.gql rename to backend/src/graphql/types/type/EmailAddress.gql index b2e65eafa..3251ff9dd 100644 --- a/backend/src/schema/types/type/EmailAddress.gql +++ b/backend/src/graphql/types/type/EmailAddress.gql @@ -9,7 +9,11 @@ type Query { } type Mutation { - Signup(email: String!, inviteCode: String = null): EmailAddress + Signup( + email: String! + locale: String! + inviteCode: String = null + ): EmailAddress SignupVerification( nonce: String! email: String! @@ -20,6 +24,7 @@ type Mutation { about: String termsAndConditionsAgreedVersion: String! locale: String + locationName: String = null ): User AddEmailAddress(email: String!): EmailAddress VerifyEmailAddress( diff --git a/backend/src/schema/types/embed.gql b/backend/src/graphql/types/type/Embed.gql similarity index 100% rename from backend/src/schema/types/embed.gql rename to backend/src/graphql/types/type/Embed.gql diff --git a/backend/src/schema/types/type/FILED.gql b/backend/src/graphql/types/type/FILED.gql similarity index 100% rename from backend/src/schema/types/type/FILED.gql rename to backend/src/graphql/types/type/FILED.gql diff --git a/backend/src/schema/types/type/Group.gql b/backend/src/graphql/types/type/Group.gql similarity index 90% rename from backend/src/schema/types/type/Group.gql rename to backend/src/graphql/types/type/Group.gql index acf585f71..0adc7853b 100644 --- a/backend/src/schema/types/type/Group.gql +++ b/backend/src/graphql/types/type/Group.gql @@ -41,6 +41,11 @@ type Group { myRole: GroupMemberRole # if 'null' then the current user is no member posts: [Post] @relation(name: "IN", direction: "IN") + + isMutedByMe: Boolean! @cypher(statement: "MATCH (this) RETURN EXISTS( (this)<-[:MUTED]-(:User {id: $cypherParams.currentUserId}) )") + + "inviteCodes to this group the current user has generated" + inviteCodes: [InviteCode]! @neo4j_ignore } @@ -137,4 +142,7 @@ type Mutation { groupId: ID! userId: ID! ): User + + muteGroup(groupId: ID!): Group + unmuteGroup(groupId: ID!): Group } diff --git a/backend/src/schema/types/type/Image.gql b/backend/src/graphql/types/type/Image.gql similarity index 100% rename from backend/src/schema/types/type/Image.gql rename to backend/src/graphql/types/type/Image.gql diff --git a/backend/src/graphql/types/type/InviteCode.gql b/backend/src/graphql/types/type/InviteCode.gql new file mode 100644 index 000000000..e0c83796a --- /dev/null +++ b/backend/src/graphql/types/type/InviteCode.gql @@ -0,0 +1,25 @@ +type InviteCode { + code: ID! + createdAt: String! + generatedBy: User @relation(name: "GENERATED", direction: "IN") + redeemedBy: [User] @relation(name: "REDEEMED", direction: "IN") + redeemedByCount: Int! @cypher(statement: "MATCH (this)<-[:REDEEMED]-(related:User)") + expiresAt: String + comment: String + + invitedTo: Group @neo4j_ignore + # invitedFrom: User! @neo4j_ignore # -> see generatedBy + + isValid: Boolean! @neo4j_ignore +} + +type Query { + validateInviteCode(code: String!): InviteCode +} + +type Mutation { + generatePersonalInviteCode(expiresAt: String = null, comment: String = null): InviteCode! + generateGroupInviteCode(groupId: ID!, expiresAt: String = null, comment: String = null): InviteCode! + invalidateInviteCode(code: String!): InviteCode + redeemInviteCode(code: String!): Boolean! +} diff --git a/backend/src/schema/types/type/Location.gql b/backend/src/graphql/types/type/Location.gql similarity index 96% rename from backend/src/schema/types/type/Location.gql rename to backend/src/graphql/types/type/Location.gql index 9cb5c970a..d9c0ec1cc 100644 --- a/backend/src/schema/types/type/Location.gql +++ b/backend/src/graphql/types/type/Location.gql @@ -14,6 +14,7 @@ type Location { lat: Float lng: Float parent: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l") + distanceToMe: Int } # This is not smart - we need one location for everything - use the same type everywhere! diff --git a/backend/src/schema/types/type/MEMBER_OF.gql b/backend/src/graphql/types/type/MEMBER_OF.gql similarity index 100% rename from backend/src/schema/types/type/MEMBER_OF.gql rename to backend/src/graphql/types/type/MEMBER_OF.gql diff --git a/backend/src/schema/types/type/Message.gql b/backend/src/graphql/types/type/Message.gql similarity index 100% rename from backend/src/schema/types/type/Message.gql rename to backend/src/graphql/types/type/Message.gql diff --git a/backend/src/schema/types/type/NOTIFIED.gql b/backend/src/graphql/types/type/NOTIFIED.gql similarity index 95% rename from backend/src/schema/types/type/NOTIFIED.gql rename to backend/src/graphql/types/type/NOTIFIED.gql index 1f825decc..d32b4e042 100644 --- a/backend/src/schema/types/type/NOTIFIED.gql +++ b/backend/src/graphql/types/type/NOTIFIED.gql @@ -26,6 +26,8 @@ enum NotificationReason { user_left_group changed_group_member_role removed_user_from_group + followed_user_posted + post_in_group } type Query { diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/graphql/types/type/Post.gql similarity index 95% rename from backend/src/schema/types/type/Post.gql rename to backend/src/graphql/types/type/Post.gql index fcaa5945a..0c654b86b 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/graphql/types/type/Post.gql @@ -103,6 +103,8 @@ enum _PostOrdering { createdAt_desc updatedAt_asc updatedAt_desc + sortDate_asc + sortDate_desc language_asc language_desc pinned_asc @@ -128,6 +130,7 @@ type Post { pinned: Boolean createdAt: String updatedAt: String + sortDate: String language: String pinnedAt: String @cypher( statement: "MATCH (this)<-[pinned:PINNED]-(:User) WHERE NOT this.deleted = true AND NOT this.disabled = true RETURN pinned.createdAt" @@ -241,15 +244,22 @@ type Mutation { pinPost(id: ID!): Post unpinPost(id: ID!): Post markTeaserAsViewed(id: ID!): Post + pushPost(id: ID!): Post! + unpushPost(id: ID!): Post! # Shout the given Type and ID - shout(id: ID!, type: ShoutTypeEnum): Boolean! + shout(id: ID!, type: ShoutTypeEnum!): Boolean! # Unshout the given Type and ID - unshout(id: ID!, type: ShoutTypeEnum): Boolean! + unshout(id: ID!, type: ShoutTypeEnum!): Boolean! toggleObservePost(id: ID!, value: Boolean!): Post! } +type PinnedPostCounts { + maxPinnedPosts: Int! + currentlyPinnedPosts: Int! +} + type Query { Post( id: ID @@ -271,4 +281,5 @@ type Query { PostsEmotionsCountByEmotion(postId: ID!, data: _EMOTEDInput!): Int! PostsEmotionsByCurrentUser(postId: ID!): [String] profilePagePosts(filter: _PostFilter, first: Int, offset: Int, orderBy: [_PostOrdering]): [Post] + PostsPinnedCounts: PinnedPostCounts! } diff --git a/backend/src/schema/types/type/REVIEWED.gql b/backend/src/graphql/types/type/REVIEWED.gql similarity index 100% rename from backend/src/schema/types/type/REVIEWED.gql rename to backend/src/graphql/types/type/REVIEWED.gql diff --git a/backend/src/schema/types/type/Report.gql b/backend/src/graphql/types/type/Report.gql similarity index 100% rename from backend/src/schema/types/type/Report.gql rename to backend/src/graphql/types/type/Report.gql diff --git a/backend/src/schema/types/schema.gql b/backend/src/graphql/types/type/Reward.gql.unused similarity index 68% rename from backend/src/schema/types/schema.gql rename to backend/src/graphql/types/type/Reward.gql.unused index 9e2d00bca..6691b34f1 100644 --- a/backend/src/schema/types/schema.gql +++ b/backend/src/graphql/types/type/Reward.gql.unused @@ -1,16 +1,7 @@ -enum ShoutTypeEnum { - Post -} - type Reward { id: ID! user: User @relation(name: "REWARDED", direction: "IN") rewarderId: ID createdAt: String badge: Badge @relation(name: "REWARDED", direction: "OUT") -} - -type SharedInboxEndpoint { - id: ID! - uri: String } \ No newline at end of file diff --git a/backend/src/schema/types/type/Room.gql b/backend/src/graphql/types/type/Room.gql similarity index 100% rename from backend/src/schema/types/type/Room.gql rename to backend/src/graphql/types/type/Room.gql diff --git a/backend/src/schema/types/type/Search.gql b/backend/src/graphql/types/type/Search.gql similarity index 100% rename from backend/src/schema/types/type/Search.gql rename to backend/src/graphql/types/type/Search.gql diff --git a/backend/src/graphql/types/type/SharedInboxEndpoint.gql.old b/backend/src/graphql/types/type/SharedInboxEndpoint.gql.old new file mode 100644 index 000000000..b078af63b --- /dev/null +++ b/backend/src/graphql/types/type/SharedInboxEndpoint.gql.old @@ -0,0 +1,4 @@ +type SharedInboxEndpoint { + id: ID! + uri: String +} \ No newline at end of file diff --git a/backend/src/schema/types/type/SocialMedia.gql b/backend/src/graphql/types/type/SocialMedia.gql similarity index 100% rename from backend/src/schema/types/type/SocialMedia.gql rename to backend/src/graphql/types/type/SocialMedia.gql diff --git a/backend/src/graphql/types/type/Statistics.gql b/backend/src/graphql/types/type/Statistics.gql new file mode 100644 index 000000000..d01ff194b --- /dev/null +++ b/backend/src/graphql/types/type/Statistics.gql @@ -0,0 +1,28 @@ +type Query { + statistics: Statistics! +} + +type Statistics { + users: Int! + usersDeleted: Int! + posts: Int! + comments: Int! + notifications: Int! + emails: Int! + follows: Int! + shouts: Int! + invites: Int! + chatMessages: Int! + chatRooms: Int! + tags: Int! + locations: Int! + groups: Int! + inviteCodes: Int! + inviteCodesExpired: Int! + inviteCodesRedeemed: Int! + badgesRewarded: Int! + badgesDisplayed: Int! + usersVerified: Int! + reports: Int! +} + diff --git a/backend/src/schema/types/type/Tag.gql b/backend/src/graphql/types/type/Tag.gql similarity index 100% rename from backend/src/schema/types/type/Tag.gql rename to backend/src/graphql/types/type/Tag.gql diff --git a/backend/src/schema/types/type/User.gql b/backend/src/graphql/types/type/User.gql similarity index 83% rename from backend/src/schema/types/type/User.gql rename to backend/src/graphql/types/type/User.gql index 70b10aa42..7c78b38ec 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/graphql/types/type/User.gql @@ -19,6 +19,21 @@ enum _UserOrdering { locale_desc } +input EmailNotificationSettingsInput { + name: EmailNotificationSettingsName + value: Boolean +} + +type EmailNotificationSettings { + type: EmailNotificationSettingsType + settings: [EmailNotificationSettingsOption] @neo4j_ignore +} + +type EmailNotificationSettingsOption { + name: EmailNotificationSettingsName + value: Boolean +} + type User { id: ID! actorId: String @@ -46,7 +61,7 @@ type User { allowEmbedIframes: Boolean showShoutsPublicly: Boolean - sendNotificationEmails: Boolean + emailNotificationSettings: [EmailNotificationSettings]! @neo4j_ignore locale: String friends: [User]! @relation(name: "FRIENDS", direction: "BOTH") friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(DISTINCT r)") @@ -57,9 +72,6 @@ type User { followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN") followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(DISTINCT r)") - inviteCodes: [InviteCode] @relation(name: "GENERATED", direction: "OUT") - redeemedInviteCode: InviteCode @relation(name: "REDEEMED", direction: "OUT") - # Is the currently logged in user following that user? followedByCurrentUser: Boolean! @cypher( statement: """ @@ -110,8 +122,18 @@ type User { categories: [Category] @relation(name: "CATEGORIZED", direction: "OUT") - badges: [Badge]! @relation(name: "REWARDED", direction: "IN") - badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)") + # Badges + badgeVerification: Badge! @neo4j_ignore + badgeTrophies: [Badge]! @relation(name: "REWARDED", direction: "IN") + badgeTrophiesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)") + badgeTrophiesSelected: [Badge!]! @neo4j_ignore + badgeTrophiesUnused: [Badge]! @neo4j_ignore + badgeTrophiesUnusedCount: Int! @neo4j_ignore + + "personal inviteCodes the user has generated" + inviteCodes: [InviteCode]! @neo4j_ignore + # inviteCodes: [InviteCode]! @relation(name: "GENERATED", direction: "OUT") + redeemedInviteCode: InviteCode @relation(name: "REDEEMED", direction: "OUT") emotions: [EMOTED] @@ -184,8 +206,7 @@ type Query { availableRoles: [UserRole]! mutedUsers: [User] blockedUsers: [User] - isLoggedIn: Boolean! - currentUser: User + currentUser: User! } enum Deletable { @@ -206,7 +227,7 @@ type Mutation { termsAndConditionsAgreedAt: String allowEmbedIframes: Boolean showShoutsPublicly: Boolean - sendNotificationEmails: Boolean + emailNotificationSettings: [EmailNotificationSettingsInput] locale: String ): User @@ -227,10 +248,13 @@ type Mutation { updateOnlineStatus(status: OnlineStatus!): Boolean! - requestPasswordReset(email: String!): Boolean! + requestPasswordReset(email: String!, locale: String!): Boolean! resetPassword(email: String!, nonce: String!, newPassword: String!): Boolean! changePassword(oldPassword: String!, newPassword: String!): String! # Get a JWT Token for the given Email and password login(email: String!, password: String!): String! + + setTrophyBadgeSelected(slot: Int!, badgeId: ID): User + resetTrophyBadgesSelected: User } diff --git a/backend/src/schema/types/type/UserData.gql b/backend/src/graphql/types/type/UserData.gql similarity index 100% rename from backend/src/schema/types/type/UserData.gql rename to backend/src/graphql/types/type/UserData.gql diff --git a/backend/src/helpers/asyncForEach.ts b/backend/src/helpers/asyncForEach.ts deleted file mode 100644 index 5577cce14..000000000 --- a/backend/src/helpers/asyncForEach.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Provide a way to iterate for each element in an array while waiting for async functions to finish - * - * @param array - * @param callback - * @returns {Promise} - */ -async function asyncForEach(array, callback) { - for (let index = 0; index < array.length; index++) { - await callback(array[index], index, array) - } -} - -export default asyncForEach diff --git a/backend/src/helpers/encryptPassword.ts b/backend/src/helpers/encryptPassword.ts deleted file mode 100644 index 657dee98a..000000000 --- a/backend/src/helpers/encryptPassword.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { hashSync } from 'bcryptjs' - -export default function (args) { - args.encryptedPassword = hashSync(args.password, 10) - delete args.password - return args -} diff --git a/backend/src/helpers/jest.ts b/backend/src/helpers/jest.ts deleted file mode 100644 index 09744e9f2..000000000 --- a/backend/src/helpers/jest.ts +++ /dev/null @@ -1,8 +0,0 @@ -// sometime we have to wait to check a db state by having a look into the db in a certain moment -// or we wait a bit to check if we missed to set an await somewhere -// see: https://www.sitepoint.com/delay-sleep-pause-wait/ -export function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)) -} -// usage – 4 seconds for example -// await sleep(4 * 1000) diff --git a/backend/src/helpers/walkRecursive.ts b/backend/src/helpers/walkRecursive.ts deleted file mode 100644 index f3be67575..000000000 --- a/backend/src/helpers/walkRecursive.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * iterate through all fields and replace it with the callback result - * @property data Array - * @property fields Array - * @property fieldName String - * @property callback Function - */ -function walkRecursive(data, fields, fieldName, callback, _key?) { - if (!Array.isArray(fields)) { - throw new Error('please provide an fields array for the walkRecursive helper') - } - const fieldDef = fields.find((f) => f.field === _key) - if (data && typeof data === 'string' && fieldDef) { - if (!fieldDef.excludes?.includes(fieldName)) data = callback(data, _key) - } else if (data && Array.isArray(data)) { - // go into the rabbit hole and dig through that array - data.forEach((res, index) => { - data[index] = walkRecursive(data[index], fields, fieldName, callback, index) - }) - } else if (data && typeof data === 'object') { - // lets get some keys and stir them - Object.keys(data).forEach((k) => { - data[k] = walkRecursive(data[k], fields, fieldName, callback, k) - }) - } - return data -} - -export default walkRecursive diff --git a/backend/src/index.ts b/backend/src/index.ts index 59718dad1..612141733 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,5 +1,7 @@ -import createServer from './server' +/* eslint-disable @typescript-eslint/restrict-template-expressions */ + import CONFIG from './config' +import createServer from './server' const { server, httpServer } = createServer() const url = new URL(CONFIG.GRAPHQL_URI) diff --git a/backend/src/jwt/decode.spec.ts b/backend/src/jwt/decode.spec.ts index ca27ef624..cbb220b5b 100644 --- a/backend/src/jwt/decode.spec.ts +++ b/backend/src/jwt/decode.spec.ts @@ -1,5 +1,10 @@ -import Factory, { cleanDatabase } from '../db/factories' -import { getDriver, getNeode } from '../db/neo4j' +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import Factory, { cleanDatabase } from '@db/factories' +import User from '@db/models/User' +import { getDriver, getNeode } from '@db/neo4j' + import decode from './decode' import encode from './encode' @@ -12,7 +17,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) // TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 @@ -82,26 +87,28 @@ describe('decode', () => { }) it('sets `lastActiveAt`', async () => { - let user = await neode.first('User', { id: 'u3' }) + let user = await neode.first('User', { id: 'u3' }, undefined) await expect(user.toJson()).resolves.not.toHaveProperty('lastActiveAt') await decode(driver, validAuthorizationHeader) - user = await neode.first('User', { id: 'u3' }) + user = await neode.first('User', { id: 'u3' }, undefined) await expect(user.toJson()).resolves.toMatchObject({ lastActiveAt: expect.any(String), }) }) it('updates `lastActiveAt` for every authenticated request', async () => { - let user = await neode.first('User', { id: 'u3' }) + let user = await neode.first('User', { id: 'u3' }, undefined) await user.update({ - updatedAt: new Date().toISOString(), - lastActiveAt: '2019-10-03T23:33:08.598Z', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + updatedAt: new Date().toISOString() as any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + lastActiveAt: '2019-10-03T23:33:08.598Z' as any, }) await expect(user.toJson()).resolves.toMatchObject({ lastActiveAt: '2019-10-03T23:33:08.598Z', }) await decode(driver, validAuthorizationHeader) - user = await neode.first('User', { id: 'u3' }) + user = await neode.first('User', { id: 'u3' }, undefined) await expect(user.toJson()).resolves.toMatchObject({ // should be a different time by now ;) lastActiveAt: expect.not.stringContaining('2019-10-03T23:33'), diff --git a/backend/src/jwt/decode.ts b/backend/src/jwt/decode.ts index e02dcc8d4..0a433d38f 100644 --- a/backend/src/jwt/decode.ts +++ b/backend/src/jwt/decode.ts @@ -1,5 +1,10 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import jwt from 'jsonwebtoken' -import CONFIG from './../config' + +import CONFIG from '@config/index' export default async (driver, authorizationHeader) => { if (!authorizationHeader) return null @@ -8,6 +13,7 @@ export default async (driver, authorizationHeader) => { try { const decoded = await jwt.verify(token, CONFIG.JWT_SECRET) id = decoded.sub + // eslint-disable-next-line no-catch-all/no-catch-all } catch (err) { return null } diff --git a/backend/src/jwt/encode.spec.ts b/backend/src/jwt/encode.spec.ts index 21ebdffec..8121118f3 100644 --- a/backend/src/jwt/encode.spec.ts +++ b/backend/src/jwt/encode.spec.ts @@ -1,6 +1,11 @@ -import encode from './encode' +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import jwt from 'jsonwebtoken' -import CONFIG from './../config' + +import CONFIG from '@config/index' + +import encode from './encode' describe('encode', () => { let payload diff --git a/backend/src/jwt/encode.ts b/backend/src/jwt/encode.ts index baeb62d3d..742bf438b 100644 --- a/backend/src/jwt/encode.ts +++ b/backend/src/jwt/encode.ts @@ -1,5 +1,10 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import jwt from 'jsonwebtoken' -import CONFIG from './../config' + +import CONFIG from '@config/index' // Generate an Access Token for the given User ID export default function encode(user) { diff --git a/backend/src/middleware/branding/brandingMiddlewares.ts b/backend/src/middleware/branding/brandingMiddlewares.ts new file mode 100644 index 000000000..8d47043e8 --- /dev/null +++ b/backend/src/middleware/branding/brandingMiddlewares.ts @@ -0,0 +1,6 @@ +// eslint-disable-next-line import/no-cycle +import { MiddlewareOrder } from '@middleware/index' + +export default (): MiddlewareOrder[] => { + return [] +} diff --git a/backend/src/middleware/categories.spec.ts b/backend/src/middleware/categories.spec.ts new file mode 100644 index 000000000..3afda82a6 --- /dev/null +++ b/backend/src/middleware/categories.spec.ts @@ -0,0 +1,97 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +import { ApolloServer } from 'apollo-server-express' +import { createTestClient } from 'apollo-server-testing' +import gql from 'graphql-tag' + +import databaseContext from '@context/database' +import Factory, { cleanDatabase } from '@db/factories' +import CONFIG from '@src/config' +import { categories } from '@src/constants/categories' +import createServer, { getContext } from '@src/server' + +const database = databaseContext() + +let server: ApolloServer +let query + +beforeAll(async () => { + await cleanDatabase() + const authenticatedUser = null + + // eslint-disable-next-line @typescript-eslint/require-await + const contextUser = async (_req) => authenticatedUser + const context = getContext({ user: contextUser, database }) + + server = createServer({ context }).server + + const createTestClientResult = createTestClient(server) + query = createTestClientResult.query + + for (const category of categories) { + await Factory.build('category', { + id: category.id, + slug: category.slug, + name: category.name, + icon: category.icon, + }) + } +}) + +afterAll(() => { + void server.stop() + void database.driver.close() + database.neode.close() +}) + +const categoriesQuery = gql` + query { + Category { + id + slug + name + icon + } + } +` + +describe('categroeis middleware', () => { + describe('categories are active', () => { + beforeEach(() => { + CONFIG.CATEGORIES_ACTIVE = true + }) + + it('returns the categories', async () => { + await expect( + query({ + query: categoriesQuery, + }), + ).resolves.toMatchObject({ + data: { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + Category: expect.arrayContaining(categories), + }, + errors: undefined, + }) + }) + }) + + describe('categories are not active', () => { + beforeEach(() => { + CONFIG.CATEGORIES_ACTIVE = false + }) + + it('returns an empty array though there are categories in the db', async () => { + await expect( + query({ + query: categoriesQuery, + }), + ).resolves.toMatchObject({ + data: { + Category: [], + }, + errors: undefined, + }) + }) + }) +}) diff --git a/backend/src/middleware/categories.ts b/backend/src/middleware/categories.ts new file mode 100644 index 000000000..759a3938f --- /dev/null +++ b/backend/src/middleware/categories.ts @@ -0,0 +1,16 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +import CONFIG from '@src/config' + +const checkCategoriesActive = (resolve, root, args, context, resolveInfo) => { + if (CONFIG.CATEGORIES_ACTIVE) { + return resolve(root, args, context, resolveInfo) + } + return [] +} + +export default { + Query: { + Category: checkCategoriesActive, + }, +} diff --git a/backend/src/middleware/chatMiddleware.ts b/backend/src/middleware/chatMiddleware.ts index 8ae252e13..17d01fd95 100644 --- a/backend/src/middleware/chatMiddleware.ts +++ b/backend/src/middleware/chatMiddleware.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { isArray } from 'lodash' const setRoomProps = (room) => { diff --git a/backend/src/middleware/excerptMiddleware.ts b/backend/src/middleware/excerptMiddleware.ts index 28b30fb4f..7a865be90 100644 --- a/backend/src/middleware/excerptMiddleware.ts +++ b/backend/src/middleware/excerptMiddleware.ts @@ -1,5 +1,11 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import trunc from 'trunc-html' -import { DESCRIPTION_EXCERPT_HTML_LENGTH } from '../constants/groups' + +import { DESCRIPTION_EXCERPT_HTML_LENGTH } from '@constants/groups' export default { Mutation: { diff --git a/backend/src/middleware/hashtags/extractHashtags.spec.ts b/backend/src/middleware/hashtags/extractHashtags.spec.ts index 739c7de54..134ede761 100644 --- a/backend/src/middleware/hashtags/extractHashtags.spec.ts +++ b/backend/src/middleware/hashtags/extractHashtags.spec.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import extractHashtags from './extractHashtags' describe('extractHashtags', () => { diff --git a/backend/src/middleware/hashtags/extractHashtags.ts b/backend/src/middleware/hashtags/extractHashtags.ts index 670673bf4..26b224a9a 100644 --- a/backend/src/middleware/hashtags/extractHashtags.ts +++ b/backend/src/middleware/hashtags/extractHashtags.ts @@ -1,4 +1,10 @@ -import * as cheerio from 'cheerio' +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { load } from 'cheerio' +// eslint-disable-next-line import/extensions import { exec, build } from 'xregexp/xregexp-all.js' // formats of a Hashtag: // https://en.wikipedia.org/w/index.php?title=Hashtag&oldid=905141980#Style @@ -10,7 +16,7 @@ const regX = build('^((\\pL+[\\pL0-9]*)|([0-9]+\\pL+[\\pL0-9]*))$') export default function (content?) { if (!content) return [] - const $ = cheerio.load(content) + const $ = load(content) // We can not search for class '.hashtag', because the classes are removed at the 'xss' middleware. // But we have to know, which Hashtags are removed from the content as well, so we search for the 'a' html-tag. const ids = $('a[data-hashtag-id]') @@ -18,6 +24,7 @@ export default function (content?) { return $(el).attr('data-hashtag-id') }) .get() + // eslint-disable-next-line @typescript-eslint/no-explicit-any const hashtags: any = [] ids.forEach((id) => { const match = exec(id, regX) diff --git a/backend/src/middleware/hashtags/hashtagsMiddleware.spec.ts b/backend/src/middleware/hashtags/hashtagsMiddleware.spec.ts index 10d53ab7b..bc3b96594 100644 --- a/backend/src/middleware/hashtags/hashtagsMiddleware.spec.ts +++ b/backend/src/middleware/hashtags/hashtagsMiddleware.spec.ts @@ -1,8 +1,13 @@ -import gql from 'graphql-tag' -import { cleanDatabase } from '../../db/factories' +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' +import gql from 'graphql-tag' + +import { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' let server let query @@ -50,22 +55,15 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { - hashtagingUser = await neode.create( - 'User', - { - id: 'you', - name: 'Al Capone', - slug: 'al-capone', - }, - { - password: '1234', - email: 'test@example.org', - }, - ) + hashtagingUser = await neode.create('User', { + id: 'you', + name: 'Al Capone', + slug: 'al-capone', + }) await neode.create('Category', { id: 'cat9', name: 'Democracy & Politics', diff --git a/backend/src/middleware/hashtags/hashtagsMiddleware.ts b/backend/src/middleware/hashtags/hashtagsMiddleware.ts index 985cd3c92..2f53ee1a5 100644 --- a/backend/src/middleware/hashtags/hashtagsMiddleware.ts +++ b/backend/src/middleware/hashtags/hashtagsMiddleware.ts @@ -1,4 +1,8 @@ -import extractHashtags from '../hashtags/extractHashtags' +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +import extractHashtags from './extractHashtags' const updateHashtagsOfPost = async (postId, hashtags, context) => { if (!hashtags.length) return diff --git a/backend/src/middleware/helpers/cleanHtml.ts b/backend/src/middleware/helpers/cleanHtml.ts index 72129274c..d429f8f9d 100644 --- a/backend/src/middleware/helpers/cleanHtml.ts +++ b/backend/src/middleware/helpers/cleanHtml.ts @@ -1,5 +1,11 @@ -import sanitizeHtml from 'sanitize-html' +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable security/detect-unsafe-regex */ import linkifyHtml from 'linkify-html' +import sanitizeHtml from 'sanitize-html' export const removeHtmlTags = (input) => { return sanitizeHtml(input, { diff --git a/backend/src/middleware/helpers/email/sendMail.ts b/backend/src/middleware/helpers/email/sendMail.ts deleted file mode 100644 index c0e54e7f7..000000000 --- a/backend/src/middleware/helpers/email/sendMail.ts +++ /dev/null @@ -1,68 +0,0 @@ -import CONFIG from '../../../config' -import { cleanHtml } from '../../../middleware/helpers/cleanHtml' -import nodemailer from 'nodemailer' -import { htmlToText } from 'nodemailer-html-to-text' - -const hasEmailConfig = CONFIG.SMTP_HOST && CONFIG.SMTP_PORT -const hasAuthData = CONFIG.SMTP_USERNAME && CONFIG.SMTP_PASSWORD -const hasDKIMData = - CONFIG.SMTP_DKIM_DOMAINNAME && CONFIG.SMTP_DKIM_KEYSELECTOR && CONFIG.SMTP_DKIM_PRIVATKEY - -let sendMailCallback: any = async () => {} -if (!hasEmailConfig) { - if (!CONFIG.TEST) { - // eslint-disable-next-line no-console - console.log('Warning: Middlewares will not try to send mails.') - // TODO: disable e-mail logging on database seeding? - // TODO: implement general logging like 'log4js', see Gradido project: https://github.com/gradido/gradido/blob/master/backend/log4js-config.json - sendMailCallback = async (templateArgs) => { - // eslint-disable-next-line no-console - console.log('--- Log Unsend E-Mail ---') - // eslint-disable-next-line no-console - console.log('To: ' + templateArgs.to) - // eslint-disable-next-line no-console - console.log('From: ' + templateArgs.from) - // eslint-disable-next-line no-console - console.log('Subject: ' + templateArgs.subject) - // eslint-disable-next-line no-console - console.log('Content:') - // eslint-disable-next-line no-console - console.log( - cleanHtml(templateArgs.html, 'dummyKey', { - allowedTags: ['a'], - allowedAttributes: { a: ['href'] }, - } as any).replace(/&/g, '&'), - ) - } - } -} else { - sendMailCallback = async (templateArgs) => { - const transporter = nodemailer.createTransport({ - host: CONFIG.SMTP_HOST, - port: CONFIG.SMTP_PORT, - ignoreTLS: CONFIG.SMTP_IGNORE_TLS, - secure: CONFIG.SMTP_SECURE, // true for 465, false for other ports - auth: hasAuthData && { - user: CONFIG.SMTP_USERNAME, - pass: CONFIG.SMTP_PASSWORD, - }, - dkim: hasDKIMData && { - domainName: CONFIG.SMTP_DKIM_DOMAINNAME, - keySelector: CONFIG.SMTP_DKIM_KEYSELECTOR, - privateKey: CONFIG.SMTP_DKIM_PRIVATKEY, - }, - }) - - transporter.use( - 'compile', - htmlToText({ - ignoreImage: true, - wordwrap: false, - }), - ) - - await transporter.sendMail(templateArgs) - } -} - -export const sendMail = sendMailCallback diff --git a/backend/src/middleware/helpers/email/templateBuilder.spec.ts b/backend/src/middleware/helpers/email/templateBuilder.spec.ts deleted file mode 100644 index cb516c0a9..000000000 --- a/backend/src/middleware/helpers/email/templateBuilder.spec.ts +++ /dev/null @@ -1,238 +0,0 @@ -import CONFIG from '../../../config' -import logosWebapp from '../../../config/logos' -import { - signupTemplate, - emailVerificationTemplate, - resetPasswordTemplate, - wrongAccountTemplate, - notificationTemplate, -} from './templateBuilder' - -const englishHint = 'English version below!' -const welcomeImageUrl = new URL(logosWebapp.LOGO_WELCOME_PATH, CONFIG.CLIENT_URI) -const supportUrl = CONFIG.SUPPORT_URL.toString() -let actionUrl, name, settingsUrl - -const signupTemplateData = () => ({ - email: 'test@example.org', - variables: { - nonce: '12345', - inviteCode: 'AAAAAA', - }, -}) -const emailVerificationTemplateData = () => ({ - email: 'test@example.org', - variables: { - nonce: '12345', - name: 'Mr Example', - }, -}) -const resetPasswordTemplateData = () => ({ - email: 'test@example.org', - variables: { - nonce: '12345', - name: 'Mr Example', - }, -}) -const wrongAccountTemplateData = () => ({ - email: 'test@example.org', - variables: {}, -}) -const notificationTemplateData = (locale) => ({ - email: 'test@example.org', - variables: { - notification: { - to: { name: 'Mr Example', locale }, - }, - }, -}) -const textsStandard = [ - { - templPropName: 'from', - isContaining: false, - text: CONFIG.EMAIL_DEFAULT_SENDER, - }, - { - templPropName: 'to', - isContaining: false, - text: 'test@example.org', - }, - // is contained in html - welcomeImageUrl.toString(), - CONFIG.ORGANIZATION_URL, - CONFIG.APPLICATION_NAME, -] -const testEmailData = (emailTemplate, templateBuilder, templateData, texts) => { - if (!emailTemplate) { - emailTemplate = templateBuilder(templateData) - } - texts.forEach((element) => { - if (typeof element === 'object') { - if (element.isContaining) { - expect(emailTemplate[element.templPropName]).toEqual(expect.stringContaining(element.text)) - } else { - expect(emailTemplate[element.templPropName]).toEqual(element.text) - } - } else { - expect(emailTemplate.html).toEqual(expect.stringContaining(element)) - } - }) - return emailTemplate -} - -describe('templateBuilder', () => { - describe('signupTemplate', () => { - describe('multi language', () => { - it('e-mail is build with all data', () => { - const subject = `Willkommen, Bienvenue, Welcome to ${CONFIG.APPLICATION_NAME}!` - const actionUrl = new URL('/registration', CONFIG.CLIENT_URI).toString() - const theSignupTemplateData = signupTemplateData() - const enContent = "Thank you for joining our cause – it's awesome to have you on board." - const deContent = - 'Danke, dass Du dich angemeldet hast – wir freuen uns, Dich dabei zu haben.' - testEmailData(null, signupTemplate, theSignupTemplateData, [ - ...textsStandard, - { - templPropName: 'subject', - isContaining: false, - text: subject, - }, - englishHint, - actionUrl, - theSignupTemplateData.variables.nonce, - theSignupTemplateData.variables.inviteCode, - enContent, - deContent, - supportUrl, - ]) - }) - }) - }) - - describe('emailVerificationTemplate', () => { - describe('multi language', () => { - it('e-mail is build with all data', () => { - const subject = 'Neue E-Mail Adresse | New E-Mail Address' - const actionUrl = new URL('/settings/my-email-address/verify', CONFIG.CLIENT_URI).toString() - const theEmailVerificationTemplateData = emailVerificationTemplateData() - const enContent = 'So, you want to change your e-mail? No problem!' - const deContent = 'Du möchtest also deine E-Mail ändern? Kein Problem!' - testEmailData(null, emailVerificationTemplate, theEmailVerificationTemplateData, [ - ...textsStandard, - { - templPropName: 'subject', - isContaining: false, - text: subject, - }, - englishHint, - actionUrl, - theEmailVerificationTemplateData.variables.nonce, - theEmailVerificationTemplateData.variables.name, - enContent, - deContent, - supportUrl, - ]) - }) - }) - }) - - describe('resetPasswordTemplate', () => { - describe('multi language', () => { - it('e-mail is build with all data', () => { - const subject = 'Neues Passwort | Reset Password' - const actionUrl = new URL('/password-reset/change-password', CONFIG.CLIENT_URI).toString() - const theResetPasswordTemplateData = resetPasswordTemplateData() - const enContent = 'So, you forgot your password? No problem!' - const deContent = 'Du hast also dein Passwort vergessen? Kein Problem!' - testEmailData(null, resetPasswordTemplate, theResetPasswordTemplateData, [ - ...textsStandard, - { - templPropName: 'subject', - isContaining: false, - text: subject, - }, - englishHint, - actionUrl, - theResetPasswordTemplateData.variables.nonce, - theResetPasswordTemplateData.variables.name, - enContent, - deContent, - supportUrl, - ]) - }) - }) - }) - - describe('wrongAccountTemplate', () => { - describe('multi language', () => { - it('e-mail is build with all data', () => { - const subject = 'Falsche Mailadresse? | Wrong E-mail?' - const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI).toString() - const theWrongAccountTemplateData = wrongAccountTemplateData() - const enContent = - "You requested a password reset but unfortunately we couldn't find an account associated with your e-mail address." - const deContent = - 'Du hast bei uns ein neues Passwort angefordert – leider haben wir aber keinen Account mit Deiner E-Mailadresse gefunden.' - testEmailData(null, wrongAccountTemplate, theWrongAccountTemplateData, [ - ...textsStandard, - { - templPropName: 'subject', - isContaining: false, - text: subject, - }, - englishHint, - actionUrl, - enContent, - deContent, - supportUrl, - ]) - }) - }) - }) - - describe('notificationTemplate', () => { - beforeEach(() => { - actionUrl = new URL('/notifications', CONFIG.CLIENT_URI).toString() - name = notificationTemplateData('en').variables.notification.to.name - settingsUrl = new URL('/settings/notifications', CONFIG.CLIENT_URI) - }) - - describe('en', () => { - it('e-mail is build with all data', () => { - const subject = `${CONFIG.APPLICATION_NAME} – Notification` - const content = 'You received at least one notification. Click on this button to view them:' - testEmailData(null, notificationTemplate, notificationTemplateData('en'), [ - ...textsStandard, - { - templPropName: 'subject', - isContaining: false, - text: subject, - }, - actionUrl, - name, - content, - settingsUrl, - ]) - }) - }) - - describe('de', () => { - it('e-mail is build with all data', async () => { - const subject = `${CONFIG.APPLICATION_NAME} – Benachrichtigung` - const content = `Du hast mindestens eine Benachrichtigung erhalten. Klick auf diesen Button, um sie anzusehen:` - testEmailData(null, notificationTemplate, notificationTemplateData('de'), [ - ...textsStandard, - { - templPropName: 'subject', - isContaining: false, - text: subject, - }, - actionUrl, - name, - content, - settingsUrl, - ]) - }) - }) - }) -}) diff --git a/backend/src/middleware/helpers/email/templateBuilder.ts b/backend/src/middleware/helpers/email/templateBuilder.ts deleted file mode 100644 index 78d7a9bf9..000000000 --- a/backend/src/middleware/helpers/email/templateBuilder.ts +++ /dev/null @@ -1,113 +0,0 @@ -import mustache from 'mustache' -import CONFIG from '../../../config' -import metadata from '../../../config/metadata' -import logosWebapp from '../../../config/logos' - -import * as templates from './templates' -import * as templatesEN from './templates/en' -import * as templatesDE from './templates/de' - -const from = CONFIG.EMAIL_DEFAULT_SENDER -const welcomeImageUrl = new URL(logosWebapp.LOGO_WELCOME_PATH, CONFIG.CLIENT_URI) - -const defaultParams = { - welcomeImageUrl, - APPLICATION_NAME: CONFIG.APPLICATION_NAME, - ORGANIZATION_NAME: metadata.ORGANIZATION_NAME, - ORGANIZATION_URL: CONFIG.ORGANIZATION_URL, - supportUrl: CONFIG.SUPPORT_URL, -} -const englishHint = 'English version below!' - -export const signupTemplate = ({ email, variables: { nonce, inviteCode = null } }) => { - const subject = `Willkommen, Bienvenue, Welcome to ${CONFIG.APPLICATION_NAME}!` - // dev format example: http://localhost:3000/registration?method=invite-mail&email=huss%40pjannto.com&nonce=64853 - const actionUrl = new URL('/registration', CONFIG.CLIENT_URI) - actionUrl.searchParams.set('email', email) - actionUrl.searchParams.set('nonce', nonce) - if (inviteCode) { - actionUrl.searchParams.set('inviteCode', inviteCode) - actionUrl.searchParams.set('method', 'invite-code') - } else { - actionUrl.searchParams.set('method', 'invite-mail') - } - const renderParams = { ...defaultParams, englishHint, actionUrl, nonce, subject } - - return { - from, - to: email, - subject, - html: mustache.render(templates.layout, renderParams, { content: templates.signup }), - } -} - -export const emailVerificationTemplate = ({ email, variables: { nonce, name } }) => { - const subject = 'Neue E-Mail Adresse | New E-Mail Address' - const actionUrl = new URL('/settings/my-email-address/verify', CONFIG.CLIENT_URI) - actionUrl.searchParams.set('email', email) - actionUrl.searchParams.set('nonce', nonce) - const renderParams = { ...defaultParams, englishHint, actionUrl, name, nonce, subject } - - return { - from, - to: email, - subject, - html: mustache.render(templates.layout, renderParams, { content: templates.emailVerification }), - } -} - -export const resetPasswordTemplate = ({ email, variables: { nonce, name } }) => { - const subject = 'Neues Passwort | Reset Password' - const actionUrl = new URL('/password-reset/change-password', CONFIG.CLIENT_URI) - actionUrl.searchParams.set('nonce', nonce) - actionUrl.searchParams.set('email', email) - const renderParams = { ...defaultParams, englishHint, actionUrl, name, nonce, subject } - - return { - from, - to: email, - subject, - html: mustache.render(templates.layout, renderParams, { content: templates.passwordReset }), - } -} - -export const wrongAccountTemplate = ({ email, _variables = {} }) => { - const subject = 'Falsche Mailadresse? | Wrong E-mail?' - const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI) - const renderParams = { ...defaultParams, englishHint, actionUrl } - - return { - from, - to: email, - subject, - html: mustache.render(templates.layout, renderParams, { content: templates.wrongAccount }), - } -} - -export const notificationTemplate = ({ email, variables: { notification } }) => { - const actionUrl = new URL('/notifications', CONFIG.CLIENT_URI) - const settingsUrl = new URL('/settings/notifications', CONFIG.CLIENT_URI) - const renderParams = { ...defaultParams, name: notification.to.name, settingsUrl, actionUrl } - let content - switch (notification.to.locale) { - case 'de': - content = templatesDE.notification - break - case 'en': - content = templatesEN.notification - break - - default: - content = templatesEN.notification - break - } - const subjectUnrendered = content.split('\n')[0].split('"')[1] - const subject = mustache.render(subjectUnrendered, renderParams, {}) - - return { - from, - to: email, - subject, - html: mustache.render(templates.layout, renderParams, { content }), - } -} diff --git a/backend/src/middleware/helpers/email/templates/de/index.ts b/backend/src/middleware/helpers/email/templates/de/index.ts deleted file mode 100644 index 0f9d13c36..000000000 --- a/backend/src/middleware/helpers/email/templates/de/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import fs from 'fs' -import path from 'path' - -const readFile = (fileName) => fs.readFileSync(path.join(__dirname, fileName), 'utf-8') - -export const notification = readFile('./notification.html') diff --git a/backend/src/middleware/helpers/email/templates/de/notification.html b/backend/src/middleware/helpers/email/templates/de/notification.html deleted file mode 100644 index a54943310..000000000 --- a/backend/src/middleware/helpers/email/templates/de/notification.html +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/backend/src/middleware/helpers/email/templates/emailVerification.html b/backend/src/middleware/helpers/email/templates/emailVerification.html deleted file mode 100644 index 35ce27e5a..000000000 --- a/backend/src/middleware/helpers/email/templates/emailVerification.html +++ /dev/null @@ -1,186 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/backend/src/middleware/helpers/email/templates/en/index.ts b/backend/src/middleware/helpers/email/templates/en/index.ts deleted file mode 100644 index 0f9d13c36..000000000 --- a/backend/src/middleware/helpers/email/templates/en/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import fs from 'fs' -import path from 'path' - -const readFile = (fileName) => fs.readFileSync(path.join(__dirname, fileName), 'utf-8') - -export const notification = readFile('./notification.html') diff --git a/backend/src/middleware/helpers/email/templates/en/notification.html b/backend/src/middleware/helpers/email/templates/en/notification.html deleted file mode 100644 index 168b21864..000000000 --- a/backend/src/middleware/helpers/email/templates/en/notification.html +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/backend/src/middleware/helpers/email/templates/index.ts b/backend/src/middleware/helpers/email/templates/index.ts deleted file mode 100644 index b8ae01bdb..000000000 --- a/backend/src/middleware/helpers/email/templates/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import fs from 'fs' -import path from 'path' - -const readFile = (fileName) => fs.readFileSync(path.join(__dirname, fileName), 'utf-8') - -export const signup = readFile('./signup.html') -export const passwordReset = readFile('./resetPassword.html') -export const wrongAccount = readFile('./wrongAccount.html') -export const emailVerification = readFile('./emailVerification.html') - -export const layout = readFile('./layout.html') diff --git a/backend/src/middleware/helpers/email/templates/layout.html b/backend/src/middleware/helpers/email/templates/layout.html deleted file mode 100644 index 0c68d6309..000000000 --- a/backend/src/middleware/helpers/email/templates/layout.html +++ /dev/null @@ -1,197 +0,0 @@ - - - - - - - - - - {{ subject }} - - - - - - - - - - - - - - - - - -
- - - - - -
- - - diff --git a/backend/src/middleware/helpers/email/templates/resetPassword.html b/backend/src/middleware/helpers/email/templates/resetPassword.html deleted file mode 100644 index 43c45455e..000000000 --- a/backend/src/middleware/helpers/email/templates/resetPassword.html +++ /dev/null @@ -1,185 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/backend/src/middleware/helpers/email/templates/signup.html b/backend/src/middleware/helpers/email/templates/signup.html deleted file mode 100644 index 4bf17fd61..000000000 --- a/backend/src/middleware/helpers/email/templates/signup.html +++ /dev/null @@ -1,214 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/backend/src/middleware/helpers/email/templates/wrongAccount.html b/backend/src/middleware/helpers/email/templates/wrongAccount.html deleted file mode 100644 index e8f71e9ea..000000000 --- a/backend/src/middleware/helpers/email/templates/wrongAccount.html +++ /dev/null @@ -1,185 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/backend/src/middleware/helpers/isUserOnline.spec.ts b/backend/src/middleware/helpers/isUserOnline.spec.ts new file mode 100644 index 000000000..de4c840e3 --- /dev/null +++ b/backend/src/middleware/helpers/isUserOnline.spec.ts @@ -0,0 +1,45 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import { isUserOnline } from './isUserOnline' + +let user + +describe('isUserOnline', () => { + beforeEach(() => { + user = { + lastActiveAt: null, + awaySince: null, + lastOnlineStatus: null, + } + }) + describe('user has lastOnlineStatus `online`', () => { + it('returns true if he was active within the last 90 seconds', () => { + user.lastOnlineStatus = 'online' + user.lastActiveAt = new Date() + expect(isUserOnline(user)).toBe(true) + }) + it('returns false if he was not active within the last 90 seconds', () => { + user.lastOnlineStatus = 'online' + user.lastActiveAt = new Date().getTime() - 90001 + expect(isUserOnline(user)).toBe(false) + }) + }) + + describe('user has lastOnlineStatus `away`', () => { + it('returns true if he went away less then 180 seconds ago', () => { + user.lastOnlineStatus = 'away' + user.awaySince = new Date() + expect(isUserOnline(user)).toBe(true) + }) + it('returns false if he went away more then 180 seconds ago', () => { + user.lastOnlineStatus = 'away' + user.awaySince = new Date().getTime() - 180001 + expect(isUserOnline(user)).toBe(false) + }) + }) + + describe('user is freshly created and has never logged in', () => { + it('returns false', () => { + expect(isUserOnline(user)).toBe(false) + }) + }) +}) diff --git a/backend/src/middleware/helpers/isUserOnline.ts b/backend/src/middleware/helpers/isUserOnline.ts new file mode 100644 index 000000000..b2d0b601f --- /dev/null +++ b/backend/src/middleware/helpers/isUserOnline.ts @@ -0,0 +1,19 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +export const isUserOnline = (user) => { + // Is Recipient considered online + const lastActive = new Date(user.lastActiveAt).getTime() + const awaySince = new Date(user.awaySince).getTime() + const now = new Date().getTime() + const status = user.lastOnlineStatus + if ( + // online & last active less than 1.5min -> online + (status === 'online' && now - lastActive < 90000) || + // away for less then 3min -> online + (status === 'away' && now - awaySince < 180000) + ) { + return true + } + return false +} diff --git a/backend/src/middleware/includedFieldsMiddleware.ts b/backend/src/middleware/includedFieldsMiddleware.ts index fd95029b0..4b0ab1b1e 100644 --- a/backend/src/middleware/includedFieldsMiddleware.ts +++ b/backend/src/middleware/includedFieldsMiddleware.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import cloneDeep from 'lodash/cloneDeep' const _includeFieldsRecursively = (selectionSet, includedFields) => { diff --git a/backend/src/middleware/index.ts b/backend/src/middleware/index.ts index 08c872db7..b4824ae0e 100644 --- a/backend/src/middleware/index.ts +++ b/backend/src/middleware/index.ts @@ -1,71 +1,69 @@ -import { applyMiddleware } from 'graphql-middleware' -import CONFIG from './../config' -import softDelete from './softDelete/softDeleteMiddleware' -import sluggify from './sluggifyMiddleware' -import excerpt from './excerptMiddleware' -import xss from './xssMiddleware' -import permissions from './permissionsMiddleware' -import includedFields from './includedFieldsMiddleware' -import orderBy from './orderByMiddleware' -import validation from './validation/validationMiddleware' -import notifications from './notifications/notificationsMiddleware' -import hashtags from './hashtags/hashtagsMiddleware' -import login from './login/loginMiddleware' -import sentry from './sentryMiddleware' -import languages from './languages/languages' -import userInteractions from './userInteractions' +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { applyMiddleware, IMiddleware } from 'graphql-middleware' + +import CONFIG from '@config/index' + +// eslint-disable-next-line import/no-cycle +import brandingMiddlewares from './branding/brandingMiddlewares' +import categories from './categories' import chatMiddleware from './chatMiddleware' +import excerpt from './excerptMiddleware' +import hashtags from './hashtags/hashtagsMiddleware' +import includedFields from './includedFieldsMiddleware' +import languages from './languages/languages' +import login from './login/loginMiddleware' +import notifications from './notifications/notificationsMiddleware' +import orderBy from './orderByMiddleware' +// eslint-disable-next-line import/no-cycle +import permissions from './permissionsMiddleware' +import sentry from './sentryMiddleware' +import sluggify from './sluggifyMiddleware' +import softDelete from './softDelete/softDeleteMiddleware' +import userInteractions from './userInteractions' +import validation from './validation/validationMiddleware' +import xss from './xssMiddleware' + +export interface MiddlewareOrder { + order: number + name: string + middleware: IMiddleware +} + +const ocelotMiddlewares: MiddlewareOrder[] = [ + { order: -200, name: 'sentry', middleware: sentry }, + { order: -190, name: 'permissions', middleware: permissions }, + { order: -180, name: 'xss', middleware: xss }, + { order: -170, name: 'validation', middleware: validation }, + { order: -160, name: 'userInteractions', middleware: userInteractions }, + { order: -150, name: 'sluggify', middleware: sluggify }, + { order: -140, name: 'languages', middleware: languages }, + { order: -130, name: 'excerpt', middleware: excerpt }, + { order: -120, name: 'login', middleware: login }, + { order: -110, name: 'notifications', middleware: notifications }, + { order: -100, name: 'hashtags', middleware: hashtags }, + { order: -90, name: 'softDelete', middleware: softDelete }, + { order: -80, name: 'includedFields', middleware: includedFields }, + { order: -70, name: 'orderBy', middleware: orderBy }, + { order: -60, name: 'chatMiddleware', middleware: chatMiddleware }, + { order: -50, name: 'categories', middleware: categories }, +] export default (schema) => { - const middlewares = { - sentry, - permissions, - xss, - validation, - sluggify, - excerpt, - login, - notifications, - hashtags, - softDelete, - includedFields, - orderBy, - languages, - userInteractions, - chatMiddleware, + const middlewares = ocelotMiddlewares + .concat(brandingMiddlewares()) + .sort((a, b) => a.order - b.order) + + const filteredMiddlewares = middlewares.filter( + (middleware) => !CONFIG.DISABLED_MIDDLEWARES.includes(middleware.name), + ) + + // Warn if we filtered + if (middlewares.length < filteredMiddlewares.length) { + // eslint-disable-next-line no-console + console.log(`Warning: Disabled "${CONFIG.DISABLED_MIDDLEWARES.join(', ')}" middleware.`) } - let order = [ - 'sentry', - 'permissions', - 'xss', - // 'activityPub', disabled temporarily - 'validation', - 'userInteractions', - 'sluggify', - 'languages', - 'excerpt', - 'login', - 'notifications', - 'hashtags', - 'softDelete', - 'includedFields', - 'orderBy', - 'chatMiddleware', - ] - - // add permisions middleware at the first position (unless we're seeding) - if (CONFIG.DISABLED_MIDDLEWARES) { - const disabledMiddlewares = CONFIG.DISABLED_MIDDLEWARES.split(',') - order = order.filter((key) => { - if (disabledMiddlewares.includes(key)) { - /* eslint-disable-next-line no-console */ - console.log(`Warning: Disabled "${disabledMiddlewares}" middleware.`) - } - return !disabledMiddlewares.includes(key) - }) - } - - const appliedMiddlewares = order.map((key) => middlewares[key]) - return applyMiddleware(schema, ...appliedMiddlewares) + return applyMiddleware(schema, ...filteredMiddlewares.map((middleware) => middleware.middleware)) } diff --git a/backend/src/middleware/languages/languages.spec.ts b/backend/src/middleware/languages/languages.spec.ts index 8daa311e1..50e3a028f 100644 --- a/backend/src/middleware/languages/languages.spec.ts +++ b/backend/src/middleware/languages/languages.spec.ts @@ -1,8 +1,12 @@ -import Factory, { cleanDatabase } from '../../db/factories' -import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' +import gql from 'graphql-tag' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' let mutate let authenticatedUser @@ -28,7 +32,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) const createPostMutation = gql` diff --git a/backend/src/middleware/languages/languages.ts b/backend/src/middleware/languages/languages.ts index 3c043ceec..fb8c51a1f 100644 --- a/backend/src/middleware/languages/languages.ts +++ b/backend/src/middleware/languages/languages.ts @@ -1,5 +1,12 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/await-thenable */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import LanguageDetect from 'languagedetect' -import { removeHtmlTags } from '../helpers/cleanHtml' + +import { removeHtmlTags } from '@middleware/helpers/cleanHtml' const setPostLanguage = (text, defaultLanguage) => { const lngDetector = new LanguageDetect() diff --git a/backend/src/middleware/login/loginMiddleware.ts b/backend/src/middleware/login/loginMiddleware.ts index abf0d0b18..35f3df702 100644 --- a/backend/src/middleware/login/loginMiddleware.ts +++ b/backend/src/middleware/login/loginMiddleware.ts @@ -1,40 +1,42 @@ -import { sendMail } from '../helpers/email/sendMail' +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { - signupTemplate, - resetPasswordTemplate, - wrongAccountTemplate, - emailVerificationTemplate, -} from '../helpers/email/templateBuilder' + sendRegistrationMail, + sendEmailVerification, + sendResetPasswordMail, +} from '@src/emails/sendEmail' const sendSignupMail = async (resolve, root, args, context, resolveInfo) => { - const { inviteCode } = args + const { inviteCode, locale } = args const response = await resolve(root, args, context, resolveInfo) const { email, nonce } = response if (nonce) { // emails that already exist do not have a nonce - if (inviteCode) { - await sendMail(signupTemplate({ email, variables: { nonce, inviteCode } })) - } else { - await sendMail(signupTemplate({ email, variables: { nonce } })) - } + await sendRegistrationMail({ email, nonce, locale, inviteCode }) } delete response.nonce return response } const sendPasswordResetMail = async (resolve, root, args, context, resolveInfo) => { - const { email } = args + const { email, locale } = args const { email: userFound, nonce, name } = await resolve(root, args, context, resolveInfo) - const template = userFound ? resetPasswordTemplate : wrongAccountTemplate - await sendMail(template({ email, variables: { nonce, name } })) + if (userFound) { + await sendResetPasswordMail({ email, nonce, name, locale }) + } else { + // this is an antifeature allowing unauthenticated users to spam any email with wrong-email notifications + // await sendWrongEmail({ email, locale }) + } return true } const sendEmailVerificationMail = async (resolve, root, args, context, resolveInfo) => { const response = await resolve(root, args, context, resolveInfo) - const { email, nonce, name } = response + const { email, nonce, name, locale } = response if (nonce) { - await sendMail(emailVerificationTemplate({ email, variables: { nonce, name } })) + await sendEmailVerification({ email, nonce, name, locale }) } delete response.nonce return response diff --git a/backend/src/middleware/notifications/mentions/extractMentionedUsers.ts b/backend/src/middleware/notifications/mentions/extractMentionedUsers.ts index ff80bb77a..ccb7dd1be 100644 --- a/backend/src/middleware/notifications/mentions/extractMentionedUsers.ts +++ b/backend/src/middleware/notifications/mentions/extractMentionedUsers.ts @@ -1,8 +1,9 @@ -import * as cheerio from 'cheerio' +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { load } from 'cheerio' export default (content?) => { if (!content) return [] - const $ = cheerio.load(content) + const $ = load(content) const userIds = $('a.mention[data-mention-id]') .map((_, el) => { return $(el).attr('data-mention-id') diff --git a/backend/src/middleware/notifications/notificationsMiddleware.emails.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.emails.spec.ts new file mode 100644 index 000000000..27aeb8cf4 --- /dev/null +++ b/backend/src/middleware/notifications/notificationsMiddleware.emails.spec.ts @@ -0,0 +1,766 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +import { ApolloServer } from 'apollo-server-express' +import { createTestClient } from 'apollo-server-testing' +import gql from 'graphql-tag' + +import databaseContext from '@context/database' +import Factory, { cleanDatabase } from '@db/factories' +import { createGroupMutation } from '@graphql/queries/createGroupMutation' +import { joinGroupMutation } from '@graphql/queries/joinGroupMutation' +import CONFIG from '@src/config' +import createServer, { getContext } from '@src/server' + +CONFIG.CATEGORIES_ACTIVE = false + +const sendNotificationMailMock: (notification) => void = jest.fn() +jest.mock('@src/emails/sendEmail', () => ({ + sendNotificationMail: (notification) => sendNotificationMailMock(notification), +})) + +let query, mutate, authenticatedUser, emaillessMember + +let postAuthor, groupMember + +const mentionString = ` + @group-member + @email-less-member` + +const createPostMutation = gql` + mutation ($id: ID, $title: String!, $content: String!, $groupId: ID) { + CreatePost(id: $id, title: $title, content: $content, groupId: $groupId) { + id + title + content + } + } +` + +const createCommentMutation = gql` + mutation ($id: ID, $postId: ID!, $content: String!) { + CreateComment(id: $id, postId: $postId, content: $content) { + id + content + } + } +` + +const notificationQuery = gql` + query ($read: Boolean) { + notifications(read: $read, orderBy: updatedAt_desc) { + read + reason + createdAt + relatedUser { + id + } + from { + __typename + ... on Post { + id + content + } + ... on Comment { + id + content + } + ... on Group { + id + } + } + } + } +` + +const followUserMutation = gql` + mutation ($id: ID!) { + followUser(id: $id) { + id + } + } +` + +const markAllAsRead = async () => + mutate({ + mutation: gql` + mutation { + markAllAsRead { + id + } + } + `, + }) + +const database = databaseContext() + +let server: ApolloServer + +beforeAll(async () => { + await cleanDatabase() + + const contextUser = async (_req) => authenticatedUser + const context = getContext({ user: contextUser, database }) + + server = createServer({ context }).server + + const createTestClientResult = createTestClient(server) + query = createTestClientResult.query + mutate = createTestClientResult.mutate +}) + +afterAll(async () => { + await cleanDatabase() + void server.stop() + void database.driver.close() + database.neode.close() +}) + +describe('emails sent for notifications', () => { + beforeEach(async () => { + postAuthor = await Factory.build( + 'user', + { + id: 'post-author', + name: 'Post Author', + slug: 'post-author', + }, + { + email: 'test@example.org', + password: '1234', + }, + ) + groupMember = await Factory.build( + 'user', + { + id: 'group-member', + name: 'Group Member', + slug: 'group-member', + }, + { + email: 'group.member@example.org', + password: '1234', + }, + ) + emaillessMember = await database.neode.create('User', { + id: 'email-less-member', + name: 'Email-less Member', + slug: 'email-less-member', + }) + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createGroupMutation(), + variables: { + id: 'public-group', + name: 'A public group', + description: 'A public group to test the notifications of mentions', + groupType: 'public', + actionRadius: 'national', + }, + }) + authenticatedUser = await groupMember.toJson() + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'public-group', + userId: 'group-member', + }, + }) + await mutate({ + mutation: followUserMutation, + variables: { id: 'post-author' }, + }) + authenticatedUser = await emaillessMember.toJson() + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'public-group', + userId: 'group-member', + }, + }) + await mutate({ + mutation: followUserMutation, + variables: { id: 'post-author' }, + }) + }) + + afterEach(async () => { + await cleanDatabase() + }) + + describe('handleContentDataOfPost', () => { + describe('post-author posts into group and mentions following group-member', () => { + describe('all email notification settings are true', () => { + beforeEach(async () => { + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post', + title: 'This is the post', + content: `Hello, ${mentionString}, my trusty followers.`, + groupId: 'public-group', + }, + }) + }) + + it('sends only one email', () => { + expect(sendNotificationMailMock).toHaveBeenCalledTimes(1) + expect(sendNotificationMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + reason: 'mentioned_in_post', + email: 'group.member@example.org', + }), + ) + }) + + it('sends 3 notifications', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'post_in_group', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'followed_user_posted', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'mentioned_in_post', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + }) + + describe('email notification for mention in post is false', () => { + beforeEach(async () => { + jest.clearAllMocks() + await groupMember.update({ emailNotificationsMention: false }) + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post', + title: 'This is the post', + content: `Hello, ${mentionString}, my trusty followers.`, + groupId: 'public-group', + }, + }) + }) + + it('sends only one email', () => { + expect(sendNotificationMailMock).toHaveBeenCalledTimes(1) + expect(sendNotificationMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + reason: 'followed_user_posted', + email: 'group.member@example.org', + }), + ) + }) + + it('sends 3 notifications', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'post_in_group', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'followed_user_posted', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'mentioned_in_post', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + }) + + describe('email notification for mention in post and followed users is false', () => { + beforeEach(async () => { + jest.clearAllMocks() + await groupMember.update({ emailNotificationsMention: false }) + await groupMember.update({ emailNotificationsFollowingUsers: false }) + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post', + title: 'This is the post', + content: `Hello, ${mentionString}, my trusty followers.`, + groupId: 'public-group', + }, + }) + }) + + it('sends only one email', () => { + expect(sendNotificationMailMock).toHaveBeenCalledTimes(1) + expect(sendNotificationMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + reason: 'post_in_group', + email: 'group.member@example.org', + }), + ) + }) + + it('sends 3 notifications', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'post_in_group', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'followed_user_posted', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'mentioned_in_post', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + }) + + describe('all relevant email notifications are false', () => { + beforeEach(async () => { + jest.clearAllMocks() + await groupMember.update({ emailNotificationsMention: false }) + await groupMember.update({ emailNotificationsFollowingUsers: false }) + await groupMember.update({ emailNotificationsPostInGroup: false }) + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post', + title: 'This is the post', + content: `Hello, ${mentionString}, my trusty followers.`, + groupId: 'public-group', + }, + }) + }) + + it('sends NO email', () => { + expect(sendNotificationMailMock).not.toHaveBeenCalled() + }) + + it('sends 3 notifications', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'post_in_group', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'followed_user_posted', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'post', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'mentioned_in_post', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + }) + }) + }) + + describe('handleContentDataOfComment', () => { + describe('user comments post and author responds with in a comment and mentions the user', () => { + describe('all email notification settings are true', () => { + beforeEach(async () => { + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post', + title: 'This is the post', + content: `Hello, ${mentionString}, my trusty followers.`, + groupId: 'public-group', + }, + }) + authenticatedUser = await groupMember.toJson() + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'comment', + content: `Hello, my beloved author.`, + postId: 'post', + }, + }) + await markAllAsRead() + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'comment-2', + content: `Hello, ${mentionString}, my trusty followers.`, + postId: 'post', + }, + }) + }) + + it('sends only one email', () => { + expect(sendNotificationMailMock).toHaveBeenCalledTimes(1) + expect(sendNotificationMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + reason: 'mentioned_in_comment', + email: 'group.member@example.org', + }), + ) + }) + + it('sends 2 notifications', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Comment', + id: 'comment-2', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'commented_on_post', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Comment', + id: 'comment-2', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'mentioned_in_comment', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + }) + + describe('email notification commented on post is false', () => { + beforeEach(async () => { + await groupMember.update({ emailNotificationsCommentOnObservedPost: false }) + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post', + title: 'This is the post', + content: `Hello, ${mentionString}, my trusty followers.`, + groupId: 'public-group', + }, + }) + authenticatedUser = await groupMember.toJson() + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'comment', + content: `Hello, my beloved author.`, + postId: 'post', + }, + }) + await markAllAsRead() + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'comment-2', + content: `Hello, ${mentionString}, my trusty followers.`, + postId: 'post', + }, + }) + }) + + it('sends only one email', () => { + expect(sendNotificationMailMock).toHaveBeenCalledTimes(1) + expect(sendNotificationMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + reason: 'mentioned_in_comment', + email: 'group.member@example.org', + }), + ) + }) + + it('sends 2 notifications', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Comment', + id: 'comment-2', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'commented_on_post', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Comment', + id: 'comment-2', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'mentioned_in_comment', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + }) + + describe('all relevant email notifications are false', () => { + beforeEach(async () => { + await groupMember.update({ emailNotificationsCommentOnObservedPost: false }) + await groupMember.update({ emailNotificationsMention: false }) + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post', + title: 'This is the post', + content: `Hello, ${mentionString}, my trusty followers.`, + groupId: 'public-group', + }, + }) + authenticatedUser = await groupMember.toJson() + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'comment', + content: `Hello, my beloved author.`, + postId: 'post', + }, + }) + await markAllAsRead() + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'comment-2', + content: `Hello, ${mentionString}, my trusty followers.`, + postId: 'post', + }, + }) + }) + + it('sends NO email', () => { + expect(sendNotificationMailMock).not.toHaveBeenCalled() + }) + + it('sends 2 notifications', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Comment', + id: 'comment-2', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'commented_on_post', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Comment', + id: 'comment-2', + content: + 'Hello,
@group-member
@email-less-member, my trusty followers.', + }, + read: false, + reason: 'mentioned_in_comment', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + }) + }) + }) +}) diff --git a/backend/src/middleware/notifications/notificationsMiddleware.followed-users.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.followed-users.spec.ts new file mode 100644 index 000000000..3bb0d48e3 --- /dev/null +++ b/backend/src/middleware/notifications/notificationsMiddleware.followed-users.spec.ts @@ -0,0 +1,493 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +import { ApolloServer } from 'apollo-server-express' +import { createTestClient } from 'apollo-server-testing' +import gql from 'graphql-tag' + +import databaseContext from '@context/database' +import Factory, { cleanDatabase } from '@db/factories' +import { createGroupMutation } from '@graphql/queries/createGroupMutation' +import CONFIG from '@src/config' +import createServer, { getContext } from '@src/server' + +CONFIG.CATEGORIES_ACTIVE = false + +const sendNotificationMailMock: (notification) => void = jest.fn() +jest.mock('@src/emails/sendEmail', () => ({ + sendNotificationMail: (notification) => sendNotificationMailMock(notification), +})) + +let query, mutate, authenticatedUser + +let postAuthor, firstFollower, secondFollower, thirdFollower, emaillessFollower + +const createPostMutation = gql` + mutation ($id: ID, $title: String!, $content: String!, $groupId: ID) { + CreatePost(id: $id, title: $title, content: $content, groupId: $groupId) { + id + title + content + } + } +` + +const notificationQuery = gql` + query ($read: Boolean) { + notifications(read: $read, orderBy: updatedAt_desc) { + read + reason + createdAt + relatedUser { + id + } + from { + __typename + ... on Post { + id + content + } + ... on Comment { + id + content + } + ... on Group { + id + } + } + } + } +` + +const followUserMutation = gql` + mutation ($id: ID!) { + followUser(id: $id) { + id + } + } +` + +const database = databaseContext() + +let server: ApolloServer + +beforeAll(async () => { + await cleanDatabase() + + // eslint-disable-next-line @typescript-eslint/require-await + const contextUser = async (_req) => authenticatedUser + const context = getContext({ user: contextUser, database }) + + server = createServer({ context }).server + + const createTestClientResult = createTestClient(server) + query = createTestClientResult.query + mutate = createTestClientResult.mutate +}) + +afterAll(async () => { + await cleanDatabase() + void server.stop() + void database.driver.close() + database.neode.close() +}) + +describe('following users notifications', () => { + beforeAll(async () => { + postAuthor = await Factory.build( + 'user', + { + id: 'post-author', + name: 'Post Author', + slug: 'post-author', + }, + { + email: 'post-author@example.org', + password: '1234', + }, + ) + firstFollower = await Factory.build( + 'user', + { + id: 'first-follower', + name: 'First Follower', + slug: 'first-follower', + }, + { + email: 'first-follower@example.org', + password: '1234', + }, + ) + secondFollower = await Factory.build( + 'user', + { + id: 'second-follower', + name: 'Second Follower', + slug: 'second-follower', + }, + { + email: 'second-follower@example.org', + password: '1234', + }, + ) + thirdFollower = await Factory.build( + 'user', + { + id: 'third-follower', + name: 'Third Follower', + slug: 'third-follower', + }, + { + email: 'third-follower@example.org', + password: '1234', + }, + ) + emaillessFollower = await database.neode.create('User', { + id: 'email-less-follower', + name: 'Email-less Follower', + slug: 'email-less-follower', + }) + await secondFollower.update({ emailNotificationsFollowingUsers: false }) + authenticatedUser = await firstFollower.toJson() + await mutate({ + mutation: followUserMutation, + variables: { id: 'post-author' }, + }) + authenticatedUser = await secondFollower.toJson() + await mutate({ + mutation: followUserMutation, + variables: { id: 'post-author' }, + }) + authenticatedUser = await thirdFollower.toJson() + await mutate({ + mutation: followUserMutation, + variables: { id: 'post-author' }, + }) + authenticatedUser = await emaillessFollower.toJson() + await mutate({ + mutation: followUserMutation, + variables: { id: 'post-author' }, + }) + jest.clearAllMocks() + }) + + describe('the followed user writes a post', () => { + beforeAll(async () => { + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post', + title: 'This is the post', + content: 'This is the content of the post', + }, + }) + }) + + it('sends NO notification to the post author', async () => { + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends notification to the first follower', async () => { + authenticatedUser = await firstFollower.toJson() + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + from: { + __typename: 'Post', + id: 'post', + }, + read: false, + reason: 'followed_user_posted', + }, + ], + }, + errors: undefined, + }) + }) + + it('sends notification to the second follower', async () => { + authenticatedUser = await secondFollower.toJson() + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + from: { + __typename: 'Post', + id: 'post', + }, + read: false, + reason: 'followed_user_posted', + }, + ], + }, + errors: undefined, + }) + }) + + it('sends notification to the email-less follower', async () => { + authenticatedUser = await emaillessFollower.toJson() + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + from: { + __typename: 'Post', + id: 'post', + }, + read: false, + reason: 'followed_user_posted', + }, + ], + }, + errors: undefined, + }) + }) + + it('sends only two emails, as second follower has emails disabled and email-less follower has no email', () => { + expect(sendNotificationMailMock).toHaveBeenCalledTimes(2) + expect(sendNotificationMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + email: 'first-follower@example.org', + reason: 'followed_user_posted', + }), + ) + expect(sendNotificationMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + email: 'third-follower@example.org', + reason: 'followed_user_posted', + }), + ) + }) + }) + + describe('followed user posts in public group', () => { + beforeAll(async () => { + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createGroupMutation(), + variables: { + id: 'g-1', + name: 'A group', + description: 'A group to test the follow user notification', + groupType: 'public', + actionRadius: 'national', + }, + }) + await mutate({ + mutation: createPostMutation, + variables: { + id: 'group-post', + title: 'This is the post in the group', + content: 'This is the content of the post in the group', + groupId: 'g-1', + }, + }) + }) + + it('sends NO notification to the post author', async () => { + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends a notification to the first follower', async () => { + authenticatedUser = await firstFollower.toJson() + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + from: { + __typename: 'Post', + id: 'group-post', + }, + read: false, + reason: 'followed_user_posted', + }, + { + from: { + __typename: 'Post', + id: 'post', + }, + read: false, + reason: 'followed_user_posted', + }, + ], + }, + errors: undefined, + }) + }) + }) + + describe('followed user posts in closed group', () => { + beforeAll(async () => { + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createGroupMutation(), + variables: { + id: 'g-2', + name: 'A closed group', + description: 'A group to test the follow user notification', + groupType: 'closed', + actionRadius: 'national', + }, + }) + await mutate({ + mutation: createPostMutation, + variables: { + id: 'closed-group-post', + title: 'This is the post in the closed group', + content: 'This is the content of the post in the closed group', + groupId: 'g-2', + }, + }) + }) + + it('sends NO notification to the post author', async () => { + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends NO notification to the first follower', async () => { + authenticatedUser = await firstFollower.toJson() + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + from: { + __typename: 'Post', + id: 'group-post', + }, + read: false, + reason: 'followed_user_posted', + }, + { + from: { + __typename: 'Post', + id: 'post', + }, + read: false, + reason: 'followed_user_posted', + }, + ], + }, + errors: undefined, + }) + }) + }) + + describe('followed user posts in hidden group', () => { + beforeAll(async () => { + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createGroupMutation(), + variables: { + id: 'g-3', + name: 'A hidden group', + description: 'A hidden group to test the follow user notification', + groupType: 'hidden', + actionRadius: 'national', + }, + }) + await mutate({ + mutation: createPostMutation, + variables: { + id: 'hidden-group-post', + title: 'This is the post in the hidden group', + content: 'This is the content of the post in the hidden group', + groupId: 'g-3', + }, + }) + }) + + it('sends NO notification to the post author', async () => { + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends NO notification to the first follower', async () => { + authenticatedUser = await firstFollower.toJson() + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + from: { + __typename: 'Post', + id: 'group-post', + }, + read: false, + reason: 'followed_user_posted', + }, + { + from: { + __typename: 'Post', + id: 'post', + }, + read: false, + reason: 'followed_user_posted', + }, + ], + }, + errors: undefined, + }) + }) + }) +}) diff --git a/backend/src/middleware/notifications/notificationsMiddleware.mentions-in-groups.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.mentions-in-groups.spec.ts new file mode 100644 index 000000000..9eb26e57f --- /dev/null +++ b/backend/src/middleware/notifications/notificationsMiddleware.mentions-in-groups.spec.ts @@ -0,0 +1,913 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +import { ApolloServer } from 'apollo-server-express' +import { createTestClient } from 'apollo-server-testing' +import gql from 'graphql-tag' + +import databaseContext from '@context/database' +import Factory, { cleanDatabase } from '@db/factories' +import { changeGroupMemberRoleMutation } from '@graphql/queries/changeGroupMemberRoleMutation' +import { createGroupMutation } from '@graphql/queries/createGroupMutation' +import { joinGroupMutation } from '@graphql/queries/joinGroupMutation' +import CONFIG from '@src/config' +import createServer, { getContext } from '@src/server' + +CONFIG.CATEGORIES_ACTIVE = false + +const sendNotificationMailMock: (notification) => void = jest.fn() +jest.mock('@src/emails/sendEmail', () => ({ + sendNotificationMail: (notification) => sendNotificationMailMock(notification), +})) + +let query, mutate, authenticatedUser + +let postAuthor, groupMember, pendingMember, noMember, emaillessMember + +const mentionString = ` + @no-member + @pending-member + @group-member. + @email-less-member. +` + +const createPostMutation = gql` + mutation ($id: ID, $title: String!, $content: String!, $groupId: ID) { + CreatePost(id: $id, title: $title, content: $content, groupId: $groupId) { + id + title + content + } + } +` + +const createCommentMutation = gql` + mutation ($id: ID, $postId: ID!, $commentContent: String!) { + CreateComment(id: $id, postId: $postId, content: $commentContent) { + id + content + } + } +` + +const notificationQuery = gql` + query ($read: Boolean) { + notifications(read: $read, orderBy: updatedAt_desc) { + read + reason + createdAt + relatedUser { + id + } + from { + __typename + ... on Post { + id + content + } + ... on Comment { + id + content + } + ... on Group { + id + } + } + } + } +` + +const markAllAsRead = async () => + mutate({ + mutation: gql` + mutation { + markAllAsRead { + id + } + } + `, + }) + +const database = databaseContext() + +let server: ApolloServer + +beforeAll(async () => { + await cleanDatabase() + + const contextUser = async (_req) => authenticatedUser + const context = getContext({ user: contextUser, database }) + + server = createServer({ context }).server + + const createTestClientResult = createTestClient(server) + query = createTestClientResult.query + mutate = createTestClientResult.mutate +}) + +afterAll(async () => { + await cleanDatabase() + void server.stop() + void database.driver.close() + database.neode.close() +}) + +describe('mentions in groups', () => { + beforeEach(async () => { + postAuthor = await Factory.build( + 'user', + { + id: 'post-author', + name: 'Post Author', + slug: 'post-author', + }, + { + email: 'post.author@example.org', + password: '1234', + }, + ) + groupMember = await Factory.build( + 'user', + { + id: 'group-member', + name: 'Group Member', + slug: 'group-member', + }, + { + email: 'group.member@example.org', + password: '1234', + }, + ) + pendingMember = await Factory.build( + 'user', + { + id: 'pending-member', + name: 'Pending Member', + slug: 'pending-member', + }, + { + email: 'pending.member@example.org', + password: '1234', + }, + ) + noMember = await Factory.build( + 'user', + { + id: 'no-member', + name: 'No Member', + slug: 'no-member', + }, + { + email: 'no.member@example.org', + password: '1234', + }, + ) + emaillessMember = await database.neode.create('User', { + id: 'email-less-member', + name: 'Email-less Member', + slug: 'email-less-member', + }) + + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createGroupMutation(), + variables: { + id: 'public-group', + name: 'A public group', + description: 'A public group to test the notifications of mentions', + groupType: 'public', + actionRadius: 'national', + }, + }) + await mutate({ + mutation: createGroupMutation(), + variables: { + id: 'closed-group', + name: 'A closed group', + description: 'A closed group to test the notifications of mentions', + groupType: 'closed', + actionRadius: 'national', + }, + }) + await mutate({ + mutation: createGroupMutation(), + variables: { + id: 'hidden-group', + name: 'A hidden group', + description: 'A hidden group to test the notifications of mentions', + groupType: 'hidden', + actionRadius: 'national', + }, + }) + authenticatedUser = await groupMember.toJson() + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'public-group', + userId: 'group-member', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'closed-group', + userId: 'group-member', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'group-member', + }, + }) + authenticatedUser = await pendingMember.toJson() + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'public-group', + userId: 'pending-member', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'closed-group', + userId: 'pending-member', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'pending-member', + }, + }) + authenticatedUser = await emaillessMember.toJson() + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'public-group', + userId: 'group-member', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'closed-group', + userId: 'group-member', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'group-member', + }, + }) + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'closed-group', + userId: 'group-member', + roleInGroup: 'usual', + }, + }) + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'hidden-group', + userId: 'group-member', + roleInGroup: 'usual', + }, + }) + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'closed-group', + userId: 'email-less-member', + roleInGroup: 'usual', + }, + }) + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'hidden-group', + userId: 'email-less-member', + roleInGroup: 'usual', + }, + }) + authenticatedUser = await groupMember.toJson() + await markAllAsRead() + authenticatedUser = await emaillessMember.toJson() + await markAllAsRead() + }) + + afterEach(async () => { + await cleanDatabase() + }) + + describe('post in public group', () => { + beforeEach(async () => { + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'public-post', + title: 'This is the post in the public group', + content: `Hey ${mentionString}! Please read this`, + groupId: 'public-group', + }, + }) + }) + + it('sends a notification to the no member', async () => { + authenticatedUser = await noMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + from: { + __typename: 'Post', + id: 'public-post', + }, + read: false, + reason: 'mentioned_in_post', + }, + ], + }, + errors: undefined, + }) + }) + + it('sends a notification to the group member', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'public-post', + content: + 'Hey
@no-member
@pending-member
@group-member.
@email-less-member.
! Please read this', + }, + read: false, + reason: 'post_in_group', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'public-post', + content: + 'Hey
@no-member
@pending-member
@group-member.
@email-less-member.
! Please read this', + }, + read: false, + reason: 'mentioned_in_post', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + + it('sends only 3 emails, one for each user with an email', () => { + expect(sendNotificationMailMock).toHaveBeenCalledTimes(3) + expect(sendNotificationMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + email: 'group.member@example.org', + reason: 'mentioned_in_post', + }), + ) + expect(sendNotificationMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + email: 'no.member@example.org', + reason: 'mentioned_in_post', + }), + ) + expect(sendNotificationMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + email: 'pending.member@example.org', + reason: 'mentioned_in_post', + }), + ) + }) + }) + + describe('post in closed group', () => { + beforeEach(async () => { + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'closed-post', + title: 'This is the post in the closed group', + content: `Hey members ${mentionString}! Please read this`, + groupId: 'closed-group', + }, + }) + }) + + it('sends NO notification to the no member', async () => { + authenticatedUser = await noMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends NO notification to the pending member', async () => { + authenticatedUser = await pendingMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends a notification to the group member', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'closed-post', + content: + 'Hey members
@no-member
@pending-member
@group-member.
@email-less-member.
! Please read this', + }, + read: false, + reason: 'post_in_group', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'closed-post', + content: + 'Hey members
@no-member
@pending-member
@group-member.
@email-less-member.
! Please read this', + }, + read: false, + reason: 'mentioned_in_post', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + + it('sends only 1 email', () => { + expect(sendNotificationMailMock).toHaveBeenCalledTimes(1) + expect(sendNotificationMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + email: 'group.member@example.org', + reason: 'mentioned_in_post', + }), + ) + }) + }) + + describe('post in hidden group', () => { + beforeEach(async () => { + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'hidden-post', + title: 'This is the post in the hidden group', + content: `Hey hiders ${mentionString}! Please read this`, + groupId: 'hidden-group', + }, + }) + }) + + it('sends NO notification to the no member', async () => { + authenticatedUser = await noMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends NO notification to the pending member', async () => { + authenticatedUser = await pendingMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends a notification to the group member', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'hidden-post', + content: + 'Hey hiders
@no-member
@pending-member
@group-member.
@email-less-member.
! Please read this', + }, + read: false, + reason: 'post_in_group', + relatedUser: null, + }, + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + id: 'hidden-post', + content: + 'Hey hiders
@no-member
@pending-member
@group-member.
@email-less-member.
! Please read this', + }, + read: false, + reason: 'mentioned_in_post', + relatedUser: null, + }, + ]), + }, + errors: undefined, + }) + }) + + it('sends only 1 email', () => { + expect(sendNotificationMailMock).toHaveBeenCalledTimes(1) + expect(sendNotificationMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + email: 'group.member@example.org', + reason: 'mentioned_in_post', + }), + ) + }) + }) + + describe('comments on group posts', () => { + describe('public group', () => { + beforeEach(async () => { + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'public-post', + title: 'This is the post in the public group', + content: `Some public content`, + groupId: 'public-group', + }, + }) + authenticatedUser = await groupMember.toJson() + await markAllAsRead() + authenticatedUser = await postAuthor.toJson() + jest.clearAllMocks() + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'public-comment', + postId: 'public-post', + commentContent: `Hey everyone ${mentionString}! Please read this`, + }, + }) + }) + + it('sends a notification to the no member', async () => { + authenticatedUser = await noMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + from: { + __typename: 'Comment', + id: 'public-comment', + }, + read: false, + reason: 'mentioned_in_comment', + }, + ], + }, + errors: undefined, + }) + }) + + it('sends a notification to the group member', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + from: { + __typename: 'Comment', + id: 'public-comment', + }, + read: false, + reason: 'mentioned_in_comment', + }, + ], + }, + errors: undefined, + }) + }) + + it('sends 2 emails', () => { + expect(sendNotificationMailMock).toHaveBeenCalledTimes(3) + expect(sendNotificationMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + email: 'group.member@example.org', + reason: 'mentioned_in_comment', + }), + ) + expect(sendNotificationMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + email: 'no.member@example.org', + reason: 'mentioned_in_comment', + }), + ) + }) + }) + + describe('closed group', () => { + beforeEach(async () => { + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'closed-post', + title: 'This is the post in the closed group', + content: `Some closed content`, + groupId: 'closed-group', + }, + }) + authenticatedUser = await groupMember.toJson() + await markAllAsRead() + authenticatedUser = await postAuthor.toJson() + jest.clearAllMocks() + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'closed-comment', + postId: 'closed-post', + commentContent: `Hey members ${mentionString}! Please read this`, + }, + }) + }) + + it('sends NO notification to the no member', async () => { + authenticatedUser = await noMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends NO notification to the pending member', async () => { + authenticatedUser = await pendingMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends a notification to the group member', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + from: { + __typename: 'Comment', + id: 'closed-comment', + }, + read: false, + reason: 'mentioned_in_comment', + }, + ], + }, + errors: undefined, + }) + }) + + it('sends 1 email', () => { + expect(sendNotificationMailMock).toHaveBeenCalledTimes(1) + expect(sendNotificationMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + email: 'group.member@example.org', + reason: 'mentioned_in_comment', + }), + ) + }) + }) + + describe('hidden group', () => { + beforeEach(async () => { + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'hidden-post', + title: 'This is the post in the hidden group', + content: `Some hidden content`, + groupId: 'hidden-group', + }, + }) + authenticatedUser = await groupMember.toJson() + await markAllAsRead() + authenticatedUser = await postAuthor.toJson() + jest.clearAllMocks() + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'hidden-comment', + postId: 'hidden-post', + commentContent: `Hey hiders ${mentionString}! Please read this`, + }, + }) + }) + + it('sends NO notification to the no member', async () => { + authenticatedUser = await noMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends NO notification to the pending member', async () => { + authenticatedUser = await pendingMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends a notification to the group member', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + from: { + __typename: 'Comment', + id: 'hidden-comment', + }, + read: false, + reason: 'mentioned_in_comment', + }, + ], + }, + errors: undefined, + }) + }) + + it('sends 1 email', () => { + expect(sendNotificationMailMock).toHaveBeenCalledTimes(1) + expect(sendNotificationMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + email: 'group.member@example.org', + reason: 'mentioned_in_comment', + }), + ) + }) + }) + }) +}) diff --git a/backend/src/middleware/notifications/observing-posts.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.observing-posts.spec.ts similarity index 72% rename from backend/src/middleware/notifications/observing-posts.spec.ts rename to backend/src/middleware/notifications/notificationsMiddleware.observing-posts.spec.ts index 13b971ed8..e8c25a16f 100644 --- a/backend/src/middleware/notifications/observing-posts.spec.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.observing-posts.spec.ts @@ -1,19 +1,27 @@ -import gql from 'graphql-tag' -import { cleanDatabase } from '../../db/factories' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { ApolloServer } from 'apollo-server-express' import { createTestClient } from 'apollo-server-testing' +import gql from 'graphql-tag' -import CONFIG from '../../config' +import CONFIG from '@config/index' +import databaseContext from '@context/database' +import Factory, { cleanDatabase } from '@db/factories' +import createServer, { getContext } from '@src/server' CONFIG.CATEGORIES_ACTIVE = false -let server, query, mutate, authenticatedUser +const sendNotificationMailMock: (notification) => void = jest.fn() +jest.mock('@src/emails/sendEmail', () => ({ + sendNotificationMail: (notification) => sendNotificationMailMock(notification), +})) -let postAuthor, firstCommenter, secondCommenter +let query, mutate, authenticatedUser -const driver = getDriver() -const neode = getNeode() +let postAuthor, firstCommenter, secondCommenter, emaillessObserver const createPostMutation = gql` mutation ($id: ID, $title: String!, $content: String!) { @@ -69,23 +77,18 @@ const toggleObservePostMutation = gql` } } ` +const database = databaseContext() + +let server: ApolloServer beforeAll(async () => { await cleanDatabase() - const createServerResult = createServer({ - context: () => { - return { - user: authenticatedUser, - neode, - driver, - cypherParams: { - currentUserId: authenticatedUser ? authenticatedUser.id : null, - }, - } - }, - }) - server = createServerResult.server + const contextUser = async (_req) => authenticatedUser + const context = getContext({ user: contextUser, database }) + + server = createServer({ context }).server + const createTestClientResult = createTestClient(server) query = createTestClientResult.query mutate = createTestClientResult.mutate @@ -93,47 +96,54 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + void server.stop() + void database.driver.close() + database.neode.close() }) describe('notifications for users that observe a post', () => { beforeAll(async () => { - postAuthor = await neode.create( - 'User', + postAuthor = await Factory.build( + 'user', { id: 'post-author', name: 'Post Author', slug: 'post-author', }, { - email: 'test@example.org', + email: 'post-author@example.org', password: '1234', }, ) - firstCommenter = await neode.create( - 'User', + firstCommenter = await Factory.build( + 'user', { id: 'first-commenter', name: 'First Commenter', slug: 'first-commenter', }, { - email: 'test2@example.org', + email: 'first-commenter@example.org', password: '1234', }, ) - secondCommenter = await neode.create( - 'User', + secondCommenter = await Factory.build( + 'user', { id: 'second-commenter', name: 'Second Commenter', slug: 'second-commenter', }, { - email: 'test3@example.org', + email: 'second-commenter@example.org', password: '1234', }, ) + emaillessObserver = await database.neode.create('User', { + id: 'email-less-observer', + name: 'Email-less Observer', + slug: 'email-less-observer', + }) authenticatedUser = await postAuthor.toJson() await mutate({ mutation: createPostMutation, @@ -143,6 +153,14 @@ describe('notifications for users that observe a post', () => { content: 'This is the content of the post', }, }) + authenticatedUser = await emaillessObserver.toJson() + await mutate({ + mutation: toggleObservePostMutation, + variables: { + id: 'post', + value: true, + }, + }) }) describe('first comment on the post', () => { @@ -194,8 +212,19 @@ describe('notifications for users that observe a post', () => { }) }) + it('sends one email', () => { + expect(sendNotificationMailMock).toHaveBeenCalledTimes(1) + expect(sendNotificationMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + email: 'post-author@example.org', + reason: 'commented_on_post', + }), + ) + }) + describe('second comment on post', () => { beforeAll(async () => { + jest.clearAllMocks() authenticatedUser = await secondCommenter.toJson() await mutate({ mutation: createCommentMutation, @@ -273,10 +302,27 @@ describe('notifications for users that observe a post', () => { errors: undefined, }) }) + + it('sends two emails', () => { + expect(sendNotificationMailMock).toHaveBeenCalledTimes(2) + expect(sendNotificationMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + email: 'post-author@example.org', + reason: 'commented_on_post', + }), + ) + expect(sendNotificationMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + email: 'first-commenter@example.org', + reason: 'commented_on_post', + }), + ) + }) }) describe('first commenter unfollows the post and post author comments post', () => { beforeAll(async () => { + jest.clearAllMocks() authenticatedUser = await firstCommenter.toJson() await mutate({ mutation: toggleObservePostMutation, @@ -372,6 +418,16 @@ describe('notifications for users that observe a post', () => { errors: undefined, }) }) + + it('sends one email', () => { + expect(sendNotificationMailMock).toHaveBeenCalledTimes(1) + expect(sendNotificationMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + email: 'second-commenter@example.org', + reason: 'commented_on_post', + }), + ) + }) }) }) }) diff --git a/backend/src/middleware/notifications/notificationsMiddleware.online-status.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.online-status.spec.ts new file mode 100644 index 000000000..1cbb6a2a1 --- /dev/null +++ b/backend/src/middleware/notifications/notificationsMiddleware.online-status.spec.ts @@ -0,0 +1,142 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +import { createTestClient } from 'apollo-server-testing' +import gql from 'graphql-tag' + +import databaseContext from '@context/database' +import Factory, { cleanDatabase } from '@db/factories' +import CONFIG from '@src/config' +import createServer, { getContext } from '@src/server' + +CONFIG.CATEGORIES_ACTIVE = false + +const sendNotificationMailMock: (notification) => void = jest.fn() +jest.mock('@src/emails/sendEmail', () => ({ + sendNotificationMail: (notification) => sendNotificationMailMock(notification), +})) + +let isUserOnlineMock = jest.fn().mockReturnValue(false) +jest.mock('../helpers/isUserOnline', () => ({ + isUserOnline: () => isUserOnlineMock(), +})) + +let mutate, authenticatedUser + +let postAuthor + +const createPostMutation = gql` + mutation ($id: ID, $title: String!, $content: String!, $groupId: ID) { + CreatePost(id: $id, title: $title, content: $content, groupId: $groupId) { + id + title + content + } + } +` + +const database = databaseContext() + +beforeAll(async () => { + await cleanDatabase() + + // eslint-disable-next-line @typescript-eslint/require-await + const contextUser = async (_req) => authenticatedUser + const context = getContext({ user: contextUser, database }) + + const { server } = createServer({ context }) + + const createTestClientResult = createTestClient(server) + mutate = createTestClientResult.mutate +}) + +afterAll(async () => { + await cleanDatabase() + await database.driver.close() +}) + +afterEach(async () => { + await cleanDatabase() +}) + +describe('online status and sending emails', () => { + beforeEach(async () => { + postAuthor = await Factory.build( + 'user', + { + id: 'post-author', + name: 'Post Author', + slug: 'post-author', + }, + { + email: 'test@example.org', + password: '1234', + }, + ) + await Factory.build( + 'user', + { + id: 'other-user', + name: 'Other User', + slug: 'other-user', + }, + { + email: 'test2@example.org', + password: '1234', + }, + ) + }) + + describe('user is online', () => { + beforeAll(() => { + isUserOnlineMock = jest.fn().mockReturnValue(true) + }) + + describe('mentioned in post', () => { + beforeEach(async () => { + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post-online-1', + title: 'This post mentions the other user', + content: + 'Hello @other-user, are you fine?', + }, + }) + }) + + it('sends NO email to the other user', () => { + expect(sendNotificationMailMock).not.toBeCalled() + }) + }) + }) + + describe('user is offline', () => { + beforeAll(() => { + isUserOnlineMock = jest.fn().mockReturnValue(false) + }) + + describe('mentioned in post', () => { + beforeEach(async () => { + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post-offline-1', + title: 'This post mentions the other user', + content: + 'Hello @other-user, are you fine?', + }, + }) + }) + + it('sends email to the other user', () => { + expect(sendNotificationMailMock).toBeCalledTimes(1) + }) + }) + }) +}) diff --git a/backend/src/middleware/notifications/notificationsMiddleware.posts-in-groups.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.posts-in-groups.spec.ts new file mode 100644 index 000000000..9a7e830ef --- /dev/null +++ b/backend/src/middleware/notifications/notificationsMiddleware.posts-in-groups.spec.ts @@ -0,0 +1,486 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +import { ApolloServer } from 'apollo-server-express' +import { createTestClient } from 'apollo-server-testing' +import gql from 'graphql-tag' + +import databaseContext from '@context/database' +import Factory, { cleanDatabase } from '@db/factories' +import { changeGroupMemberRoleMutation } from '@graphql/queries/changeGroupMemberRoleMutation' +import { createGroupMutation } from '@graphql/queries/createGroupMutation' +import { joinGroupMutation } from '@graphql/queries/joinGroupMutation' +import CONFIG from '@src/config' +import createServer, { getContext } from '@src/server' + +CONFIG.CATEGORIES_ACTIVE = false + +const sendNotificationMailMock: (notification) => void = jest.fn() +jest.mock('@src/emails/sendEmail', () => ({ + sendNotificationMail: (notification) => sendNotificationMailMock(notification), +})) + +let query, mutate, authenticatedUser + +let postAuthor, groupMember, pendingMember, emaillessMember + +const createPostMutation = gql` + mutation ($id: ID, $title: String!, $content: String!, $groupId: ID) { + CreatePost(id: $id, title: $title, content: $content, groupId: $groupId) { + id + title + content + } + } +` + +const notificationQuery = gql` + query ($read: Boolean) { + notifications(read: $read, orderBy: updatedAt_desc) { + read + reason + createdAt + relatedUser { + id + } + from { + __typename + ... on Post { + id + content + } + ... on Comment { + id + content + } + ... on Group { + id + } + } + } + } +` + +const muteGroupMutation = gql` + mutation ($groupId: ID!) { + muteGroup(groupId: $groupId) { + id + isMutedByMe + } + } +` + +const unmuteGroupMutation = gql` + mutation ($groupId: ID!) { + unmuteGroup(groupId: $groupId) { + id + isMutedByMe + } + } +` + +const markAllAsRead = async () => + mutate({ + mutation: gql` + mutation { + markAllAsRead { + id + } + } + `, + }) + +const database = databaseContext() + +let server: ApolloServer +beforeAll(async () => { + await cleanDatabase() + + const contextUser = async (_req) => authenticatedUser + const context = getContext({ user: contextUser, database }) + + server = createServer({ context }).server + + const createTestClientResult = createTestClient(server) + query = createTestClientResult.query + mutate = createTestClientResult.mutate +}) + +afterAll(async () => { + await cleanDatabase() + void server.stop() + void database.driver.close() + database.neode.close() +}) + +describe('notify group members of new posts in group', () => { + beforeEach(async () => { + postAuthor = await Factory.build( + 'user', + { + id: 'post-author', + name: 'Post Author', + slug: 'post-author', + }, + { + email: 'test@example.org', + password: '1234', + }, + ) + groupMember = await Factory.build( + 'user', + { + id: 'group-member', + name: 'Group Member', + slug: 'group-member', + }, + { + email: 'group.member@example.org', + password: '1234', + }, + ) + pendingMember = await Factory.build( + 'user', + { + id: 'pending-member', + name: 'Pending Member', + slug: 'pending-member', + }, + { + email: 'test3@example.org', + password: '1234', + }, + ) + emaillessMember = await database.neode.create('User', { + id: 'email-less-member', + name: 'Email-less Member', + slug: 'email-less-member', + }) + + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createGroupMutation(), + variables: { + id: 'g-1', + name: 'A closed group', + description: 'A closed group to test the notifications to group members', + groupType: 'closed', + actionRadius: 'national', + }, + }) + authenticatedUser = await groupMember.toJson() + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'g-1', + userId: 'group-member', + }, + }) + authenticatedUser = await pendingMember.toJson() + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'g-1', + userId: 'pending-member', + }, + }) + authenticatedUser = await emaillessMember.toJson() + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'g-1', + userId: 'group-member', + }, + }) + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'g-1', + userId: 'group-member', + roleInGroup: 'usual', + }, + }) + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'g-1', + userId: 'email-less-member', + roleInGroup: 'usual', + }, + }) + }) + + afterEach(async () => { + await cleanDatabase() + }) + + describe('group owner posts in group', () => { + beforeEach(async () => { + jest.clearAllMocks() + authenticatedUser = await groupMember.toJson() + await markAllAsRead() + authenticatedUser = await postAuthor.toJson() + await markAllAsRead() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post', + title: 'This is the new post in the group', + content: 'This is the content of the new post in the group', + groupId: 'g-1', + }, + }) + }) + + it('sends NO notification to the author of the post', async () => { + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends NO notification to the pending group member', async () => { + authenticatedUser = await pendingMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends notification to the group member', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + from: { + __typename: 'Post', + id: 'post', + }, + read: false, + reason: 'post_in_group', + }, + ], + }, + errors: undefined, + }) + }) + + it('sends one email', () => { + expect(sendNotificationMailMock).toHaveBeenCalledTimes(1) + expect(sendNotificationMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + reason: 'post_in_group', + email: 'group.member@example.org', + }), + ) + }) + + describe('group member mutes group', () => { + beforeEach(async () => { + authenticatedUser = await groupMember.toJson() + await mutate({ + mutation: muteGroupMutation, + variables: { + groupId: 'g-1', + }, + }) + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post-1', + title: 'This is another post in the group', + content: 'This is the content of another post in the group', + groupId: 'g-1', + }, + }) + }) + + it('sends NO notification when another post is posted', async () => { + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends NO email', () => { + expect(sendNotificationMailMock).not.toHaveBeenCalled() + }) + + describe('group member unmutes group again but disables email', () => { + beforeEach(async () => { + authenticatedUser = await groupMember.toJson() + await mutate({ + mutation: unmuteGroupMutation, + variables: { + groupId: 'g-1', + }, + }) + jest.clearAllMocks() + await groupMember.update({ emailNotificationsPostInGroup: false }) + }) + + it('sends notification when another post is posted', async () => { + authenticatedUser = await groupMember.toJson() + await markAllAsRead() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post-2', + title: 'This is yet another post in the group', + content: 'This is the content of yet another post in the group', + groupId: 'g-1', + }, + }) + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + from: { + __typename: 'Post', + id: 'post-2', + }, + read: false, + reason: 'post_in_group', + }, + ], + }, + errors: undefined, + }) + }) + + it('sends NO email', () => { + expect(sendNotificationMailMock).not.toHaveBeenCalled() + }) + }) + }) + + describe('group member blocks author', () => { + beforeEach(async () => { + await groupMember.relateTo(postAuthor, 'blocked') + authenticatedUser = await groupMember.toJson() + await markAllAsRead() + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post-1', + title: 'This is another post in the group', + content: 'This is the content of another post in the group', + groupId: 'g-1', + }, + }) + }) + + it('sends no notification to the user', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends NO email', () => { + expect(sendNotificationMailMock).not.toHaveBeenCalled() + }) + }) + + describe('group member mutes author', () => { + beforeEach(async () => { + await groupMember.relateTo(postAuthor, 'muted') + authenticatedUser = await groupMember.toJson() + await markAllAsRead() + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post-1', + title: 'This is another post in the group', + content: 'This is the content of another post in the group', + groupId: 'g-1', + }, + }) + }) + + it('sends no notification to the user', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends NO email', () => { + expect(sendNotificationMailMock).not.toHaveBeenCalled() + }) + }) + }) +}) diff --git a/backend/src/middleware/notifications/notificationsMiddleware.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.spec.ts index 57354d13f..7b51cec25 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.spec.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.spec.ts @@ -1,20 +1,43 @@ -import gql from 'graphql-tag' -import { cleanDatabase } from '../../db/factories' +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +import { ApolloServer } from 'apollo-server-express' import { createTestClient } from 'apollo-server-testing' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer, { pubsub } from '../../server' -import { - createGroupMutation, - joinGroupMutation, - leaveGroupMutation, - changeGroupMemberRoleMutation, - removeUserFromGroupMutation, -} from '../../graphql/groups' +import gql from 'graphql-tag' + +import databaseContext from '@context/database' +import pubsubContext from '@context/pubsub' +import Factory, { cleanDatabase } from '@db/factories' +import { changeGroupMemberRoleMutation } from '@graphql/queries/changeGroupMemberRoleMutation' +import { createGroupMutation } from '@graphql/queries/createGroupMutation' +import { createMessageMutation } from '@graphql/queries/createMessageMutation' +import { createRoomMutation } from '@graphql/queries/createRoomMutation' +import { joinGroupMutation } from '@graphql/queries/joinGroupMutation' +import { leaveGroupMutation } from '@graphql/queries/leaveGroupMutation' +import { removeUserFromGroupMutation } from '@graphql/queries/removeUserFromGroupMutation' +import createServer, { getContext } from '@src/server' + +const sendChatMessageMailMock: (notification) => void = jest.fn() +const sendNotificationMailMock: (notification) => void = jest.fn() +jest.mock('@src/emails/sendEmail', () => ({ + sendChatMessageMail: (notification) => sendChatMessageMailMock(notification), + sendNotificationMail: (notification) => sendNotificationMailMock(notification), +})) + +let isUserOnlineMock = jest.fn() +jest.mock('../helpers/isUserOnline', () => ({ + isUserOnline: () => isUserOnlineMock(), +})) + +const database = databaseContext() +const pubsub = pubsubContext() +const pubsubSpy = jest.spyOn(pubsub, 'publish') + +let query, mutate, notifiedUser, authenticatedUser -let server, query, mutate, notifiedUser, authenticatedUser -let publishSpy -const driver = getDriver() -const neode = getNeode() const categoryIds = ['cat9'] const createPostMutation = gql` mutation ($id: ID, $title: String!, $postContent: String!, $categoryIds: [ID]!) { @@ -42,20 +65,16 @@ const createCommentMutation = gql` } ` +let server: ApolloServer + beforeAll(async () => { await cleanDatabase() - publishSpy = jest.spyOn(pubsub, 'publish') - const createServerResult = createServer({ - context: () => { - return { - user: authenticatedUser, - neode, - driver, - } - }, - }) - server = createServerResult.server + const contextUser = async (_req) => authenticatedUser + const context = getContext({ user: contextUser, database, pubsub }) + + server = createServer({ context }).server + const createTestClientResult = createTestClient(server) query = createTestClientResult.query mutate = createTestClientResult.mutate @@ -63,13 +82,14 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + void server.stop() + void database.driver.close() + database.neode.close() }) beforeEach(async () => { - publishSpy.mockClear() - notifiedUser = await neode.create( - 'User', + notifiedUser = await Factory.build( + 'user', { id: 'you', name: 'Al Capone', @@ -80,7 +100,7 @@ beforeEach(async () => { password: '1234', }, ) - await neode.create('Category', { + await database.neode.create('Category', { id: 'cat9', name: 'Democracy & Politics', icon: 'university', @@ -169,9 +189,10 @@ describe('notifications', () => { describe('commenter is not me', () => { beforeEach(async () => { + jest.clearAllMocks() commentContent = 'Commenters comment.' - commentAuthor = await neode.create( - 'User', + commentAuthor = await Factory.build( + 'user', { id: 'commentAuthor', name: 'Mrs Comment', @@ -184,25 +205,8 @@ describe('notifications', () => { ) }) - it('sends me a notification', async () => { + it('sends me a notification and email', async () => { await createCommentOnPostAction() - const expected = expect.objectContaining({ - data: { - notifications: [ - { - read: false, - createdAt: expect.any(String), - reason: 'commented_on_post', - from: { - __typename: 'Comment', - id: 'c47', - content: commentContent, - }, - relatedUser: null, - }, - ], - }, - }) await expect( query({ query: notificationQuery, @@ -210,24 +214,108 @@ describe('notifications', () => { read: false, }, }), - ).resolves.toEqual(expected) + ).resolves.toMatchObject( + expect.objectContaining({ + data: { + notifications: [ + { + read: false, + createdAt: expect.any(String), + reason: 'commented_on_post', + from: { + __typename: 'Comment', + id: 'c47', + content: commentContent, + }, + relatedUser: null, + }, + ], + }, + }), + ) + + // Mail + expect(sendNotificationMailMock).toHaveBeenCalledTimes(1) + expect(sendNotificationMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + reason: 'commented_on_post', + email: 'test@example.org', + }), + ) }) - it('sends me no notification if I have blocked the comment author', async () => { - await notifiedUser.relateTo(commentAuthor, 'blocked') - await createCommentOnPostAction() - const expected = expect.objectContaining({ - data: { notifications: [] }, - }) + describe('if I have disabled `emailNotificationsCommentOnObservedPost`', () => { + it('sends me a notification but no email', async () => { + await notifiedUser.update({ emailNotificationsCommentOnObservedPost: false }) + await createCommentOnPostAction() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject( + expect.objectContaining({ + data: { + notifications: [ + { + read: false, + createdAt: expect.any(String), + reason: 'commented_on_post', + from: { + __typename: 'Comment', + id: 'c47', + content: commentContent, + }, + relatedUser: null, + }, + ], + }, + }), + ) - await expect( - query({ - query: notificationQuery, - variables: { - read: false, - }, - }), - ).resolves.toEqual(expected) + // No Mail + expect(sendNotificationMailMock).not.toHaveBeenCalled() + }) + }) + + describe('if I have blocked the comment author', () => { + it('sends me no notification', async () => { + await notifiedUser.relateTo(commentAuthor, 'blocked') + await createCommentOnPostAction() + const expected = expect.objectContaining({ + data: { notifications: [] }, + }) + + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toEqual(expected) + }) + }) + + describe('if I have muted the comment author', () => { + it('sends me no notification', async () => { + await notifiedUser.relateTo(commentAuthor, 'muted') + await createCommentOnPostAction() + const expected = expect.objectContaining({ + data: { notifications: [] }, + }) + + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toEqual(expected) + }) }) }) @@ -256,8 +344,9 @@ describe('notifications', () => { }) beforeEach(async () => { - postAuthor = await neode.create( - 'User', + jest.clearAllMocks() + postAuthor = await Factory.build( + 'user', { id: 'postAuthor', name: 'Mrs Post', @@ -278,7 +367,7 @@ describe('notifications', () => { 'Hey @al-capone how do you do?' }) - it('sends me a notification', async () => { + it('sends me a notification and email', async () => { await createPostAction() const expectedContent = 'Hey @al-capone how do you do?' @@ -306,11 +395,56 @@ describe('notifications', () => { ], }, }) + + // Mail + expect(sendNotificationMailMock).toHaveBeenCalledTimes(1) + expect(sendNotificationMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + reason: 'mentioned_in_post', + email: 'test@example.org', + }), + ) + }) + + describe('if I have disabled `emailNotificationsMention`', () => { + it('sends me a notification but no email', async () => { + await notifiedUser.update({ emailNotificationsMention: false }) + await createPostAction() + const expectedContent = + 'Hey @al-capone how do you do?' + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + errors: undefined, + data: { + notifications: [ + { + read: false, + createdAt: expect.any(String), + reason: 'mentioned_in_post', + from: { + __typename: 'Post', + id: 'p47', + content: expectedContent, + }, + }, + ], + }, + }) + + // Mail + expect(sendNotificationMailMock).not.toHaveBeenCalled() + }) }) it('publishes `NOTIFICATION_ADDED` to me', async () => { await createPostAction() - expect(publishSpy).toHaveBeenCalledWith( + expect(pubsubSpy).toHaveBeenCalledWith( 'NOTIFICATION_ADDED', expect.objectContaining({ notificationAdded: expect.objectContaining({ @@ -321,7 +455,7 @@ describe('notifications', () => { }), }), ) - expect(publishSpy).toHaveBeenCalledTimes(1) + expect(pubsubSpy).toHaveBeenCalledTimes(1) }) describe('updates the post and mentions me again', () => { @@ -471,7 +605,49 @@ describe('notifications', () => { it('does not publish `NOTIFICATION_ADDED`', async () => { await createPostAction() - expect(publishSpy).not.toHaveBeenCalled() + expect(pubsubSpy).not.toHaveBeenCalled() + }) + }) + + describe('but the author of the post muted me', () => { + beforeEach(async () => { + await postAuthor.relateTo(notifiedUser, 'muted') + }) + + it('sends me a notification', async () => { + await createPostAction() + const expected = expect.objectContaining({ + data: { + notifications: [ + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + content: + 'Hey @al-capone how do you do?', + id: 'p47', + }, + read: false, + reason: 'mentioned_in_post', + relatedUser: null, + }, + ], + }, + }) + + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toEqual(expected) + }) + + it('publishes `NOTIFICATION_ADDED`', async () => { + await createPostAction() + expect(pubsubSpy).toHaveBeenCalled() }) }) }) @@ -486,8 +662,8 @@ describe('notifications', () => { beforeEach(async () => { commentContent = 'One mention about me with @al-capone.' - commentAuthor = await neode.create( - 'User', + commentAuthor = await Factory.build( + 'user', { id: 'commentAuthor', name: 'Mrs Comment', @@ -501,15 +677,15 @@ describe('notifications', () => { }) it('sends only one notification with reason mentioned_in_comment', async () => { - postAuthor = await neode.create( - 'User', + postAuthor = await Factory.build( + 'user', { id: 'MrPostAuthor', name: 'Mr Author', slug: 'mr-author', }, { - email: 'post-author@example.org', + email: 'post-author2@example.org', password: '1234', }, ) @@ -584,8 +760,8 @@ describe('notifications', () => { await postAuthor.relateTo(notifiedUser, 'blocked') commentContent = 'One mention about me with @al-capone.' - commentAuthor = await neode.create( - 'User', + commentAuthor = await Factory.build( + 'user', { id: 'commentAuthor', name: 'Mrs Comment', @@ -615,7 +791,7 @@ describe('notifications', () => { it('does not publish `NOTIFICATION_ADDED` to authenticated user', async () => { await createCommentOnPostAction() - expect(publishSpy).toHaveBeenCalledWith( + expect(pubsubSpy).toHaveBeenCalledWith( 'NOTIFICATION_ADDED', expect.objectContaining({ notificationAdded: expect.objectContaining({ @@ -626,9 +802,273 @@ describe('notifications', () => { }), }), ) - expect(publishSpy).toHaveBeenCalledTimes(1) + expect(pubsubSpy).toHaveBeenCalledTimes(1) }) }) + + describe('but the author of the post muted me', () => { + beforeEach(async () => { + await postAuthor.relateTo(notifiedUser, 'muted') + commentContent = + 'One mention about me with @al-capone.' + commentAuthor = await Factory.build( + 'user', + { + id: 'commentAuthor', + name: 'Mrs Comment', + slug: 'mrs-comment', + }, + { + email: 'comment-author@example.org', + password: '1234', + }, + ) + }) + + it('sends me a notification', async () => { + await createCommentOnPostAction() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + createdAt: expect.any(String), + from: { + __typename: 'Comment', + content: + 'One mention about me with @al-capone.', + id: 'c47', + }, + read: false, + reason: 'mentioned_in_comment', + relatedUser: null, + }, + ], + }, + errors: undefined, + }) + }) + + it('publishes `NOTIFICATION_ADDED` to authenticated user and me', async () => { + await createCommentOnPostAction() + expect(pubsubSpy).toHaveBeenCalledWith( + 'NOTIFICATION_ADDED', + expect.objectContaining({ + notificationAdded: expect.objectContaining({ + reason: 'commented_on_post', + to: expect.objectContaining({ + id: 'postAuthor', // that's expected, it's not me but the post author + }), + }), + }), + ) + expect(pubsubSpy).toHaveBeenCalledTimes(2) + }) + }) + }) + }) + }) + + describe('chat notifications', () => { + let chatSender + let chatReceiver + let roomId + + beforeEach(async () => { + jest.clearAllMocks() + + chatSender = await Factory.build( + 'user', + { + id: 'chatSender', + name: 'chatSender', + slug: 'chatSender', + }, + { + email: 'chatSender@example.org', + password: '1234', + }, + ) + + chatReceiver = await Factory.build( + 'user', + { id: 'chatReceiver', name: 'chatReceiver', slug: 'chatReceiver' }, + { email: 'user@example.org' }, + ) + + authenticatedUser = await chatSender.toJson() + + const room = await mutate({ + mutation: createRoomMutation(), + variables: { + userId: 'chatReceiver', + }, + }) + roomId = room.data.CreateRoom.id + }) + + describe('if the chatReceiver is online', () => { + it('publishes subscriptions but sends no email', async () => { + isUserOnlineMock = jest.fn().mockReturnValue(true) + + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId, + content: 'Some nice message to chatReceiver', + }, + }) + + expect(pubsubSpy).toHaveBeenCalledWith('ROOM_COUNT_UPDATED', { + roomCountUpdated: '1', + userId: 'chatReceiver', + }) + expect(pubsubSpy).toHaveBeenCalledWith('CHAT_MESSAGE_ADDED', { + chatMessageAdded: expect.objectContaining({ + id: expect.any(String), + content: 'Some nice message to chatReceiver', + senderId: 'chatSender', + username: 'chatSender', + avatar: expect.any(String), + date: expect.any(String), + saved: true, + distributed: false, + seen: false, + }), + userId: 'chatReceiver', + }) + + expect(sendChatMessageMailMock).not.toHaveBeenCalled() + }) + }) + + describe('if the chatReceiver is offline', () => { + it('publishes subscriptions and sends an email', async () => { + isUserOnlineMock = jest.fn().mockReturnValue(false) + + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId, + content: 'Some nice message to chatReceiver', + }, + }) + + expect(pubsubSpy).toHaveBeenCalledWith('ROOM_COUNT_UPDATED', { + roomCountUpdated: '1', + userId: 'chatReceiver', + }) + expect(pubsubSpy).toHaveBeenCalledWith('CHAT_MESSAGE_ADDED', { + chatMessageAdded: expect.objectContaining({ + id: expect.any(String), + content: 'Some nice message to chatReceiver', + senderId: 'chatSender', + username: 'chatSender', + avatar: expect.any(String), + date: expect.any(String), + saved: true, + distributed: false, + seen: false, + }), + userId: 'chatReceiver', + }) + + expect(sendChatMessageMailMock).toHaveBeenCalledTimes(1) + expect(sendChatMessageMailMock).toHaveBeenCalledWith({ + email: 'user@example.org', + senderUser: expect.objectContaining({ + name: 'chatSender', + slug: 'chatsender', + id: 'chatSender', + }), + recipientUser: expect.objectContaining({ + name: 'chatReceiver', + slug: 'chatreceiver', + id: 'chatReceiver', + }), + }) + }) + }) + + describe('if the chatReceiver has blocked chatSender', () => { + it('publishes no subscriptions and sends no email', async () => { + isUserOnlineMock = jest.fn().mockReturnValue(false) + await chatReceiver.relateTo(chatSender, 'blocked') + + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId, + content: 'Some nice message to chatReceiver', + }, + }) + + expect(pubsubSpy).not.toHaveBeenCalled() + expect(pubsubSpy).not.toHaveBeenCalled() + + expect(sendChatMessageMailMock).not.toHaveBeenCalled() + }) + }) + + describe('if the chatReceiver has muted chatSender', () => { + it('publishes no subscriptions and sends no email', async () => { + isUserOnlineMock = jest.fn().mockReturnValue(false) + await chatReceiver.relateTo(chatSender, 'muted') + + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId, + content: 'Some nice message to chatReceiver', + }, + }) + + expect(pubsubSpy).not.toHaveBeenCalled() + expect(pubsubSpy).not.toHaveBeenCalled() + + expect(sendChatMessageMailMock).not.toHaveBeenCalled() + }) + }) + + describe('if the chatReceiver has disabled `emailNotificationsChatMessage`', () => { + it('publishes subscriptions but sends no email', async () => { + isUserOnlineMock = jest.fn().mockReturnValue(false) + await chatReceiver.update({ emailNotificationsChatMessage: false }) + + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId, + content: 'Some nice message to chatReceiver', + }, + }) + + expect(pubsubSpy).toHaveBeenCalledWith('ROOM_COUNT_UPDATED', { + roomCountUpdated: '1', + userId: 'chatReceiver', + }) + expect(pubsubSpy).toHaveBeenCalledWith('CHAT_MESSAGE_ADDED', { + chatMessageAdded: expect.objectContaining({ + id: expect.any(String), + content: 'Some nice message to chatReceiver', + senderId: 'chatSender', + username: 'chatSender', + avatar: expect.any(String), + date: expect.any(String), + saved: true, + distributed: false, + seen: false, + }), + userId: 'chatReceiver', + }) + + expect(sendChatMessageMailMock).not.toHaveBeenCalled() }) }) }) @@ -637,8 +1077,8 @@ describe('notifications', () => { let groupOwner beforeEach(async () => { - groupOwner = await neode.create( - 'User', + groupOwner = await Factory.build( + 'user', { id: 'group-owner', name: 'Group Owner', @@ -665,7 +1105,7 @@ describe('notifications', () => { }) describe('user joins group', () => { - beforeEach(async () => { + const joinGroupAction = async () => { authenticatedUser = await notifiedUser.toJson() await mutate({ mutation: joinGroupMutation(), @@ -675,9 +1115,14 @@ describe('notifications', () => { }, }) authenticatedUser = await groupOwner.toJson() + } + + beforeEach(async () => { + jest.clearAllMocks() }) - it('has the notification in database', async () => { + it('sends the group owner a notification and email', async () => { + await joinGroupAction() await expect( query({ query: notificationQuery, @@ -701,19 +1146,54 @@ describe('notifications', () => { }, errors: undefined, }) + + // Mail + expect(sendNotificationMailMock).toHaveBeenCalledTimes(1) + expect(sendNotificationMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + reason: 'user_joined_group', + email: 'owner@example.org', + }), + ) + }) + + describe('if the group owner has disabled `emailNotificationsGroupMemberJoined`', () => { + it('sends the group owner a notification but no email', async () => { + await groupOwner.update({ emailNotificationsGroupMemberJoined: false }) + await joinGroupAction() + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + read: false, + reason: 'user_joined_group', + createdAt: expect.any(String), + from: { + __typename: 'Group', + id: 'closed-group', + }, + relatedUser: { + id: 'you', + }, + }, + ], + }, + errors: undefined, + }) + + // Mail + expect(sendNotificationMailMock).not.toHaveBeenCalled() + }) }) }) - describe('user leaves group', () => { - beforeEach(async () => { + describe('user joins and leaves group', () => { + const leaveGroupAction = async () => { authenticatedUser = await notifiedUser.toJson() - await mutate({ - mutation: joinGroupMutation(), - variables: { - groupId: 'closed-group', - userId: authenticatedUser.id, - }, - }) await mutate({ mutation: leaveGroupMutation(), variables: { @@ -722,16 +1202,29 @@ describe('notifications', () => { }, }) authenticatedUser = await groupOwner.toJson() + } + + beforeEach(async () => { + jest.clearAllMocks() + authenticatedUser = await notifiedUser.toJson() + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'closed-group', + userId: authenticatedUser.id, + }, + }) }) - it('has two the notification in database', async () => { + it('sends the group owner two notifications and emails', async () => { + await leaveGroupAction() await expect( query({ query: notificationQuery, }), ).resolves.toMatchObject({ data: { - notifications: [ + notifications: expect.arrayContaining([ { read: false, reason: 'user_left_group', @@ -756,23 +1249,75 @@ describe('notifications', () => { id: 'you', }, }, - ], + ]), }, errors: undefined, }) + + // Mail + expect(sendNotificationMailMock).toHaveBeenCalledTimes(2) + expect(sendNotificationMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + reason: 'user_joined_group', + email: 'owner@example.org', + }), + ) + expect(sendNotificationMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + reason: 'user_left_group', + email: 'owner@example.org', + }), + ) + }) + + describe('if the group owner has disabled `emailNotificationsGroupMemberLeft`', () => { + it('sends the group owner two notification but only only one email', async () => { + await groupOwner.update({ emailNotificationsGroupMemberLeft: false }) + await leaveGroupAction() + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: expect.arrayContaining([ + { + read: false, + reason: 'user_left_group', + createdAt: expect.any(String), + from: { + __typename: 'Group', + id: 'closed-group', + }, + relatedUser: { + id: 'you', + }, + }, + { + read: false, + reason: 'user_joined_group', + createdAt: expect.any(String), + from: { + __typename: 'Group', + id: 'closed-group', + }, + relatedUser: { + id: 'you', + }, + }, + ]), + }, + errors: undefined, + }) + + // Mail + expect(sendNotificationMailMock).toHaveBeenCalledTimes(1) + }) }) }) describe('user role in group changes', () => { - beforeEach(async () => { - authenticatedUser = await notifiedUser.toJson() - await mutate({ - mutation: joinGroupMutation(), - variables: { - groupId: 'closed-group', - userId: authenticatedUser.id, - }, - }) + const changeGroupMemberRoleAction = async () => { authenticatedUser = await groupOwner.toJson() await mutate({ mutation: changeGroupMemberRoleMutation(), @@ -783,9 +1328,23 @@ describe('notifications', () => { }, }) authenticatedUser = await notifiedUser.toJson() + } + + beforeEach(async () => { + authenticatedUser = await notifiedUser.toJson() + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'closed-group', + userId: authenticatedUser.id, + }, + }) + // Clear after because the above generates a notification not related + jest.clearAllMocks() }) - it('has notification in database', async () => { + it('sends the group member a notification and email', async () => { + await changeGroupMemberRoleAction() await expect( query({ query: notificationQuery, @@ -809,19 +1368,53 @@ describe('notifications', () => { }, errors: undefined, }) + + // Mail + expect(sendNotificationMailMock).toHaveBeenCalledTimes(1) + expect(sendNotificationMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + reason: 'changed_group_member_role', + email: 'test@example.org', + }), + ) + }) + + describe('if the group member has disabled `emailNotificationsGroupMemberRoleChanged`', () => { + it('sends the group member a notification but no email', async () => { + notifiedUser.update({ emailNotificationsGroupMemberRoleChanged: false }) + await changeGroupMemberRoleAction() + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + read: false, + reason: 'changed_group_member_role', + createdAt: expect.any(String), + from: { + __typename: 'Group', + id: 'closed-group', + }, + relatedUser: { + id: 'group-owner', + }, + }, + ], + }, + errors: undefined, + }) + + // Mail + expect(sendNotificationMailMock).not.toHaveBeenCalled() + }) }) }) describe('user is removed from group', () => { - beforeEach(async () => { - authenticatedUser = await notifiedUser.toJson() - await mutate({ - mutation: joinGroupMutation(), - variables: { - groupId: 'closed-group', - userId: authenticatedUser.id, - }, - }) + const removeUserFromGroupAction = async () => { authenticatedUser = await groupOwner.toJson() await mutate({ mutation: removeUserFromGroupMutation(), @@ -831,9 +1424,23 @@ describe('notifications', () => { }, }) authenticatedUser = await notifiedUser.toJson() + } + + beforeEach(async () => { + authenticatedUser = await notifiedUser.toJson() + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'closed-group', + userId: authenticatedUser.id, + }, + }) + // Clear after because the above generates a notification not related + jest.clearAllMocks() }) - it('has notification in database', async () => { + it('sends the previous group member a notification and email', async () => { + await removeUserFromGroupAction() await expect( query({ query: notificationQuery, @@ -857,6 +1464,48 @@ describe('notifications', () => { }, errors: undefined, }) + + // Mail + expect(sendNotificationMailMock).toHaveBeenCalledTimes(1) + expect(sendNotificationMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + reason: 'removed_user_from_group', + email: 'test@example.org', + }), + ) + }) + + describe('if the previous group member has disabled `emailNotificationsGroupMemberRemoved`', () => { + it('sends the previous group member a notification but no email', async () => { + notifiedUser.update({ emailNotificationsGroupMemberRemoved: false }) + await removeUserFromGroupAction() + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + read: false, + reason: 'removed_user_from_group', + createdAt: expect.any(String), + from: { + __typename: 'Group', + id: 'closed-group', + }, + relatedUser: { + id: 'group-owner', + }, + }, + ], + }, + errors: undefined, + }) + + // Mail + expect(sendNotificationMailMock).not.toHaveBeenCalled() + }) }) }) }) diff --git a/backend/src/middleware/notifications/notificationsMiddleware.ts b/backend/src/middleware/notifications/notificationsMiddleware.ts index aa2cee06e..559c72b06 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.ts @@ -1,63 +1,52 @@ -import { pubsub, NOTIFICATION_ADDED } from '../../server' +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable security/detect-object-injection */ +import { + NOTIFICATION_ADDED, + ROOM_COUNT_UPDATED, + CHAT_MESSAGE_ADDED, +} from '@constants/subscriptions' +import { getUnreadRoomsCount } from '@graphql/resolvers/rooms' +import { isUserOnline } from '@middleware/helpers/isUserOnline' +import { validateNotifyUsers } from '@middleware/validation/validationMiddleware' +import { sendNotificationMail, sendChatMessageMail } from '@src/emails/sendEmail' + import extractMentionedUsers from './mentions/extractMentionedUsers' -import { validateNotifyUsers } from '../validation/validationMiddleware' -import { sendMail } from '../helpers/email/sendMail' -import { notificationTemplate } from '../helpers/email/templateBuilder' -const queryNotificationEmails = async (context, notificationUserIds) => { - if (!(notificationUserIds && notificationUserIds.length)) return [] - const userEmailCypher = ` - MATCH (user: User) - // blocked users are filtered out from notifications already - WHERE user.id in $notificationUserIds - WITH user - MATCH (user)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) - RETURN emailAddress {.email} - ` - const session = context.driver.session() - const writeTxResultPromise = session.readTransaction(async (transaction) => { - const emailAddressTransactionResponse = await transaction.run(userEmailCypher, { - notificationUserIds, - }) - return emailAddressTransactionResponse.records.map((record) => record.get('emailAddress')) - }) - try { - const emailAddresses = await writeTxResultPromise - return emailAddresses - } catch (error) { - throw new Error(error) - } finally { - session.close() - } -} - -const publishNotifications = async (context, promises) => { - let notifications = await Promise.all(promises) - notifications = notifications.flat() - const notificationsEmailAddresses = await queryNotificationEmails( - context, - notifications.map((notification) => notification.to.id), - ) - notifications.forEach((notificationAdded, index) => { - pubsub.publish(NOTIFICATION_ADDED, { notificationAdded }) - if (notificationAdded.to.sendNotificationEmails) { - sendMail( - notificationTemplate({ - email: notificationsEmailAddresses[index].email, - variables: { notification: notificationAdded }, - }), - ) +const publishNotifications = async ( + context, + notificationsPromise, + emailNotificationSetting: string, + emailsSent: string[] = [], +): Promise => { + const notifications = await notificationsPromise + notifications.forEach((notificationAdded) => { + context.pubsub.publish(NOTIFICATION_ADDED, { notificationAdded }) + if ( + notificationAdded.email && // no primary email was found + (notificationAdded.to[emailNotificationSetting] ?? true) && + !isUserOnline(notificationAdded.to) && + !emailsSent.includes(notificationAdded.email) + ) { + void sendNotificationMail(notificationAdded) + emailsSent.push(notificationAdded.email) } }) + return emailsSent } const handleJoinGroup = async (resolve, root, args, context, resolveInfo) => { const { groupId, userId } = args const user = await resolve(root, args, context, resolveInfo) if (user) { - await publishNotifications(context, [ + await publishNotifications( + context, notifyOwnersOfGroup(groupId, userId, 'user_joined_group', context), - ]) + 'emailNotificationsGroupMemberJoined', + ) } return user } @@ -66,9 +55,11 @@ const handleLeaveGroup = async (resolve, root, args, context, resolveInfo) => { const { groupId, userId } = args const user = await resolve(root, args, context, resolveInfo) if (user) { - await publishNotifications(context, [ + await publishNotifications( + context, notifyOwnersOfGroup(groupId, userId, 'user_left_group', context), - ]) + 'emailNotificationsGroupMemberLeft', + ) } return user } @@ -77,9 +68,11 @@ const handleChangeGroupMemberRole = async (resolve, root, args, context, resolve const { groupId, userId } = args const user = await resolve(root, args, context, resolveInfo) if (user) { - await publishNotifications(context, [ + await publishNotifications( + context, notifyMemberOfGroup(groupId, userId, 'changed_group_member_role', context), - ]) + 'emailNotificationsGroupMemberRoleChanged', + ) } return user } @@ -88,20 +81,39 @@ const handleRemoveUserFromGroup = async (resolve, root, args, context, resolveIn const { groupId, userId } = args const user = await resolve(root, args, context, resolveInfo) if (user) { - await publishNotifications(context, [ + await publishNotifications( + context, notifyMemberOfGroup(groupId, userId, 'removed_user_from_group', context), - ]) + 'emailNotificationsGroupMemberRemoved', + ) } return user } const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => { + const { groupId } = args const idsOfUsers = extractMentionedUsers(args.content) const post = await resolve(root, args, context, resolveInfo) if (post) { - await publishNotifications(context, [ + const sentEmails: string[] = await publishNotifications( + context, notifyUsersOfMention('Post', post.id, idsOfUsers, 'mentioned_in_post', context), - ]) + 'emailNotificationsMention', + ) + sentEmails.concat( + await publishNotifications( + context, + notifyFollowingUsers(post.id, groupId, context), + 'emailNotificationsFollowingUsers', + sentEmails, + ), + ) + await publishNotifications( + context, + notifyGroupMembersOfNewPost(post.id, groupId, context), + 'emailNotificationsPostInGroup', + sentEmails, + ) } return post } @@ -112,7 +124,8 @@ const handleContentDataOfComment = async (resolve, root, args, context, resolveI const comment = await resolve(root, args, context, resolveInfo) const [postAuthor] = await postAuthorOfComment(comment.id, { context }) idsOfMentionedUsers = idsOfMentionedUsers.filter((id) => id !== postAuthor.id) - await publishNotifications(context, [ + const sentEmails: string[] = await publishNotifications( + context, notifyUsersOfMention( 'Comment', comment.id, @@ -120,8 +133,14 @@ const handleContentDataOfComment = async (resolve, root, args, context, resolveI 'mentioned_in_comment', context, ), + 'emailNotificationsMention', + ) + await publishNotifications( + context, notifyUsersOfComment('Comment', comment.id, 'commented_on_post', context), - ]) + 'emailNotificationsCommentOnObservedPost', + sentEmails, + ) return comment } @@ -144,6 +163,93 @@ const postAuthorOfComment = async (commentId, { context }) => { } } +const notifyFollowingUsers = async (postId, groupId, context) => { + const reason = 'followed_user_posted' + const cypher = ` + MATCH (post:Post { id: $postId })<-[:WROTE]-(author:User { id: $userId })<-[:FOLLOWS]-(user:User) + OPTIONAL MATCH (post)-[:IN]->(group:Group { id: $groupId }) + OPTIONAL MATCH (user)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) + WITH post, author, user, emailAddress, group + WHERE group IS NULL OR group.groupType = 'public' + MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user) + SET notification.read = FALSE + SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime())) + SET notification.updatedAt = toString(datetime()) + WITH notification, author, user, emailAddress.email as email, + post {.*, author: properties(author) } AS finalResource + RETURN notification { + .*, + from: finalResource, + to: properties(user), + email: email, + relatedUser: properties(author) + } + ` + const session = context.driver.session() + const writeTxResultPromise = session.writeTransaction(async (transaction) => { + const notificationTransactionResponse = await transaction.run(cypher, { + postId, + reason, + groupId: groupId || null, + userId: context.user.id, + }) + return notificationTransactionResponse.records.map((record) => record.get('notification')) + }) + try { + return await writeTxResultPromise + } catch (error) { + throw new Error(error) + } finally { + session.close() + } +} + +const notifyGroupMembersOfNewPost = async (postId, groupId, context) => { + if (!groupId) return [] + const reason = 'post_in_group' + const cypher = ` + MATCH (post:Post { id: $postId })<-[:WROTE]-(author:User { id: $userId }) + OPTIONAL MATCH (user)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) + MATCH (post)-[:IN]->(group:Group { id: $groupId })<-[membership:MEMBER_OF]-(user:User) + WHERE NOT membership.role = 'pending' + AND NOT (user)-[:MUTED]->(group) + AND NOT (user)-[:MUTED]->(author) + AND NOT (user)-[:BLOCKED]-(author) + AND NOT user.id = $userId + WITH post, author, user, emailAddress + MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user) + SET notification.read = FALSE + SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime())) + SET notification.updatedAt = toString(datetime()) + WITH notification, author, user, emailAddress.email as email, + post {.*, author: properties(author) } AS finalResource + RETURN notification { + .*, + from: finalResource, + to: properties(user), + email: email, + relatedUser: properties(author) + } + ` + const session = context.driver.session() + const writeTxResultPromise = session.writeTransaction(async (transaction) => { + const notificationTransactionResponse = await transaction.run(cypher, { + postId, + reason, + groupId, + userId: context.user.id, + }) + return notificationTransactionResponse.records.map((record) => record.get('notification')) + }) + try { + return await writeTxResultPromise + } catch (error) { + throw new Error(error) + } finally { + session.close() + } +} + const notifyOwnersOfGroup = async (groupId, userId, reason, context) => { const cypher = ` MATCH (user:User { id: $userId }) @@ -152,12 +258,13 @@ const notifyOwnersOfGroup = async (groupId, userId, reason, context) => { WITH owner, group, user, membership MERGE (group)-[notification:NOTIFIED {reason: $reason}]->(owner) WITH group, owner, notification, user, membership + OPTIONAL MATCH (owner)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) SET notification.read = FALSE SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime())) SET notification.updatedAt = toString(datetime()) SET notification.relatedUserId = $userId - WITH owner, group { __typename: 'Group', .*, myRole: membership.roleInGroup } AS finalGroup, user, notification - RETURN notification {.*, from: finalGroup, to: properties(owner), relatedUser: properties(user) } + WITH owner, emailAddress.email as email, group { __typename: 'Group', .*, myRole: membership.roleInGroup } AS finalGroup, user, notification + RETURN notification {.*, from: finalGroup, to: properties(owner), email: email, relatedUser: properties(user) } ` const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { @@ -169,8 +276,7 @@ const notifyOwnersOfGroup = async (groupId, userId, reason, context) => { return notificationTransactionResponse.records.map((record) => record.get('notification')) }) try { - const notifications = await writeTxResultPromise - return notifications + return await writeTxResultPromise } catch (error) { throw new Error(error) } finally { @@ -184,17 +290,18 @@ const notifyMemberOfGroup = async (groupId, userId, reason, context) => { MATCH (owner:User { id: $ownerId }) MATCH (user:User { id: $userId }) MATCH (group:Group { id: $groupId }) + OPTIONAL MATCH (user)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) OPTIONAL MATCH (user)-[membership:MEMBER_OF]->(group) - WITH user, group, owner, membership + WITH user, group, owner, membership, emailAddress MERGE (group)-[notification:NOTIFIED {reason: $reason}]->(user) - WITH group, user, notification, owner, membership + WITH group, user, notification, owner, membership, emailAddress SET notification.read = FALSE SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime())) SET notification.updatedAt = toString(datetime()) SET notification.relatedUserId = $ownerId WITH group { __typename: 'Group', .*, myRole: membership.roleInGroup } AS finalGroup, - notification, user, owner - RETURN notification {.*, from: finalGroup, to: properties(user), relatedUser: properties(owner) } + notification, user, emailAddress.email as email, owner + RETURN notification {.*, from: finalGroup, to: properties(user), email: email, relatedUser: properties(owner) } ` const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { @@ -207,8 +314,7 @@ const notifyMemberOfGroup = async (groupId, userId, reason, context) => { return notificationTransactionResponse.records.map((record) => record.get('notification')) }) try { - const notifications = await writeTxResultPromise - return notifications + return await writeTxResultPromise } catch (error) { throw new Error(error) } finally { @@ -217,7 +323,7 @@ const notifyMemberOfGroup = async (groupId, userId, reason, context) => { } const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => { - if (!(idsOfUsers && idsOfUsers.length)) return [] + if (!idsOfUsers?.length) return [] await validateNotifyUsers(label, reason) let mentionedCypher switch (reason) { @@ -225,10 +331,16 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => { mentionedCypher = ` MATCH (post: Post { id: $id })<-[:WROTE]-(author: User) MATCH (user: User) - WHERE user.id in $idsOfUsers - AND NOT (user)-[:BLOCKED]-(author) + WHERE user.id in $idsOfUsers + AND NOT (user)-[:BLOCKED]-(author) + AND NOT (user)-[:MUTED]->(author) + OPTIONAL MATCH (user)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) + OPTIONAL MATCH (post)-[:IN]->(group:Group) + OPTIONAL MATCH (group)<-[membership:MEMBER_OF]-(user) + WITH post, author, user, group, emailAddress + WHERE group IS NULL OR group.groupType = 'public' OR membership.role IN ['usual', 'admin', 'owner'] MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user) - WITH post AS resource, notification, user + WITH post AS resource, notification, user, emailAddress ` break } @@ -236,25 +348,32 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => { mentionedCypher = ` MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(commenter: User) MATCH (user: User) - WHERE user.id in $idsOfUsers - AND NOT (user)-[:BLOCKED]-(commenter) - AND NOT (user)-[:BLOCKED]-(postAuthor) + WHERE user.id in $idsOfUsers + AND NOT (user)-[:BLOCKED]-(commenter) + AND NOT (user)-[:BLOCKED]-(postAuthor) + AND NOT (user)-[:MUTED]->(commenter) + AND NOT (user)-[:MUTED]->(postAuthor) + OPTIONAL MATCH (user)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) + OPTIONAL MATCH (post)-[:IN]->(group:Group) + OPTIONAL MATCH (group)<-[membership:MEMBER_OF]-(user) + WITH comment, user, group, emailAddress + WHERE group IS NULL OR group.groupType = 'public' OR membership.role IN ['usual', 'admin', 'owner'] MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user) - WITH comment AS resource, notification, user + WITH comment AS resource, notification, user, emailAddress ` break } } mentionedCypher += ` - WITH notification, user, resource, + WITH notification, user, resource, emailAddress, [(resource)<-[:WROTE]-(author:User) | author {.*}] AS authors, [(resource)-[:COMMENTS]->(post:Post)<-[:WROTE]-(author:User) | post{.*, author: properties(author)} ] AS posts - WITH resource, user, notification, authors, posts, + WITH resource, user, emailAddress.email as email, notification, authors, posts, resource {.*, __typename: [l IN labels(resource) WHERE l IN ['Post', 'Comment', 'Group']][0], author: authors[0], post: posts[0]} AS finalResource SET notification.read = FALSE SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime())) SET notification.updatedAt = toString(datetime()) - RETURN notification {.*, from: finalResource, to: properties(user), relatedUser: properties(user) } + RETURN notification {.*, from: finalResource, to: properties(user), email: email, relatedUser: properties(user) } ` const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { @@ -266,8 +385,7 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => { return notificationTransactionResponse.records.map((record) => record.get('notification')) }) try { - const notifications = await writeTxResultPromise - return notifications + return await writeTxResultPromise } catch (error) { throw new Error(error) } finally { @@ -282,19 +400,23 @@ const notifyUsersOfComment = async (label, commentId, reason, context) => { const notificationTransactionResponse = await transaction.run( ` MATCH (observingUser:User)-[:OBSERVES { active: true }]->(post:Post)<-[:COMMENTS]-(comment:Comment { id: $commentId })<-[:WROTE]-(commenter:User) - WHERE NOT (observingUser)-[:BLOCKED]-(commenter) AND NOT observingUser.id = $userId - WITH observingUser, post, comment, commenter + WHERE NOT (observingUser)-[:BLOCKED]-(commenter) + AND NOT (observingUser)-[:MUTED]->(commenter) + AND NOT observingUser.id = $userId + OPTIONAL MATCH (observingUser)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) + WITH observingUser, emailAddress, post, comment, commenter MATCH (postAuthor:User)-[:WROTE]->(post) MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(observingUser) SET notification.read = FALSE SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime())) SET notification.updatedAt = toString(datetime()) - WITH notification, observingUser, post, commenter, postAuthor, + WITH notification, observingUser, emailAddress.email as email, post, commenter, postAuthor, comment {.*, __typename: labels(comment)[0], author: properties(commenter), post: post {.*, author: properties(postAuthor) } } AS finalResource RETURN notification { .*, from: finalResource, to: properties(observingUser), + email: email, relatedUser: properties(commenter) } `, @@ -307,8 +429,72 @@ const notifyUsersOfComment = async (label, commentId, reason, context) => { return notificationTransactionResponse.records.map((record) => record.get('notification')) }) try { - const notifications = await writeTxResultPromise - return notifications + return await writeTxResultPromise + } finally { + session.close() + } +} + +const handleCreateMessage = async (resolve, root, args, context, resolveInfo) => { + // Execute resolver + const message = await resolve(root, args, context, resolveInfo) + + // Query Parameters + const { roomId } = args + const { + user: { id: currentUserId }, + } = context + + // Find Recipient + const session = context.driver.session() + const messageRecipient = session.readTransaction(async (transaction) => { + const messageRecipientCypher = ` + MATCH (senderUser:User { id: $currentUserId })-[:CHATS_IN]->(room:Room { id: $roomId }) + MATCH (room)<-[:CHATS_IN]-(recipientUser:User)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) + WHERE NOT recipientUser.id = $currentUserId + AND NOT (recipientUser)-[:BLOCKED]-(senderUser) + AND NOT (recipientUser)-[:MUTED]->(senderUser) + RETURN senderUser {.*}, recipientUser {.*}, emailAddress {.email} + ` + const txResponse = await transaction.run(messageRecipientCypher, { + currentUserId, + roomId, + }) + + return { + senderUser: await txResponse.records.map((record) => record.get('senderUser'))[0], + recipientUser: await txResponse.records.map((record) => record.get('recipientUser'))[0], + email: await txResponse.records.map((record) => record.get('emailAddress'))[0]?.email, + } + }) + + try { + // Execute Query + const { senderUser, recipientUser, email } = await messageRecipient + + if (recipientUser) { + // send subscriptions + const roomCountUpdated = await getUnreadRoomsCount(recipientUser.id, session) + + void context.pubsub.publish(ROOM_COUNT_UPDATED, { + roomCountUpdated, + userId: recipientUser.id, + }) + void context.pubsub.publish(CHAT_MESSAGE_ADDED, { + chatMessageAdded: message, + userId: recipientUser.id, + }) + + // Send EMail if we found a user(not blocked) and he is not considered online + if (recipientUser.emailNotificationsChatMessage !== false && !isUserOnline(recipientUser)) { + void sendChatMessageMail({ email, senderUser, recipientUser }) + } + } + + // Return resolver result to client + return message + } catch (error) { + throw new Error(error) } finally { session.close() } @@ -324,5 +510,6 @@ export default { LeaveGroup: handleLeaveGroup, ChangeGroupMemberRole: handleChangeGroupMemberRole, RemoveUserFromGroup: handleRemoveUserFromGroup, + CreateMessage: handleCreateMessage, }, } diff --git a/backend/src/middleware/orderByMiddleware.spec.ts b/backend/src/middleware/orderByMiddleware.spec.ts index 7453cf301..b98c21c0b 100644 --- a/backend/src/middleware/orderByMiddleware.spec.ts +++ b/backend/src/middleware/orderByMiddleware.spec.ts @@ -1,8 +1,10 @@ -import gql from 'graphql-tag' -import { cleanDatabase } from '../db/factories' -import { getNeode, getDriver } from '../db/neo4j' +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' -import createServer from '../server' +import gql from 'graphql-tag' + +import { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' const neode = getNeode() const driver = getDriver() @@ -24,7 +26,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { diff --git a/backend/src/middleware/orderByMiddleware.ts b/backend/src/middleware/orderByMiddleware.ts index 64eac8b74..c2d2ce447 100644 --- a/backend/src/middleware/orderByMiddleware.ts +++ b/backend/src/middleware/orderByMiddleware.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import cloneDeep from 'lodash/cloneDeep' const defaultOrderBy = (resolve, root, args, context, resolveInfo) => { @@ -5,7 +9,7 @@ const defaultOrderBy = (resolve, root, args, context, resolveInfo) => { const newestFirst = { kind: 'Argument', name: { kind: 'Name', value: 'orderBy' }, - value: { kind: 'EnumValue', value: 'createdAt_desc' }, + value: { kind: 'EnumValue', value: 'sortDate_desc' }, } const [fieldNode] = copy.fieldNodes if (fieldNode) fieldNode.arguments.push(newestFirst) diff --git a/backend/src/middleware/permissionsMiddleware.spec.ts b/backend/src/middleware/permissionsMiddleware.spec.ts index 667e74164..f7422f59f 100644 --- a/backend/src/middleware/permissionsMiddleware.spec.ts +++ b/backend/src/middleware/permissionsMiddleware.spec.ts @@ -1,37 +1,45 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { ApolloServer } from 'apollo-server-express' import { createTestClient } from 'apollo-server-testing' -import createServer from '../server' -import Factory, { cleanDatabase } from '../db/factories' import gql from 'graphql-tag' -import { getDriver, getNeode } from '../db/neo4j' -import CONFIG from '../config' -const instance = getNeode() -const driver = getDriver() +import CONFIG from '@config/index' +import databaseContext from '@context/database' +import Factory, { cleanDatabase } from '@db/factories' +import createServer, { getContext } from '@src/server' -let query, mutate, variables -let authenticatedUser, owner, anotherRegularUser, administrator, moderator +let variables +let owner, anotherRegularUser, administrator, moderator + +const database = databaseContext() + +let server: ApolloServer +let authenticatedUser +let query, mutate + +beforeAll(async () => { + await cleanDatabase() + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/require-await + const contextUser = async (_req) => authenticatedUser + const context = getContext({ user: contextUser, database }) + + server = createServer({ context }).server + + const createTestClientResult = createTestClient(server) + query = createTestClientResult.query + mutate = createTestClientResult.mutate +}) + +afterAll(() => { + void server.stop() + void database.driver.close() + database.neode.close() +}) describe('authorization', () => { - beforeAll(async () => { - await cleanDatabase() - - const { server } = createServer({ - context: () => ({ - driver, - instance, - user: authenticatedUser, - }), - }) - query = createTestClient(server).query - mutate = createTestClient(server).mutate - }) - - afterAll(async () => { - await cleanDatabase() - driver.close() - }) - - // TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 afterEach(async () => { await cleanDatabase() }) @@ -104,7 +112,7 @@ describe('authorization', () => { query({ query: userQuery, variables: { name: 'Owner' } }), ).resolves.toMatchObject({ errors: [{ message: 'Not Authorized!' }], - data: { User: [null] }, + data: { User: null }, }) }) }) @@ -172,8 +180,8 @@ describe('authorization', () => { describe('access Signup', () => { const signupMutation = gql` - mutation ($email: String!, $inviteCode: String) { - Signup(email: $email, inviteCode: $inviteCode) { + mutation ($email: String!, $locale: String!, $inviteCode: String) { + Signup(email: $email, locale: $locale, inviteCode: $inviteCode) { email } } @@ -184,6 +192,7 @@ describe('authorization', () => { variables = { email: 'some@email.org', inviteCode: 'ABCDEF', + locale: 'de', } CONFIG.INVITE_REGISTRATION = false CONFIG.PUBLIC_REGISTRATION = false @@ -226,6 +235,7 @@ describe('authorization', () => { variables = { email: 'some@email.org', inviteCode: 'ABCDEF', + locale: 'de', } CONFIG.INVITE_REGISTRATION = false CONFIG.PUBLIC_REGISTRATION = true @@ -235,7 +245,7 @@ describe('authorization', () => { }) describe('as anyone', () => { - beforeEach(async () => { + beforeEach(() => { authenticatedUser = null }) @@ -260,10 +270,11 @@ describe('authorization', () => { }) describe('as anyone with valid invite code', () => { - beforeEach(async () => { + beforeEach(() => { variables = { email: 'some@email.org', inviteCode: 'ABCDEF', + locale: 'de', } authenticatedUser = null }) @@ -279,10 +290,11 @@ describe('authorization', () => { }) describe('as anyone without valid invite', () => { - beforeEach(async () => { + beforeEach(() => { variables = { email: 'some@email.org', inviteCode: 'no valid invite code', + locale: 'de', } authenticatedUser = null }) diff --git a/backend/src/middleware/permissionsMiddleware.ts b/backend/src/middleware/permissionsMiddleware.ts index a38610efd..cc70fc00b 100644 --- a/backend/src/middleware/permissionsMiddleware.ts +++ b/backend/src/middleware/permissionsMiddleware.ts @@ -1,7 +1,17 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { rule, shield, deny, allow, or, and } from 'graphql-shield' -import { getNeode } from '../db/neo4j' -import CONFIG from '../config' -import { validateInviteCode } from '../schema/resolvers/transactions/inviteCodes' + +import CONFIG from '@config/index' +import SocialMedia from '@db/models/SocialMedia' +import { getNeode } from '@db/neo4j' +// eslint-disable-next-line import/no-cycle +import { validateInviteCode } from '@graphql/resolvers/inviteCodes' +import { Context } from '@src/server' const debug = !!CONFIG.DEBUG const allowExternalErrors = true @@ -11,26 +21,26 @@ const neode = getNeode() const isAuthenticated = rule({ cache: 'contextual', })(async (_parent, _args, ctx, _info) => { - return !!(ctx && ctx.user && ctx.user.id) + return !!ctx?.user?.id }) -const isModerator = rule()(async (parent, args, { user }, info) => { +const isModerator = rule()(async (_parent, _args, { user }, _info) => { return user && (user.role === 'moderator' || user.role === 'admin') }) -const isAdmin = rule()(async (parent, args, { user }, info) => { +const isAdmin = rule()(async (_parent, _args, { user }, _info) => { return user && user.role === 'admin' }) const onlyYourself = rule({ cache: 'no_cache', -})(async (parent, args, context, info) => { +})(async (_parent, args, context, _info) => { return context.user.id === args.id }) const isMyOwn = rule({ cache: 'no_cache', -})(async (parent, args, { user }, info) => { +})(async (parent, _args, { user }, _info) => { return user && user.id === parent.id }) @@ -41,21 +51,22 @@ const isMySocialMedia = rule({ if (!user) { return false } - let socialMedia = await neode.find('SocialMedia', args.id) + const socialMedia = await neode.find('SocialMedia', args.id) // Did we find a social media node? if (!socialMedia) { return false } - socialMedia = await socialMedia.toJson() // whats this for? + const socialMediaJson = await socialMedia.toJson() // whats this for? // Is it my social media entry? - return socialMedia.ownedBy.node.id === user.id + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (socialMediaJson.ownedBy as any).node.id === user.id }) const isAllowedToChangeGroupSettings = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { - if (!(user && user.id)) return false + if (!user?.id) return false const ownerId = user.id const { id: groupId } = args const session = driver.session() @@ -85,7 +96,7 @@ const isAllowedToChangeGroupSettings = rule({ const isAllowedSeeingGroupMembers = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { - if (!(user && user.id)) return false + if (!user?.id) return false const { id: groupId } = args const session = driver.session() const readTxPromise = session.readTransaction(async (transaction) => { @@ -121,7 +132,7 @@ const isAllowedSeeingGroupMembers = rule({ const isAllowedToChangeGroupMemberRole = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { - if (!(user && user.id)) return false + if (!user?.id) return false const currentUserId = user.id const { groupId, userId, roleInGroup } = args if (currentUserId === userId) return false @@ -168,7 +179,7 @@ const isAllowedToChangeGroupMemberRole = rule({ const isAllowedToJoinGroup = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { - if (!(user && user.id)) return false + if (!user?.id) return false const { groupId, userId } = args const session = driver.session() const readTxPromise = session.readTransaction(async (transaction) => { @@ -198,7 +209,7 @@ const isAllowedToJoinGroup = rule({ const isAllowedToLeaveGroup = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { - if (!(user && user.id)) return false + if (!user?.id) return false const { groupId, userId } = args if (user.id !== userId) return false const session = driver.session() @@ -228,7 +239,7 @@ const isAllowedToLeaveGroup = rule({ const isMemberOfGroup = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { - if (!(user && user.id)) return false + if (!user?.id) return false const { groupId } = args if (!groupId) return true const userId = user.id @@ -256,7 +267,7 @@ const isMemberOfGroup = rule({ const canRemoveUserFromGroup = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { - if (!(user && user.id)) return false + if (!user?.id) return false const { groupId, userId } = args const currentUserId = user.id if (currentUserId === userId) return false @@ -292,7 +303,7 @@ const canRemoveUserFromGroup = rule({ const canCommentPost = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { - if (!(user && user.id)) return false + if (!user?.id) return false const { postId } = args const userId = user.id const session = driver.session() @@ -349,7 +360,7 @@ const isAuthor = rule({ const isDeletingOwnAccount = rule({ cache: 'no_cache', -})(async (parent, args, context, _info) => { +})(async (_parent, args, context, _info) => { return context.user.id === args.id }) @@ -361,11 +372,28 @@ const noEmailFilter = rule({ const publicRegistration = rule()(() => CONFIG.PUBLIC_REGISTRATION) -const inviteRegistration = rule()(async (_parent, args, { user, driver }) => { +const inviteRegistration = rule()(async (_parent, args, context: Context) => { if (!CONFIG.INVITE_REGISTRATION) return false const { inviteCode } = args - const session = driver.session() - return validateInviteCode(session, inviteCode) + return validateInviteCode(context, inviteCode) +}) + +const isAllowedToGenerateGroupInviteCode = rule({ + cache: 'no_cache', +})(async (_parent, args, context: Context) => { + if (!context.user) return false + + return !!( + await context.database.query({ + query: ` + MATCH (user:User{id: user.id})-[membership:MEMBER_OF]->(group:Group {id: $args.groupId}) + WHERE (group.type IN ['closed','hidden'] AND membership.role IN ['admin', 'owner']) + OR (NOT group.type IN ['closed','hidden'] AND NOT membership.role = 'pending') + RETURN count(group) as count + `, + variables: { user: context.user, args }, + }) + ).records[0].get('count') }) // Permissions @@ -383,15 +411,14 @@ export default shield( Tag: allow, reports: isModerator, statistics: allow, - currentUser: allow, + currentUser: isAuthenticated, Group: isAuthenticated, GroupMembers: isAllowedSeeingGroupMembers, GroupCount: isAuthenticated, Post: allow, profilePagePosts: allow, Comment: allow, - User: or(noEmailFilter, isAdmin), - isLoggedIn: allow, + User: and(isAuthenticated, or(noEmailFilter, isAdmin)), Badge: allow, PostsEmotionsCountByEmotion: allow, PostsEmotionsByCurrentUser: isAuthenticated, @@ -400,15 +427,16 @@ export default shield( notifications: isAuthenticated, Donations: isAuthenticated, userData: isAuthenticated, - MyInviteCodes: isAuthenticated, - isValidInviteCode: allow, VerifyNonce: allow, - queryLocations: isAuthenticated, + queryLocations: allow, availableRoles: isAdmin, - getInviteCode: isAuthenticated, // and inviteRegistration Room: isAuthenticated, Message: isAuthenticated, UnreadRooms: isAuthenticated, + PostsPinnedCounts: isAdmin, + + // Invite Code + validateInviteCode: allow, }, Mutation: { '*': deny, @@ -429,10 +457,9 @@ export default shield( CreateSocialMedia: isAuthenticated, UpdateSocialMedia: isMySocialMedia, DeleteSocialMedia: isMySocialMedia, - // AddBadgeRewarded: isAdmin, - // RemoveBadgeRewarded: isAdmin, - reward: isAdmin, - unreward: isAdmin, + setVerificationBadge: isAdmin, + rewardTrophyBadge: isAdmin, + revokeBadge: isAdmin, followUser: isAuthenticated, unfollowUser: isAuthenticated, shout: isAuthenticated, @@ -457,8 +484,16 @@ export default shield( VerifyEmailAddress: isAuthenticated, pinPost: isAdmin, unpinPost: isAdmin, + pushPost: isAdmin, + unpushPost: isAdmin, UpdateDonations: isAdmin, - GenerateInviteCode: isAuthenticated, + + // InviteCode + generatePersonalInviteCode: isAuthenticated, + generateGroupInviteCode: isAllowedToGenerateGroupInviteCode, + invalidateInviteCode: isAuthenticated, + redeemInviteCode: isAuthenticated, + switchUserRole: isAdmin, markTeaserAsViewed: allow, saveCategorySettings: isAuthenticated, @@ -467,9 +502,39 @@ export default shield( CreateMessage: isAuthenticated, MarkMessagesAsSeen: isAuthenticated, toggleObservePost: isAuthenticated, + muteGroup: and(isAuthenticated, isMemberOfGroup), + unmuteGroup: and(isAuthenticated, isMemberOfGroup), + setTrophyBadgeSelected: isAuthenticated, + resetTrophyBadgesSelected: isAuthenticated, }, User: { + '*': isAuthenticated, + id: allow, + name: allow, + slug: allow, + avatar: allow, email: or(isMyOwn, isAdmin), + emailNotificationSettings: isMyOwn, + inviteCodes: isMyOwn, + }, + Group: { + '*': isAuthenticated, // TODO - only those who are allowed to see the group + slug: allow, + avatar: allow, + name: allow, + about: allow, + groupType: allow, + }, + InviteCode: { + '*': allow, + redeemedBy: isAuthenticated, // TODO only for self generated, must be done in resolver + redeemedByCount: isAuthenticated, // TODO only for self generated, must be done in resolver + createdAt: isAuthenticated, // TODO only for self generated, must be done in resolver + expiresAt: isAuthenticated, // TODO only for self generated, must be done in resolver + comment: isAuthenticated, // TODO only for self generated, must be done in resolver + }, + Location: { + distanceToMe: isAuthenticated, }, Report: isModerator, }, diff --git a/backend/src/middleware/sentryMiddleware.ts b/backend/src/middleware/sentryMiddleware.ts index 73f393eef..743ec32df 100644 --- a/backend/src/middleware/sentryMiddleware.ts +++ b/backend/src/middleware/sentryMiddleware.ts @@ -1,6 +1,12 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import { sentry } from 'graphql-middleware-sentry' -import CONFIG from '../config' +import CONFIG from '@config/index' + +// eslint-disable-next-line import/no-mutable-exports, @typescript-eslint/no-explicit-any let sentryMiddleware: any = (resolve, root, args, context, resolveInfo) => resolve(root, args, context, resolveInfo) @@ -12,9 +18,10 @@ if (CONFIG.SENTRY_DSN_BACKEND) { release: CONFIG.COMMIT, environment: CONFIG.NODE_ENV, }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any withScope: (scope, error, context: any) => { scope.setUser({ - id: context.user && context.user.id, + id: context.user?.id, }) scope.setExtra('body', context.req.body) scope.setExtra('origin', context.req.headers.origin) diff --git a/backend/src/middleware/sluggifyMiddleware.ts b/backend/src/middleware/sluggifyMiddleware.ts index bbe47c9aa..0a45521f0 100644 --- a/backend/src/middleware/sluggifyMiddleware.ts +++ b/backend/src/middleware/sluggifyMiddleware.ts @@ -1,13 +1,18 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ + +/* eslint-disable @typescript-eslint/no-unsafe-return */ +import type { Context } from '@src/server' + import uniqueSlug from './slugify/uniqueSlug' -const isUniqueFor = (context, type) => { - return async (slug) => { +const isUniqueFor = (context: Context, type: string) => { + return async (slug: string) => { const session = context.driver.session() try { const existingSlug = await session.readTransaction((transaction) => { return transaction.run( ` - MATCH(p:${type} {slug: $slug }) + MATCH(p:${type} {slug: $slug }) RETURN p.slug `, { slug }, @@ -15,26 +20,50 @@ const isUniqueFor = (context, type) => { }) return existingSlug.records.length === 0 } finally { - session.close() + await session.close() } } } export default { Mutation: { - SignupVerification: async (resolve, root, args, context, info) => { + SignupVerification: async ( + resolve, + root, + args: { slug: string; name: string }, + context: Context, + info, + ) => { args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'User'))) return resolve(root, args, context, info) }, - CreateGroup: async (resolve, root, args, context, info) => { + CreateGroup: async ( + resolve, + root, + args: { slug: string; name: string }, + context: Context, + info, + ) => { args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Group'))) return resolve(root, args, context, info) }, - CreatePost: async (resolve, root, args, context, info) => { + CreatePost: async ( + resolve, + root, + args: { slug: string; title: string }, + context: Context, + info, + ) => { args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post'))) return resolve(root, args, context, info) }, - UpdatePost: async (resolve, root, args, context, info) => { + UpdatePost: async ( + resolve, + root, + args: { slug: string; title: string }, + context: Context, + info, + ) => { // TODO: is this absolutely correct? what happens if "args.title" is not defined? may it works accidentally, because "args.title" or "args.slug" is always send? args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post'))) return resolve(root, args, context, info) diff --git a/backend/src/middleware/slugify/uniqueSlug.spec.ts b/backend/src/middleware/slugify/uniqueSlug.spec.ts index 659a439c2..8259583cd 100644 --- a/backend/src/middleware/slugify/uniqueSlug.spec.ts +++ b/backend/src/middleware/slugify/uniqueSlug.spec.ts @@ -14,9 +14,11 @@ describe('uniqueSlug', () => { }) it('slugify null string', async () => { - const string = null + const nullString = null const isUnique = jest.fn().mockResolvedValue(true) - await expect(uniqueSlug(string, isUnique)).resolves.toEqual('anonymous') + await expect(uniqueSlug(nullString as unknown as string, isUnique)).resolves.toEqual( + 'anonymous', + ) }) it('Converts umlaut to a two letter equivalent', async () => { diff --git a/backend/src/middleware/slugify/uniqueSlug.ts b/backend/src/middleware/slugify/uniqueSlug.ts index 41d58ece3..8f540a6ab 100644 --- a/backend/src/middleware/slugify/uniqueSlug.ts +++ b/backend/src/middleware/slugify/uniqueSlug.ts @@ -1,14 +1,15 @@ import slugify from 'slug' -export default async function uniqueSlug(string, isUnique) { - const slug = slugify(string || 'anonymous', { +type IsUnique = (slug: string) => Promise +export default async function uniqueSlug(str: string, isUnique: IsUnique) { + const slug = slugify(str || 'anonymous', { lower: true, multicharmap: { Ä: 'AE', ä: 'ae', Ö: 'OE', ö: 'oe', Ü: 'UE', ü: 'ue', ß: 'ss' }, }) if (await isUnique(slug)) return slug let count = 0 - let uniqueSlug + let uniqueSlug: string do { count += 1 uniqueSlug = `${slug}-${count}` diff --git a/backend/src/middleware/slugifyMiddleware.spec.ts b/backend/src/middleware/slugifyMiddleware.spec.ts index 26bb2cb96..f40c2064a 100644 --- a/backend/src/middleware/slugifyMiddleware.spec.ts +++ b/backend/src/middleware/slugifyMiddleware.spec.ts @@ -1,42 +1,47 @@ -import { getNeode, getDriver } from '../db/neo4j' -import createServer from '../server' +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { ApolloServer } from 'apollo-server-express' import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../db/factories' -import { createGroupMutation, updateGroupMutation } from '../graphql/groups' -import { createPostMutation } from '../graphql/posts' -import { signupVerificationMutation } from '../graphql/authentications' -let authenticatedUser +import databaseContext from '@context/database' +import Factory, { cleanDatabase } from '@db/factories' +import { createGroupMutation } from '@graphql/queries/createGroupMutation' +import { createPostMutation } from '@graphql/queries/createPostMutation' +import { signupVerificationMutation } from '@graphql/queries/signupVerificationMutation' +import { updateGroupMutation } from '@graphql/queries/updateGroupMutation' +import createServer, { getContext } from '@src/server' + let variables const categoryIds = ['cat9'] -const driver = getDriver() -const neode = getNeode() const descriptionAdditional100 = ' 123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789' -const { server } = createServer({ - context: () => { - return { - driver, - neode, - user: authenticatedUser, - cypherParams: { - currentUserId: authenticatedUser ? authenticatedUser.id : null, - }, - } - }, -}) +const database = databaseContext() -const { mutate } = createTestClient(server) +let server: ApolloServer +let authenticatedUser +let mutate beforeAll(async () => { await cleanDatabase() + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/require-await + const contextUser = async (_req) => authenticatedUser + const context = getContext({ user: contextUser, database }) + + server = createServer({ context }).server + + const createTestClientResult = createTestClient(server) + mutate = createTestClientResult.mutate }) -afterAll(async () => { - await cleanDatabase() - driver.close() +afterAll(() => { + void server.stop() + void database.driver.close() + database.neode.close() }) beforeEach(async () => { diff --git a/backend/src/middleware/softDelete/softDeleteMiddleware.spec.ts b/backend/src/middleware/softDelete/softDeleteMiddleware.spec.ts index 88d46a1c7..ed9dcbf37 100644 --- a/backend/src/middleware/softDelete/softDeleteMiddleware.spec.ts +++ b/backend/src/middleware/softDelete/softDeleteMiddleware.spec.ts @@ -1,8 +1,15 @@ -import Factory, { cleanDatabase } from '../../db/factories' -import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' +/* eslint-disable @typescript-eslint/await-thenable */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' +import gql from 'graphql-tag' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' const neode = getNeode() const driver = getDriver() @@ -41,7 +48,7 @@ beforeAll(async () => { }, { avatar: Factory.build('image', { - url: '/some/offensive/avatar.jpg', + url: 'http://localhost/some/offensive/avatar.jpg', }), }, ), @@ -109,7 +116,7 @@ beforeAll(async () => { }, { image: Factory.build('image', { - url: '/some/offensive/image.jpg', + url: 'http://localhost/some/offensive/image.jpg', }), author: troll, categoryIds, @@ -195,7 +202,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('softDeleteMiddleware', () => { @@ -271,7 +278,7 @@ describe('softDeleteMiddleware', () => { expect(subject.about).toEqual('This self description is very offensive')) it('displays avatar', () => expect(subject.avatar).toEqual({ - url: expect.stringContaining('/some/offensive/avatar.jpg'), + url: expect.stringMatching('http://localhost/some/offensive/avatar.jpg'), })) }) @@ -286,7 +293,7 @@ describe('softDeleteMiddleware', () => { expect(subject.contentExcerpt).toEqual('This is an offensive post content')) it('displays image', () => expect(subject.image).toEqual({ - url: expect.stringContaining('/some/offensive/image.jpg'), + url: expect.stringMatching('http://localhost/some/offensive/image.jpg'), })) }) diff --git a/backend/src/middleware/softDelete/softDeleteMiddleware.ts b/backend/src/middleware/softDelete/softDeleteMiddleware.ts index 2e1f60251..4120733ff 100644 --- a/backend/src/middleware/softDelete/softDeleteMiddleware.ts +++ b/backend/src/middleware/softDelete/softDeleteMiddleware.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ const isModerator = ({ user }) => { return user && (user.role === 'moderator' || user.role === 'admin') } diff --git a/backend/src/middleware/userInteractions.spec.ts b/backend/src/middleware/userInteractions.spec.ts index 94d1ff274..61d92ff83 100644 --- a/backend/src/middleware/userInteractions.spec.ts +++ b/backend/src/middleware/userInteractions.spec.ts @@ -1,8 +1,12 @@ -import Factory, { cleanDatabase } from '../db/factories' -import gql from 'graphql-tag' -import { getNeode, getDriver } from '../db/neo4j' -import createServer from '../server' +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' +import gql from 'graphql-tag' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' let query, aUser, bUser, post, authenticatedUser, variables @@ -42,7 +46,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('middleware/userInteractions', () => { diff --git a/backend/src/middleware/userInteractions.ts b/backend/src/middleware/userInteractions.ts index 62e8e47f7..bb850a650 100644 --- a/backend/src/middleware/userInteractions.ts +++ b/backend/src/middleware/userInteractions.ts @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ const createRelatedCypher = (relation) => ` MATCH (user:User { id: $currentUser}) MATCH (post:Post { id: $postId}) diff --git a/backend/src/middleware/validation/validationMiddleware.spec.ts b/backend/src/middleware/validation/validationMiddleware.spec.ts index 2e1cd6fa7..ea4f6ec54 100644 --- a/backend/src/middleware/validation/validationMiddleware.spec.ts +++ b/backend/src/middleware/validation/validationMiddleware.spec.ts @@ -1,8 +1,12 @@ -import gql from 'graphql-tag' -import Factory, { cleanDatabase } from '../../db/factories' -import { getNeode, getDriver } from '../../db/neo4j' +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' -import createServer from '../../server' +import gql from 'graphql-tag' + +import Factory, { cleanDatabase } from '@db/factories' +import { getNeode, getDriver } from '@db/neo4j' +import createServer from '@src/server' const neode = getNeode() const driver = getDriver() @@ -75,7 +79,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { diff --git a/backend/src/middleware/validation/validationMiddleware.ts b/backend/src/middleware/validation/validationMiddleware.ts index ff26f5ef1..75f8f5d09 100644 --- a/backend/src/middleware/validation/validationMiddleware.ts +++ b/backend/src/middleware/validation/validationMiddleware.ts @@ -1,3 +1,10 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { UserInputError } from 'apollo-server' const COMMENT_MIN_LENGTH = 1 @@ -80,7 +87,7 @@ const validateReview = async (resolve, root, args, context, info) => { try { const txResult = await reportReadTxPromise existingReportedResource = txResult - if (!existingReportedResource || !existingReportedResource.length) + if (!existingReportedResource?.length) throw new Error(`Resource not found or is not a Post|Comment|User!`) existingReportedResource = existingReportedResource[0] if (!existingReportedResource.filed) @@ -101,7 +108,13 @@ const validateReview = async (resolve, root, args, context, info) => { } export const validateNotifyUsers = async (label, reason) => { - const reasonsAllowed = ['mentioned_in_post', 'mentioned_in_comment', 'commented_on_post'] + const reasonsAllowed = [ + 'mentioned_in_post', + 'mentioned_in_comment', + 'commented_on_post', + 'followed_user_posted', + 'post_in_group', + ] if (!reasonsAllowed.includes(reason)) throw new Error('Notification reason is not allowed!') if ( (label === 'Post' && reason !== 'mentioned_in_post') || diff --git a/backend/src/middleware/xssMiddleware.ts b/backend/src/middleware/xssMiddleware.ts index c10997e8d..e8beb5463 100644 --- a/backend/src/middleware/xssMiddleware.ts +++ b/backend/src/middleware/xssMiddleware.ts @@ -1,7 +1,42 @@ -import walkRecursive from '../helpers/walkRecursive' -import { cleanHtml } from '../middleware/helpers/cleanHtml' +/* eslint-disable security/detect-object-injection */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable promise/prefer-await-to-callbacks */ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +import { cleanHtml } from './helpers/cleanHtml' -// exclamation mark separetes field names, that should not be sanitized +/** + * iterate through all fields and replace it with the callback result + * @property data Array + * @property fields Array + * @property fieldName String + * @property callback Function + */ +const walkRecursive = (data, fields, fieldName, callback, _key?) => { + if (!Array.isArray(fields)) { + throw new Error('please provide an fields array for the walkRecursive helper') + } + const fieldDef = fields.find((f) => f.field === _key) + if (data && typeof data === 'string' && fieldDef) { + if (!fieldDef.excludes?.includes(fieldName)) data = callback(data, _key) + } else if (data && Array.isArray(data)) { + // go into the rabbit hole and dig through that array + data.forEach((res, index) => { + data[index] = walkRecursive(data[index], fields, fieldName, callback, index) + }) + } else if (data && typeof data === 'object') { + // lets get some keys and stir them + Object.keys(data).forEach((k) => { + data[k] = walkRecursive(data[k], fields, fieldName, callback, k) + }) + } + return data +} + +// exclamation mark separates field names, that should not be sanitized const fields = [ { field: 'content', excludes: ['CreateMessage', 'Message'] }, { field: 'contentExcerpt' }, diff --git a/backend/src/schema/resolvers/badges.ts b/backend/src/schema/resolvers/badges.ts deleted file mode 100644 index d10d6b482..000000000 --- a/backend/src/schema/resolvers/badges.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { neo4jgraphql } from 'neo4j-graphql-js' - -export default { - Query: { - Badge: async (object, args, context, resolveInfo) => { - return neo4jgraphql(object, args, context, resolveInfo) - }, - }, -} diff --git a/backend/src/schema/resolvers/helpers/databaseLogger.ts b/backend/src/schema/resolvers/helpers/databaseLogger.ts deleted file mode 100644 index fac1a5c4a..000000000 --- a/backend/src/schema/resolvers/helpers/databaseLogger.ts +++ /dev/null @@ -1,15 +0,0 @@ -import Debug from 'debug' -const debugCypher = Debug('human-connection:neo4j:cypher') -const debugStats = Debug('human-connection:neo4j:stats') - -export default function log(response) { - const { counters, resultConsumedAfter, resultAvailableAfter, query } = response.summary - const { text, parameters } = query - debugCypher('%s', text) - debugCypher('%o', parameters) - debugStats('%o', counters) - debugStats('%o', { - resultConsumedAfter: resultConsumedAfter.toNumber(), - resultAvailableAfter: resultAvailableAfter.toNumber(), - }) -} diff --git a/backend/src/schema/resolvers/helpers/generateInviteCode.ts b/backend/src/schema/resolvers/helpers/generateInviteCode.ts deleted file mode 100644 index 5a123ff88..000000000 --- a/backend/src/schema/resolvers/helpers/generateInviteCode.ts +++ /dev/null @@ -1,13 +0,0 @@ -import CONSTANTS_REGISTRATION from './../../../constants/registration' - -export default function generateInviteCode() { - // 6 random numbers in [ 0, 35 ] are 36 possible numbers (10 [0-9] + 26 [A-Z]) - return Array.from( - { length: CONSTANTS_REGISTRATION.INVITE_CODE_LENGTH }, - (n: number = Math.floor(Math.random() * 36)) => { - // n > 9: it is a letter (ASCII 65 is A) -> 10 + 55 = 65 - // else: it is a number (ASCII 48 is 0) -> 0 + 48 = 48 - return String.fromCharCode(n > 9 ? n + 55 : n + 48) - }, - ).join('') -} diff --git a/backend/src/schema/resolvers/inviteCodes.spec.ts b/backend/src/schema/resolvers/inviteCodes.spec.ts deleted file mode 100644 index 1df791ba6..000000000 --- a/backend/src/schema/resolvers/inviteCodes.spec.ts +++ /dev/null @@ -1,209 +0,0 @@ -import Factory, { cleanDatabase } from '../../db/factories' -import { getDriver } from '../../db/neo4j' -import gql from 'graphql-tag' -import createServer from '../../server' -import { createTestClient } from 'apollo-server-testing' -import CONSTANTS_REGISTRATION from './../../constants/registration' - -let user -let query -let mutate - -const driver = getDriver() - -const generateInviteCodeMutation = gql` - mutation ($expiresAt: String = null) { - GenerateInviteCode(expiresAt: $expiresAt) { - code - createdAt - expiresAt - } - } -` -const myInviteCodesQuery = gql` - query { - MyInviteCodes { - code - createdAt - expiresAt - } - } -` -const isValidInviteCodeQuery = gql` - query ($code: ID!) { - isValidInviteCode(code: $code) - } -` - -beforeAll(async () => { - await cleanDatabase() - - const { server } = createServer({ - context: () => { - return { - driver, - user, - } - }, - }) - query = createTestClient(server).query - mutate = createTestClient(server).mutate -}) - -afterAll(async () => { - await cleanDatabase() - driver.close() -}) - -describe('inviteCodes', () => { - describe('as unauthenticated user', () => { - it('cannot generate invite codes', async () => { - await expect(mutate({ mutation: generateInviteCodeMutation })).resolves.toEqual( - expect.objectContaining({ - errors: expect.arrayContaining([ - expect.objectContaining({ - extensions: { code: 'INTERNAL_SERVER_ERROR' }, - }), - ]), - data: { - GenerateInviteCode: null, - }, - }), - ) - }) - - it('cannot query invite codes', async () => { - await expect(query({ query: myInviteCodesQuery })).resolves.toEqual( - expect.objectContaining({ - errors: expect.arrayContaining([ - expect.objectContaining({ - extensions: { code: 'INTERNAL_SERVER_ERROR' }, - }), - ]), - data: { - MyInviteCodes: null, - }, - }), - ) - }) - }) - - describe('as authenticated user', () => { - beforeAll(async () => { - const authenticatedUser = await Factory.build( - 'user', - { - role: 'user', - }, - { - email: 'user@example.org', - password: '1234', - }, - ) - user = await authenticatedUser.toJson() - }) - - it('generates an invite code without expiresAt', async () => { - await expect(mutate({ mutation: generateInviteCodeMutation })).resolves.toEqual( - expect.objectContaining({ - errors: undefined, - data: { - GenerateInviteCode: { - code: expect.stringMatching( - new RegExp( - `^[0-9A-Z]{${CONSTANTS_REGISTRATION.INVITE_CODE_LENGTH},${CONSTANTS_REGISTRATION.INVITE_CODE_LENGTH}}$`, - ), - ), - expiresAt: null, - createdAt: expect.any(String), - }, - }, - }), - ) - }) - - it('generates an invite code with expiresAt', async () => { - const nextWeek = new Date() - nextWeek.setDate(nextWeek.getDate() + 7) - await expect( - mutate({ - mutation: generateInviteCodeMutation, - variables: { expiresAt: nextWeek.toISOString() }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: undefined, - data: { - GenerateInviteCode: { - code: expect.stringMatching( - new RegExp( - `^[0-9A-Z]{${CONSTANTS_REGISTRATION.INVITE_CODE_LENGTH},${CONSTANTS_REGISTRATION.INVITE_CODE_LENGTH}}$`, - ), - ), - expiresAt: nextWeek.toISOString(), - createdAt: expect.any(String), - }, - }, - }), - ) - }) - - let inviteCodes - - it('returns the created invite codes when queried', async () => { - const response = await query({ query: myInviteCodesQuery }) - inviteCodes = response.data.MyInviteCodes - expect(inviteCodes).toHaveLength(2) - }) - - it('does not return the created invite codes of other users when queried', async () => { - await Factory.build('inviteCode') - const response = await query({ query: myInviteCodesQuery }) - inviteCodes = response.data.MyInviteCodes - expect(inviteCodes).toHaveLength(2) - }) - - it('validates an invite code without expiresAt', async () => { - const unExpiringInviteCode = inviteCodes.filter((ic) => ic.expiresAt === null)[0].code - const result = await query({ - query: isValidInviteCodeQuery, - variables: { code: unExpiringInviteCode }, - }) - expect(result.data.isValidInviteCode).toBeTruthy() - }) - - it('validates an invite code in lower case', async () => { - const unExpiringInviteCode = inviteCodes.filter((ic) => ic.expiresAt === null)[0].code - const result = await query({ - query: isValidInviteCodeQuery, - variables: { code: unExpiringInviteCode.toLowerCase() }, - }) - expect(result.data.isValidInviteCode).toBeTruthy() - }) - - it('validates an invite code with expiresAt in the future', async () => { - const expiringInviteCode = inviteCodes.filter((ic) => ic.expiresAt !== null)[0].code - const result = await query({ - query: isValidInviteCodeQuery, - variables: { code: expiringInviteCode }, - }) - expect(result.data.isValidInviteCode).toBeTruthy() - }) - - it('does not validate an invite code which expired in the past', async () => { - const lastWeek = new Date() - lastWeek.setDate(lastWeek.getDate() - 7) - const inviteCode = await Factory.build('inviteCode', { - expiresAt: lastWeek.toISOString(), - }) - const code = inviteCode.get('code') - const result = await query({ query: isValidInviteCodeQuery, variables: { code } }) - expect(result.data.isValidInviteCode).toBeFalsy() - }) - - it('does not validate an invite code which does not exits', async () => { - const result = await query({ query: isValidInviteCodeQuery, variables: { code: 'AAA' } }) - expect(result.data.isValidInviteCode).toBeFalsy() - }) - }) -}) diff --git a/backend/src/schema/resolvers/inviteCodes.ts b/backend/src/schema/resolvers/inviteCodes.ts deleted file mode 100644 index 442ff17b1..000000000 --- a/backend/src/schema/resolvers/inviteCodes.ts +++ /dev/null @@ -1,137 +0,0 @@ -import generateInviteCode from './helpers/generateInviteCode' -import Resolver from './helpers/Resolver' -import { validateInviteCode } from './transactions/inviteCodes' - -const uniqueInviteCode = async (session, code) => { - return session.readTransaction(async (txc) => { - const result = await txc.run(`MATCH (ic:InviteCode { id: $code }) RETURN count(ic) AS count`, { - code, - }) - return parseInt(String(result.records[0].get('count'))) === 0 - }) -} - -export default { - Query: { - getInviteCode: async (_parent, args, context, _resolveInfo) => { - const { - user: { id: userId }, - } = context - const session = context.driver.session() - const readTxResultPromise = session.readTransaction(async (txc) => { - const result = await txc.run( - `MATCH (user:User {id: $userId})-[:GENERATED]->(ic:InviteCode) - WHERE ic.expiresAt IS NULL - OR datetime(ic.expiresAt) >= datetime() - RETURN properties(ic) AS inviteCodes`, - { - userId, - }, - ) - return result.records.map((record) => record.get('inviteCodes')) - }) - try { - const inviteCode = await readTxResultPromise - if (inviteCode && inviteCode.length > 0) return inviteCode[0] - let code = generateInviteCode() - while (!(await uniqueInviteCode(session, code))) { - code = generateInviteCode() - } - const writeTxResultPromise = session.writeTransaction(async (txc) => { - const result = await txc.run( - `MATCH (user:User {id: $userId}) - MERGE (user)-[:GENERATED]->(ic:InviteCode { code: $code }) - ON CREATE SET - ic.createdAt = toString(datetime()), - ic.expiresAt = $expiresAt - RETURN ic AS inviteCode`, - { - userId, - code, - expiresAt: null, - }, - ) - return result.records.map((record) => record.get('inviteCode').properties) - }) - const txResult = await writeTxResultPromise - return txResult[0] - } finally { - session.close() - } - }, - MyInviteCodes: async (_parent, args, context, _resolveInfo) => { - const { - user: { id: userId }, - } = context - const session = context.driver.session() - const readTxResultPromise = session.readTransaction(async (txc) => { - const result = await txc.run( - `MATCH (user:User {id: $userId})-[:GENERATED]->(ic:InviteCode) - RETURN properties(ic) AS inviteCodes`, - { - userId, - }, - ) - return result.records.map((record) => record.get('inviteCodes')) - }) - try { - const txResult = await readTxResultPromise - return txResult - } finally { - session.close() - } - }, - isValidInviteCode: async (_parent, args, context, _resolveInfo) => { - const { code } = args - const session = context.driver.session() - if (!code) return false - return validateInviteCode(session, code) - }, - }, - Mutation: { - GenerateInviteCode: async (_parent, args, context, _resolveInfo) => { - const { - user: { id: userId }, - } = context - const session = context.driver.session() - let code = generateInviteCode() - while (!(await uniqueInviteCode(session, code))) { - code = generateInviteCode() - } - const writeTxResultPromise = session.writeTransaction(async (txc) => { - const result = await txc.run( - `MATCH (user:User {id: $userId}) - MERGE (user)-[:GENERATED]->(ic:InviteCode { code: $code }) - ON CREATE SET - ic.createdAt = toString(datetime()), - ic.expiresAt = $expiresAt - RETURN ic AS inviteCode`, - { - userId, - code, - expiresAt: args.expiresAt, - }, - ) - return result.records.map((record) => record.get('inviteCode').properties) - }) - try { - const txResult = await writeTxResultPromise - return txResult[0] - } finally { - session.close() - } - }, - }, - InviteCode: { - ...Resolver('InviteCode', { - idAttribute: 'code', - undefinedToNull: ['expiresAt'], - hasOne: { - generatedBy: '<-[:GENERATED]-(related:User)', - }, - hasMany: { - redeemedBy: '<-[:REDEEMED]-(related:User)', - }, - }), - }, -} diff --git a/backend/src/schema/resolvers/locations.spec.ts b/backend/src/schema/resolvers/locations.spec.ts deleted file mode 100644 index 82aebd441..000000000 --- a/backend/src/schema/resolvers/locations.spec.ts +++ /dev/null @@ -1,91 +0,0 @@ -import Factory, { cleanDatabase } from '../../db/factories' -import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' -import { createTestClient } from 'apollo-server-testing' - -let mutate, authenticatedUser - -const driver = getDriver() -const neode = getNeode() - -beforeAll(async () => { - await cleanDatabase() - - const { server } = createServer({ - context: () => { - return { - driver, - neode, - user: authenticatedUser, - } - }, - }) - mutate = createTestClient(server).mutate -}) - -afterAll(async () => { - await cleanDatabase() - driver.close() -}) - -// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 -afterEach(async () => { - await cleanDatabase() -}) - -describe('resolvers', () => { - describe('Location', () => { - describe('custom mutation, not handled by neo4j-graphql-js', () => { - let variables - const updateUserMutation = gql` - mutation ($id: ID!, $name: String) { - UpdateUser(id: $id, name: $name) { - name - location { - name: nameRU - nameEN - } - } - } - ` - - beforeEach(async () => { - variables = { - id: 'u47', - name: 'John Doughnut', - } - const Paris = await Factory.build('location', { - id: 'region.9397217726497330', - name: 'Paris', - type: 'region', - lng: 2.35183, - lat: 48.85658, - nameEN: 'Paris', - }) - - const user = await Factory.build('user', { - id: 'u47', - name: 'John Doe', - }) - await user.relateTo(Paris, 'isIn') - authenticatedUser = await user.toJson() - }) - - it('returns `null` if location translation is not available', async () => { - await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject({ - data: { - UpdateUser: { - name: 'John Doughnut', - location: { - name: null, - nameEN: 'Paris', - }, - }, - }, - errors: undefined, - }) - }) - }) - }) -}) diff --git a/backend/src/schema/resolvers/locations.ts b/backend/src/schema/resolvers/locations.ts deleted file mode 100644 index fa0feafa1..000000000 --- a/backend/src/schema/resolvers/locations.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { UserInputError } from 'apollo-server' -import Resolver from './helpers/Resolver' -import { queryLocations } from './users/location' - -export default { - Location: { - ...Resolver('Location', { - undefinedToNull: [ - 'nameEN', - 'nameDE', - 'nameFR', - 'nameNL', - 'nameIT', - 'nameES', - 'namePT', - 'namePL', - 'nameRU', - ], - }), - }, - Query: { - queryLocations: async (object, args, context, resolveInfo) => { - try { - return queryLocations(args) - } catch (e) { - throw new UserInputError(e.message) - } - }, - }, -} diff --git a/backend/src/schema/resolvers/registration.ts b/backend/src/schema/resolvers/registration.ts deleted file mode 100644 index 8d5aac346..000000000 --- a/backend/src/schema/resolvers/registration.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { UserInputError } from 'apollo-server' -import { getNeode } from '../../db/neo4j' -import encryptPassword from '../../helpers/encryptPassword' -import generateNonce from './helpers/generateNonce' -import existingEmailAddress from './helpers/existingEmailAddress' -import normalizeEmail from './helpers/normalizeEmail' - -const neode = getNeode() - -export default { - Mutation: { - Signup: async (_parent, args, context) => { - args.nonce = generateNonce() - args.email = normalizeEmail(args.email) - let emailAddress = await existingEmailAddress({ args, context }) - /* - if (emailAddress.user) { - // what to do? - } - */ - if (emailAddress.alreadyExistingEmail) return emailAddress.alreadyExistingEmail - try { - emailAddress = await neode.create('EmailAddress', args) - return emailAddress.toJson() - } catch (e) { - throw new UserInputError(e.message) - } - }, - SignupVerification: async (_parent, args, context) => { - const { termsAndConditionsAgreedVersion } = args - const regEx = /^[0-9]+\.[0-9]+\.[0-9]+$/g - if (!regEx.test(termsAndConditionsAgreedVersion)) { - throw new UserInputError('Invalid version format!') - } - args.termsAndConditionsAgreedAt = new Date().toISOString() - - let { nonce, email, inviteCode } = args - email = normalizeEmail(email) - delete args.nonce - delete args.email - delete args.inviteCode - args = encryptPassword(args) - - const { driver } = context - const session = driver.session() - const writeTxResultPromise = session.writeTransaction(async (transaction) => { - const createUserTransactionResponse = await transaction.run(signupCypher(inviteCode), { - args, - nonce, - email, - inviteCode, - }) - const [user] = createUserTransactionResponse.records.map((record) => record.get('user')) - if (!user) throw new UserInputError('Invalid email or nonce') - return user - }) - try { - const user = await writeTxResultPromise - return user - } catch (e) { - if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed') - throw new UserInputError('User with this slug already exists!') - throw new UserInputError(e.message) - } finally { - session.close() - } - }, - }, -} - -const signupCypher = (inviteCode) => { - let optionalMatch = '' - let optionalMerge = '' - if (inviteCode) { - optionalMatch = ` - OPTIONAL MATCH - (inviteCode:InviteCode {code: $inviteCode})<-[:GENERATED]-(host:User) - ` - optionalMerge = ` - MERGE (user)-[:REDEEMED { createdAt: toString(datetime()) }]->(inviteCode) - MERGE (host)-[:INVITED { createdAt: toString(datetime()) }]->(user) - MERGE (user)-[:FOLLOWS { createdAt: toString(datetime()) }]->(host) - MERGE (host)-[:FOLLOWS { createdAt: toString(datetime()) }]->(user) - ` - } - const cypher = ` - MATCH (email:EmailAddress {nonce: $nonce, email: $email}) - WHERE NOT (email)-[:BELONGS_TO]->() - ${optionalMatch} - CREATE (user:User) - MERGE (user)-[:PRIMARY_EMAIL]->(email) - MERGE (user)<-[:BELONGS_TO]-(email) - ${optionalMerge} - SET user += $args - SET user.id = randomUUID() - SET user.role = 'user' - SET user.createdAt = toString(datetime()) - SET user.updatedAt = toString(datetime()) - SET user.allowEmbedIframes = false - SET user.showShoutsPublicly = false - SET user.sendNotificationEmails = true - SET email.verifiedAt = toString(datetime()) - WITH user - OPTIONAL MATCH (post:Post)-[:IN]->(group:Group) - WHERE NOT group.groupType = 'public' - WITH user, collect(post) AS invisiblePosts - FOREACH (invisiblePost IN invisiblePosts | - MERGE (user)-[:CANNOT_SEE]->(invisiblePost) - ) - RETURN user {.*} - ` - return cypher -} diff --git a/backend/src/schema/resolvers/rewards.spec.ts b/backend/src/schema/resolvers/rewards.spec.ts deleted file mode 100644 index 06fe87ec0..000000000 --- a/backend/src/schema/resolvers/rewards.spec.ts +++ /dev/null @@ -1,351 +0,0 @@ -import { createTestClient } from 'apollo-server-testing' -import Factory, { cleanDatabase } from '../../db/factories' -import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' - -const driver = getDriver() -const instance = getNeode() - -let authenticatedUser, regularUser, administrator, moderator, badge, query, mutate - -describe('rewards', () => { - const variables = { - from: 'indiegogo_en_rhino', - to: 'regular-user-id', - } - - beforeAll(async () => { - await cleanDatabase() - - const { server } = createServer({ - context: () => { - return { - driver, - neode: instance, - user: authenticatedUser, - } - }, - }) - query = createTestClient(server).query - mutate = createTestClient(server).mutate - }) - - afterAll(async () => { - await cleanDatabase() - driver.close() - }) - - beforeEach(async () => { - regularUser = await Factory.build( - 'user', - { - id: 'regular-user-id', - role: 'user', - }, - { - email: 'user@example.org', - password: '1234', - }, - ) - moderator = await Factory.build( - 'user', - { - id: 'moderator-id', - role: 'moderator', - }, - { - email: 'moderator@example.org', - }, - ) - administrator = await Factory.build( - 'user', - { - id: 'admin-id', - role: 'admin', - }, - { - email: 'admin@example.org', - }, - ) - badge = await Factory.build('badge', { - id: 'indiegogo_en_rhino', - type: 'crowdfunding', - status: 'permanent', - icon: '/img/badges/indiegogo_en_rhino.svg', - }) - }) - - // TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 - afterEach(async () => { - await cleanDatabase() - }) - - describe('reward', () => { - const rewardMutation = gql` - mutation ($from: ID!, $to: ID!) { - reward(badgeKey: $from, userId: $to) { - id - badges { - id - } - } - } - ` - - describe('unauthenticated', () => { - it('throws authorization error', async () => { - authenticatedUser = null - await expect(mutate({ mutation: rewardMutation, variables })).resolves.toMatchObject({ - data: { reward: null }, - errors: [{ message: 'Not Authorized!' }], - }) - }) - }) - - describe('authenticated admin', () => { - beforeEach(async () => { - authenticatedUser = await administrator.toJson() - }) - - describe('badge for id does not exist', () => { - it('rejects with an informative error message', async () => { - await expect( - mutate({ - mutation: rewardMutation, - variables: { to: 'regular-user-id', from: 'non-existent-badge-id' }, - }), - ).resolves.toMatchObject({ - data: { reward: null }, - errors: [{ message: "Couldn't find a badge with that id" }], - }) - }) - }) - - describe('non-existent user', () => { - it('rejects with a telling error message', async () => { - await expect( - mutate({ - mutation: rewardMutation, - variables: { to: 'non-existent-user-id', from: 'indiegogo_en_rhino' }, - }), - ).resolves.toMatchObject({ - data: { reward: null }, - errors: [{ message: "Couldn't find a user with that id" }], - }) - }) - }) - - it('rewards a badge to user', async () => { - const expected = { - data: { - reward: { - id: 'regular-user-id', - badges: [{ id: 'indiegogo_en_rhino' }], - }, - }, - errors: undefined, - } - await expect(mutate({ mutation: rewardMutation, variables })).resolves.toMatchObject( - expected, - ) - }) - - it('rewards a second different badge to same user', async () => { - await Factory.build('badge', { - id: 'indiegogo_en_racoon', - icon: '/img/badges/indiegogo_en_racoon.svg', - }) - const badges = [{ id: 'indiegogo_en_racoon' }, { id: 'indiegogo_en_rhino' }] - const expected = { - data: { - reward: { - id: 'regular-user-id', - badges: expect.arrayContaining(badges), - }, - }, - errors: undefined, - } - await mutate({ - mutation: rewardMutation, - variables: { - to: 'regular-user-id', - from: 'indiegogo_en_rhino', - }, - }) - await expect( - mutate({ - mutation: rewardMutation, - variables: { - to: 'regular-user-id', - from: 'indiegogo_en_racoon', - }, - }), - ).resolves.toMatchObject(expected) - }) - - it('rewards the same badge as well to another user', async () => { - const expected = { - data: { - reward: { - id: 'regular-user-2-id', - badges: [{ id: 'indiegogo_en_rhino' }], - }, - }, - errors: undefined, - } - await Factory.build( - 'user', - { - id: 'regular-user-2-id', - }, - { - email: 'regular2@email.com', - }, - ) - await mutate({ - mutation: rewardMutation, - variables, - }) - await expect( - mutate({ - mutation: rewardMutation, - variables: { - to: 'regular-user-2-id', - from: 'indiegogo_en_rhino', - }, - }), - ).resolves.toMatchObject(expected) - }) - - it('creates no duplicate reward relationships', async () => { - await mutate({ - mutation: rewardMutation, - variables, - }) - await mutate({ - mutation: rewardMutation, - variables, - }) - - const userQuery = gql` - { - User(id: "regular-user-id") { - badgesCount - badges { - id - } - } - } - ` - const expected = { - data: { User: [{ badgesCount: 1, badges: [{ id: 'indiegogo_en_rhino' }] }] }, - errors: undefined, - } - - await expect(query({ query: userQuery })).resolves.toMatchObject(expected) - }) - }) - - describe('authenticated moderator', () => { - beforeEach(async () => { - authenticatedUser = moderator.toJson() - }) - - describe('rewards badge to user', () => { - it('throws authorization error', async () => { - await expect(mutate({ mutation: rewardMutation, variables })).resolves.toMatchObject({ - data: { reward: null }, - errors: [{ message: 'Not Authorized!' }], - }) - }) - }) - }) - }) - - describe('unreward', () => { - beforeEach(async () => { - await regularUser.relateTo(badge, 'rewarded') - }) - const expected = { - data: { unreward: { id: 'regular-user-id', badges: [] } }, - errors: undefined, - } - - const unrewardMutation = gql` - mutation ($from: ID!, $to: ID!) { - unreward(badgeKey: $from, userId: $to) { - id - badges { - id - } - } - } - ` - - describe('check test setup', () => { - it('user has one badge', async () => { - authenticatedUser = regularUser.toJson() - const userQuery = gql` - { - User(id: "regular-user-id") { - badgesCount - badges { - id - } - } - } - ` - const expected = { - data: { User: [{ badgesCount: 1, badges: [{ id: 'indiegogo_en_rhino' }] }] }, - errors: undefined, - } - await expect(query({ query: userQuery })).resolves.toMatchObject(expected) - }) - }) - - describe('unauthenticated', () => { - it('throws authorization error', async () => { - authenticatedUser = null - await expect(mutate({ mutation: unrewardMutation, variables })).resolves.toMatchObject({ - data: { unreward: null }, - errors: [{ message: 'Not Authorized!' }], - }) - }) - }) - - describe('authenticated admin', () => { - beforeEach(async () => { - authenticatedUser = await administrator.toJson() - }) - - it('removes a badge from user', async () => { - await expect(mutate({ mutation: unrewardMutation, variables })).resolves.toMatchObject( - expected, - ) - }) - - it('does not crash when unrewarding multiple times', async () => { - await mutate({ mutation: unrewardMutation, variables }) - await expect(mutate({ mutation: unrewardMutation, variables })).resolves.toMatchObject( - expected, - ) - }) - }) - - describe('authenticated moderator', () => { - beforeEach(async () => { - authenticatedUser = await moderator.toJson() - }) - - describe('removes bage from user', () => { - it('throws authorization error', async () => { - await expect(mutate({ mutation: unrewardMutation, variables })).resolves.toMatchObject({ - data: { unreward: null }, - errors: [{ message: 'Not Authorized!' }], - }) - }) - }) - }) - }) -}) diff --git a/backend/src/schema/resolvers/rewards.ts b/backend/src/schema/resolvers/rewards.ts deleted file mode 100644 index c271ca8f8..000000000 --- a/backend/src/schema/resolvers/rewards.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { getNeode } from '../../db/neo4j' -import { UserInputError } from 'apollo-server' - -const neode = getNeode() - -const getUserAndBadge = async ({ badgeKey, userId }) => { - const user = await neode.first('User', 'id', userId) - const badge = await neode.first('Badge', 'id', badgeKey) - if (!user) throw new UserInputError("Couldn't find a user with that id") - if (!badge) throw new UserInputError("Couldn't find a badge with that id") - return { user, badge } -} - -export default { - Mutation: { - reward: async (_object, params, context, _resolveInfo) => { - const { user, badge } = await getUserAndBadge(params) - await user.relateTo(badge, 'rewarded') - return user.toJson() - }, - - unreward: async (_object, params, context, _resolveInfo) => { - const { badgeKey, userId } = params - const { user } = await getUserAndBadge(params) - const session = context.driver.session() - try { - await session.writeTransaction((transaction) => { - return transaction.run( - ` - MATCH (badge:Badge {id: $badgeKey})-[reward:REWARDED]->(rewardedUser:User {id: $userId}) - DELETE reward - RETURN rewardedUser - `, - { - badgeKey, - userId, - }, - ) - }) - } finally { - session.close() - } - return user.toJson() - }, - }, -} diff --git a/backend/src/schema/resolvers/roles.ts b/backend/src/schema/resolvers/roles.ts deleted file mode 100644 index be9861e08..000000000 --- a/backend/src/schema/resolvers/roles.ts +++ /dev/null @@ -1,7 +0,0 @@ -export default { - Query: { - availableRoles: async (_parent, args, context, _resolveInfo) => { - return ['admin', 'moderator', 'user'] - }, - }, -} diff --git a/backend/src/schema/resolvers/statistics.ts b/backend/src/schema/resolvers/statistics.ts deleted file mode 100644 index b454ce8f4..000000000 --- a/backend/src/schema/resolvers/statistics.ts +++ /dev/null @@ -1,45 +0,0 @@ -import log from './helpers/databaseLogger' - -export default { - Query: { - statistics: async (_parent, _args, { driver }) => { - const session = driver.session() - const counts: any = {} - try { - const mapping = { - countUsers: 'User', - countPosts: 'Post', - countComments: 'Comment', - countNotifications: 'NOTIFIED', - countEmails: 'EmailAddress', - countFollows: 'FOLLOWS', - countShouts: 'SHOUTED', - } - const statisticsReadTxResultPromise = session.readTransaction(async (transaction) => { - const statisticsTransactionResponse = await transaction.run( - ` - CALL apoc.meta.stats() YIELD labels, relTypesCount - RETURN labels, relTypesCount - `, - ) - log(statisticsTransactionResponse) - return statisticsTransactionResponse.records.map((record) => { - return { - ...record.get('labels'), - ...record.get('relTypesCount'), - } - }) - }) - const [statistics] = await statisticsReadTxResultPromise - Object.keys(mapping).forEach((key) => { - const stat = statistics[mapping[key]] - counts[key] = stat ? stat.toNumber() : 0 - }) - counts.countInvites = counts.countEmails - counts.countUsers - return counts - } finally { - session.close() - } - }, - }, -} diff --git a/backend/src/schema/resolvers/transactions/inviteCodes.ts b/backend/src/schema/resolvers/transactions/inviteCodes.ts deleted file mode 100644 index 554b15f86..000000000 --- a/backend/src/schema/resolvers/transactions/inviteCodes.ts +++ /dev/null @@ -1,22 +0,0 @@ -export async function validateInviteCode(session, inviteCode) { - const readTxResultPromise = session.readTransaction(async (txc) => { - const result = await txc.run( - `MATCH (ic:InviteCode { code: toUpper($inviteCode) }) - RETURN - CASE - WHEN ic.expiresAt IS NULL THEN true - WHEN datetime(ic.expiresAt) >= datetime() THEN true - ELSE false END AS result`, - { - inviteCode, - }, - ) - return result.records.map((record) => record.get('result')) - }) - try { - const txResult = await readTxResultPromise - return !!txResult[0] - } finally { - session.close() - } -} diff --git a/backend/src/schema/resolvers/user_management.ts b/backend/src/schema/resolvers/user_management.ts deleted file mode 100644 index d88eafdae..000000000 --- a/backend/src/schema/resolvers/user_management.ts +++ /dev/null @@ -1,102 +0,0 @@ -import encode from '../../jwt/encode' -import bcrypt from 'bcryptjs' -import { AuthenticationError } from 'apollo-server' -import { getNeode } from '../../db/neo4j' -import normalizeEmail from './helpers/normalizeEmail' -import log from './helpers/databaseLogger' - -const neode = getNeode() - -export default { - Query: { - isLoggedIn: (_, args, { driver, user }) => { - return Boolean(user && user.id) - }, - currentUser: async (object, params, context, resolveInfo) => { - const { user, driver } = context - if (!user) return null - const session = driver.session() - const currentUserTransactionPromise = session.readTransaction(async (transaction) => { - const result = await transaction.run( - ` - MATCH (user:User {id: $id}) - OPTIONAL MATCH (category:Category) WHERE NOT ((user)-[:NOT_INTERESTED_IN]->(category)) - OPTIONAL MATCH (cats:Category) - WITH user, [(user)<-[:OWNED_BY]-(medium:SocialMedia) | properties(medium) ] AS media, category, toString(COUNT(cats)) AS categoryCount - RETURN user {.*, socialMedia: media, activeCategories: collect(category.id) } AS user, categoryCount - `, - { id: user.id }, - ) - const [categoryCount] = result.records.map((record) => record.get('categoryCount')) - const [currentUser] = result.records.map((record) => record.get('user')) - // frontend expects empty array when all categories are selected - if (currentUser.activeCategories.length === parseInt(categoryCount)) - currentUser.activeCategories = [] - return currentUser - }) - try { - const currentUser = await currentUserTransactionPromise - return currentUser - } finally { - session.close() - } - }, - }, - Mutation: { - login: async (_, { email, password }, { driver, req, user }) => { - // if (user && user.id) { - // throw new Error('Already logged in.') - // } - email = normalizeEmail(email) - const session = driver.session() - try { - const loginReadTxResultPromise = session.readTransaction(async (transaction) => { - const loginTransactionResponse = await transaction.run( - ` - MATCH (user:User {deleted: false})-[:PRIMARY_EMAIL]->(e:EmailAddress {email: $userEmail}) - RETURN user {.id, .slug, .name, .encryptedPassword, .role, .disabled, email:e.email} as user LIMIT 1 - `, - { userEmail: email }, - ) - log(loginTransactionResponse) - return loginTransactionResponse.records.map((record) => record.get('user')) - }) - const [currentUser] = await loginReadTxResultPromise - if ( - currentUser && - (await bcrypt.compareSync(password, currentUser.encryptedPassword)) && - !currentUser.disabled - ) { - delete currentUser.encryptedPassword - return encode(currentUser) - } else if (currentUser && currentUser.disabled) { - throw new AuthenticationError('Your account has been disabled.') - } else { - throw new AuthenticationError('Incorrect email address or password.') - } - } finally { - session.close() - } - }, - changePassword: async (_, { oldPassword, newPassword }, { driver, user }) => { - const currentUser = await neode.find('User', user.id) - - const encryptedPassword = currentUser.get('encryptedPassword') - if (!(await bcrypt.compareSync(oldPassword, encryptedPassword))) { - throw new AuthenticationError('Old password is not correct') - } - - if (await bcrypt.compareSync(newPassword, encryptedPassword)) { - throw new AuthenticationError('Old password and new password should be different') - } - - const newEncryptedPassword = await bcrypt.hashSync(newPassword, 10) - await currentUser.update({ - encryptedPassword: newEncryptedPassword, - updatedAt: new Date().toISOString(), - }) - - return encode(await currentUser.toJson()) - }, - }, -} diff --git a/backend/src/schema/resolvers/users.spec.ts b/backend/src/schema/resolvers/users.spec.ts deleted file mode 100644 index 09f98ad53..000000000 --- a/backend/src/schema/resolvers/users.spec.ts +++ /dev/null @@ -1,841 +0,0 @@ -import Factory, { cleanDatabase } from '../../db/factories' -import gql from 'graphql-tag' -import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' -import { createTestClient } from 'apollo-server-testing' -import { categories } from '../../constants/categories' - -const categoryIds = ['cat9'] -let user -let admin -let authenticatedUser - -let query -let mutate -let variables - -const driver = getDriver() -const neode = getNeode() - -const deleteUserMutation = gql` - mutation ($id: ID!, $resource: [Deletable]) { - DeleteUser(id: $id, resource: $resource) { - id - name - about - deleted - contributions { - id - content - contentExcerpt - deleted - comments { - id - content - contentExcerpt - deleted - } - } - comments { - id - content - contentExcerpt - deleted - } - } - } -` -const switchUserRoleMutation = gql` - mutation ($role: UserRole!, $id: ID!) { - switchUserRole(role: $role, id: $id) { - name - role - id - updatedAt - email - } - } -` - -const saveCategorySettings = gql` - mutation ($activeCategories: [String]) { - saveCategorySettings(activeCategories: $activeCategories) - } -` - -const updateOnlineStatus = gql` - mutation ($status: OnlineStatus!) { - updateOnlineStatus(status: $status) - } -` - -beforeAll(async () => { - await cleanDatabase() - - const { server } = createServer({ - context: () => { - return { - driver, - neode, - user: authenticatedUser, - } - }, - }) - query = createTestClient(server).query - mutate = createTestClient(server).mutate -}) - -afterAll(async () => { - await cleanDatabase() - driver.close() -}) - -// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 -afterEach(async () => { - await cleanDatabase() -}) - -describe('User', () => { - describe('query by email address', () => { - let userQuery - - beforeEach(async () => { - userQuery = gql` - query ($email: String) { - User(email: $email) { - name - } - } - ` - variables = { - email: 'any-email-address@example.org', - } - await Factory.build('user', { name: 'Johnny' }, { email: 'any-email-address@example.org' }) - }) - - it('is forbidden', async () => { - await expect(query({ query: userQuery, variables })).resolves.toMatchObject({ - errors: [{ message: 'Not Authorized!' }], - }) - }) - - describe('as admin', () => { - beforeEach(async () => { - const admin = await Factory.build( - 'user', - { - role: 'admin', - }, - { - email: 'admin@example.org', - password: '1234', - }, - ) - authenticatedUser = await admin.toJson() - }) - - it('is permitted', async () => { - await expect(query({ query: userQuery, variables })).resolves.toMatchObject({ - data: { User: [{ name: 'Johnny' }] }, - errors: undefined, - }) - }) - - it('non-existing email address, issue #2294', async () => { - // see: https://github.com/Human-Connection/Human-Connection/issues/2294 - await expect( - query({ - query: userQuery, - variables: { - email: 'this-email-does-not-exist@example.org', - }, - }), - ).resolves.toMatchObject({ - data: { User: [] }, - errors: undefined, - }) - }) - }) - }) -}) - -describe('UpdateUser', () => { - let updateUserMutation - - beforeEach(async () => { - updateUserMutation = gql` - mutation ( - $id: ID! - $name: String - $termsAndConditionsAgreedVersion: String - $locationName: String # empty string '' sets it to null - ) { - UpdateUser( - id: $id - name: $name - termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion - locationName: $locationName - ) { - id - name - termsAndConditionsAgreedVersion - termsAndConditionsAgreedAt - locationName - location { - name - nameDE - nameEN - } - } - } - ` - variables = { - id: 'u47', - name: 'John Doughnut', - } - - user = await Factory.build( - 'user', - { - id: 'u47', - name: 'John Doe', - termsAndConditionsAgreedVersion: null, - termsAndConditionsAgreedAt: null, - allowEmbedIframes: false, - }, - { - email: 'user@example.org', - }, - ) - }) - - describe('as another user', () => { - beforeEach(async () => { - const someoneElse = await Factory.build( - 'user', - { - name: 'James Doe', - }, - { - email: 'someone-else@example.org', - }, - ) - - authenticatedUser = await someoneElse.toJson() - }) - - it('is not allowed to change other user accounts', async () => { - const { errors } = await mutate({ mutation: updateUserMutation, variables }) - expect(errors[0]).toHaveProperty('message', 'Not Authorized!') - }) - }) - - describe('as the same user', () => { - beforeEach(async () => { - authenticatedUser = await user.toJson() - }) - - it('updates the name', async () => { - const expected = { - data: { - UpdateUser: { - id: 'u47', - name: 'John Doughnut', - }, - }, - errors: undefined, - } - await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject( - expected, - ) - }) - - describe('given a new agreed version of terms and conditions', () => { - beforeEach(async () => { - variables = { ...variables, termsAndConditionsAgreedVersion: '0.0.2' } - }) - it('update termsAndConditionsAgreedVersion', async () => { - const expected = { - data: { - UpdateUser: expect.objectContaining({ - termsAndConditionsAgreedVersion: '0.0.2', - termsAndConditionsAgreedAt: expect.any(String), - }), - }, - errors: undefined, - } - - await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject( - expected, - ) - }) - }) - - describe('given any attribute other than termsAndConditionsAgreedVersion', () => { - beforeEach(async () => { - variables = { ...variables, name: 'any name' } - }) - it('update termsAndConditionsAgreedVersion', async () => { - const expected = { - data: { - UpdateUser: expect.objectContaining({ - termsAndConditionsAgreedVersion: null, - termsAndConditionsAgreedAt: null, - }), - }, - errors: undefined, - } - - await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject( - expected, - ) - }) - }) - - it('rejects if version of terms and conditions has wrong format', async () => { - variables = { - ...variables, - termsAndConditionsAgreedVersion: 'invalid version format', - } - const { errors } = await mutate({ mutation: updateUserMutation, variables }) - expect(errors[0]).toHaveProperty('message', 'Invalid version format!') - }) - - describe('supports updating location', () => { - describe('change location to "Hamburg, New Jersey, United States"', () => { - it('has updated location to "Hamburg, New Jersey, United States"', async () => { - variables = { ...variables, locationName: 'Hamburg, New Jersey, United States' } - await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject({ - data: { - UpdateUser: { - locationName: 'Hamburg, New Jersey, United States', - location: expect.objectContaining({ - name: 'Hamburg', - nameDE: 'Hamburg', - nameEN: 'Hamburg', - }), - }, - }, - errors: undefined, - }) - }) - }) - - describe('change location to unset location', () => { - it('has updated location to unset location', async () => { - variables = { ...variables, locationName: '' } - await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject({ - data: { - UpdateUser: { - locationName: null, - location: null, - }, - }, - errors: undefined, - }) - }) - }) - }) - }) -}) - -describe('Delete a User as admin', () => { - beforeEach(async () => { - variables = { id: ' u343', resource: [] } - - user = await Factory.build('user', { - name: 'My name should be deleted', - about: 'along with my about', - id: 'u343', - }) - }) - - describe('authenticated as Admin', () => { - beforeEach(async () => { - admin = await Factory.build( - 'user', - { - role: 'admin', - }, - { - email: 'admin@example.org', - password: '1234', - }, - ) - authenticatedUser = await admin.toJson() - }) - - describe('deleting a user account', () => { - beforeEach(() => { - variables = { ...variables, id: 'u343' } - }) - - describe('given posts and comments', () => { - beforeEach(async () => { - await Factory.build('category', { - id: 'cat9', - name: 'Democracy & Politics', - icon: 'university', - }) - await Factory.build( - 'post', - { - id: 'p139', - content: 'Post by user u343', - }, - { - author: user, - categoryIds, - }, - ) - await Factory.build( - 'comment', - { - id: 'c155', - content: 'Comment by user u343', - }, - { - author: user, - }, - ) - await Factory.build( - 'comment', - { - id: 'c156', - content: "A comment by someone else on user u343's post", - }, - { - postId: 'p139', - }, - ) - }) - - it("deletes account, but doesn't delete posts or comments by default", async () => { - const expectedResponse = { - data: { - DeleteUser: { - id: 'u343', - name: 'UNAVAILABLE', - about: 'UNAVAILABLE', - deleted: true, - contributions: [ - { - id: 'p139', - content: 'Post by user u343', - contentExcerpt: 'Post by user u343', - deleted: false, - comments: [ - { - id: 'c156', - content: "A comment by someone else on user u343's post", - contentExcerpt: "A comment by someone else on user u343's post", - deleted: false, - }, - ], - }, - ], - comments: [ - { - id: 'c155', - content: 'Comment by user u343', - contentExcerpt: 'Comment by user u343', - deleted: false, - }, - ], - }, - }, - errors: undefined, - } - await expect(mutate({ mutation: deleteUserMutation, variables })).resolves.toMatchObject( - expectedResponse, - ) - }) - - describe('deletion of all posts and comments requested', () => { - beforeEach(() => { - variables = { ...variables, resource: ['Comment', 'Post'] } - }) - - it('marks posts and comments as deleted', async () => { - const expectedResponse = { - data: { - DeleteUser: { - id: 'u343', - name: 'UNAVAILABLE', - about: 'UNAVAILABLE', - deleted: true, - contributions: [ - { - id: 'p139', - content: 'UNAVAILABLE', - contentExcerpt: 'UNAVAILABLE', - deleted: true, - comments: [ - { - id: 'c156', - content: 'UNAVAILABLE', - contentExcerpt: 'UNAVAILABLE', - deleted: true, - }, - ], - }, - ], - comments: [ - { - id: 'c155', - content: 'UNAVAILABLE', - contentExcerpt: 'UNAVAILABLE', - deleted: true, - }, - ], - }, - }, - errors: undefined, - } - await expect( - mutate({ mutation: deleteUserMutation, variables }), - ).resolves.toMatchObject(expectedResponse) - }) - }) - }) - - describe('connected `EmailAddress` nodes', () => { - it('will be removed completely', async () => { - await expect(neode.all('EmailAddress')).resolves.toHaveLength(2) - await mutate({ mutation: deleteUserMutation, variables }) - - await expect(neode.all('EmailAddress')).resolves.toHaveLength(1) - }) - }) - - describe('connected `SocialMedia` nodes', () => { - beforeEach(async () => { - const socialMedia = await Factory.build('socialMedia') - await socialMedia.relateTo(user, 'ownedBy') - }) - - it('will be removed completely', async () => { - await expect(neode.all('SocialMedia')).resolves.toHaveLength(1) - await mutate({ mutation: deleteUserMutation, variables }) - await expect(neode.all('SocialMedia')).resolves.toHaveLength(0) - }) - }) - }) - }) -}) - -describe('switch user role', () => { - beforeEach(async () => { - user = await Factory.build('user', { - id: 'user', - role: 'user', - }) - admin = await Factory.build('user', { - role: 'admin', - id: 'admin', - }) - }) - - describe('as simple user', () => { - it('cannot change the role', async () => { - authenticatedUser = await user.toJson() - variables = { - id: 'user', - role: 'admin', - } - await expect(mutate({ mutation: switchUserRoleMutation, variables })).resolves.toEqual( - expect.objectContaining({ - errors: [ - expect.objectContaining({ - message: 'Not Authorized!', - }), - ], - }), - ) - }) - }) - - describe('as admin', () => { - it('changes the role of other user', async () => { - authenticatedUser = await admin.toJson() - variables = { - id: 'user', - role: 'moderator', - } - await expect(mutate({ mutation: switchUserRoleMutation, variables })).resolves.toEqual( - expect.objectContaining({ - data: { - switchUserRole: expect.objectContaining({ - role: 'moderator', - }), - }, - }), - ) - }) - - it('cannot change own role', async () => { - authenticatedUser = await admin.toJson() - variables = { - id: 'admin', - role: 'moderator', - } - await expect(mutate({ mutation: switchUserRoleMutation, variables })).resolves.toEqual( - expect.objectContaining({ - errors: [ - expect.objectContaining({ - message: 'you-cannot-change-your-own-role', - }), - ], - }), - ) - }) - }) -}) - -describe('save category settings', () => { - beforeEach(async () => { - await Promise.all( - categories.map(({ icon, name }, index) => { - return Factory.build('category', { - id: `cat${index + 1}`, - slug: name, - name, - icon, - }) - }), - ) - }) - - beforeEach(async () => { - user = await Factory.build('user', { - id: 'user', - role: 'user', - }) - variables = { - activeCategories: ['cat1', 'cat3', 'cat5'], - } - }) - - describe('not authenticated', () => { - beforeEach(async () => { - authenticatedUser = undefined - }) - - it('throws an error', async () => { - await expect(mutate({ mutation: saveCategorySettings, variables })).resolves.toEqual( - expect.objectContaining({ - errors: [ - expect.objectContaining({ - message: 'Not Authorized!', - }), - ], - }), - ) - }) - }) - - describe('authenticated', () => { - beforeEach(async () => { - authenticatedUser = await user.toJson() - }) - - const userQuery = gql` - query ($id: ID) { - User(id: $id) { - activeCategories - } - } - ` - - describe('no categories saved', () => { - it('returns true for active categories mutation', async () => { - await expect(mutate({ mutation: saveCategorySettings, variables })).resolves.toEqual( - expect.objectContaining({ - data: { saveCategorySettings: true }, - }), - ) - }) - - describe('query for user', () => { - beforeEach(async () => { - await mutate({ mutation: saveCategorySettings, variables }) - }) - - it('returns the active categories when user is queried', async () => { - await expect( - query({ query: userQuery, variables: { id: authenticatedUser.id } }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - User: [ - { - activeCategories: expect.arrayContaining(['cat1', 'cat3', 'cat5']), - }, - ], - }, - }), - ) - }) - }) - }) - - describe('categories already saved', () => { - beforeEach(async () => { - variables = { - activeCategories: ['cat1', 'cat3', 'cat5'], - } - await mutate({ mutation: saveCategorySettings, variables }) - variables = { - activeCategories: ['cat10', 'cat11', 'cat12', 'cat8', 'cat9'], - } - }) - - it('returns true', async () => { - await expect(mutate({ mutation: saveCategorySettings, variables })).resolves.toEqual( - expect.objectContaining({ - data: { saveCategorySettings: true }, - }), - ) - }) - - describe('query for user', () => { - beforeEach(async () => { - await mutate({ mutation: saveCategorySettings, variables }) - }) - - it('returns the new active categories when user is queried', async () => { - await expect( - query({ query: userQuery, variables: { id: authenticatedUser.id } }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - User: [ - { - activeCategories: expect.arrayContaining([ - 'cat10', - 'cat11', - 'cat12', - 'cat8', - 'cat9', - ]), - }, - ], - }, - }), - ) - }) - }) - }) - }) -}) - -describe('updateOnlineStatus', () => { - beforeEach(async () => { - user = await Factory.build('user', { - id: 'user', - role: 'user', - }) - variables = { - status: 'online', - } - }) - - describe('not authenticated', () => { - beforeEach(async () => { - authenticatedUser = undefined - }) - - it('throws an error', async () => { - await expect(mutate({ mutation: updateOnlineStatus, variables })).resolves.toEqual( - expect.objectContaining({ - errors: [ - expect.objectContaining({ - message: 'Not Authorized!', - }), - ], - }), - ) - }) - }) - - describe('authenticated', () => { - beforeEach(async () => { - authenticatedUser = await user.toJson() - }) - - describe('set online', () => { - it('returns true and saves the user in the database as online', async () => { - await expect(mutate({ mutation: updateOnlineStatus, variables })).resolves.toEqual( - expect.objectContaining({ - data: { updateOnlineStatus: true }, - }), - ) - - const cypher = 'MATCH (u:User {id: $id}) RETURN u' - const result = await neode.cypher(cypher, { id: authenticatedUser.id }) - const dbUser = neode.hydrateFirst(result, 'u', neode.model('User')) - await expect(dbUser.toJson()).resolves.toMatchObject({ - lastOnlineStatus: 'online', - }) - await expect(dbUser.toJson()).resolves.not.toMatchObject({ - awaySince: expect.any(String), - }) - }) - }) - - describe('set away', () => { - beforeEach(() => { - variables = { - status: 'away', - } - }) - - it('returns true and saves the user in the database as away', async () => { - await expect(mutate({ mutation: updateOnlineStatus, variables })).resolves.toEqual( - expect.objectContaining({ - data: { updateOnlineStatus: true }, - }), - ) - - const cypher = 'MATCH (u:User {id: $id}) RETURN u' - const result = await neode.cypher(cypher, { id: authenticatedUser.id }) - const dbUser = neode.hydrateFirst(result, 'u', neode.model('User')) - await expect(dbUser.toJson()).resolves.toMatchObject({ - lastOnlineStatus: 'away', - awaySince: expect.any(String), - }) - }) - - it('stores the timestamp of the first away call', async () => { - await expect(mutate({ mutation: updateOnlineStatus, variables })).resolves.toEqual( - expect.objectContaining({ - data: { updateOnlineStatus: true }, - }), - ) - - const cypher = 'MATCH (u:User {id: $id}) RETURN u' - const result = await neode.cypher(cypher, { id: authenticatedUser.id }) - const dbUser = neode.hydrateFirst(result, 'u', neode.model('User')) - await expect(dbUser.toJson()).resolves.toMatchObject({ - lastOnlineStatus: 'away', - awaySince: expect.any(String), - }) - - const awaySince = (await dbUser.toJson()).awaySince - - await expect(mutate({ mutation: updateOnlineStatus, variables })).resolves.toEqual( - expect.objectContaining({ - data: { updateOnlineStatus: true }, - }), - ) - - const result2 = await neode.cypher(cypher, { id: authenticatedUser.id }) - const dbUser2 = neode.hydrateFirst(result2, 'u', neode.model('User')) - await expect(dbUser2.toJson()).resolves.toMatchObject({ - lastOnlineStatus: 'away', - awaySince, - }) - }) - }) - }) -}) diff --git a/backend/src/schema/types/type/Badge.gql b/backend/src/schema/types/type/Badge.gql deleted file mode 100644 index dff1de89a..000000000 --- a/backend/src/schema/types/type/Badge.gql +++ /dev/null @@ -1,29 +0,0 @@ -type Badge { - id: ID! - type: BadgeType! - status: BadgeStatus! - icon: String! - createdAt: String - updatedAt: String - - rewarded: [User]! @relation(name: "REWARDED", direction: "OUT") -} - -enum BadgeStatus { - permanent - temporary -} - -enum BadgeType { - role - crowdfunding -} - -type Query { - Badge: [Badge] -} - -type Mutation { - reward(badgeKey: ID!, userId: ID!): User - unreward(badgeKey: ID!, userId: ID!): User -} diff --git a/backend/src/schema/types/type/InviteCode.gql b/backend/src/schema/types/type/InviteCode.gql deleted file mode 100644 index 3293c735b..000000000 --- a/backend/src/schema/types/type/InviteCode.gql +++ /dev/null @@ -1,18 +0,0 @@ -type InviteCode { - code: ID! - createdAt: String! - generatedBy: User @relation(name: "GENERATED", direction: "IN") - redeemedBy: [User] @relation(name: "REDEEMED", direction: "IN") - expiresAt: String -} - - -type Mutation { - GenerateInviteCode(expiresAt: String = null): InviteCode -} - -type Query { - MyInviteCodes: [InviteCode] - isValidInviteCode(code: ID!): Boolean - getInviteCode: InviteCode -} diff --git a/backend/src/schema/types/type/Statistics.gql b/backend/src/schema/types/type/Statistics.gql deleted file mode 100644 index 3963a3e50..000000000 --- a/backend/src/schema/types/type/Statistics.gql +++ /dev/null @@ -1,14 +0,0 @@ -type Query { - statistics: Statistics! -} - -type Statistics { - countUsers: Int! - countPosts: Int! - countComments: Int! - countNotifications: Int! - countInvites: Int! - countFollows: Int! - countShouts: Int! -} - diff --git a/backend/src/server.spec.ts b/backend/src/server.spec.ts deleted file mode 100644 index 6d4ef546d..000000000 --- a/backend/src/server.spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { createTestClient } from 'apollo-server-testing' -import createServer from './server' - -/** - * This file is for demonstration purposes. It does not really test the - * `isLoggedIn` query but demonstrates how we can use `apollo-server-testing`. - * All we need to do is to get an instance of `ApolloServer` and maybe we want - * stub out `context` as shown below. - * - */ - -let user -let action -describe('isLoggedIn', () => { - beforeEach(() => { - action = async () => { - const { server } = createServer({ - context: () => { - return { - user, - } - }, - }) - const { query } = createTestClient(server) - - const isLoggedIn = `{ isLoggedIn }` - return query({ query: isLoggedIn }) - } - }) - - it('returns false', async () => { - const expected = expect.objectContaining({ data: { isLoggedIn: false } }) - await expect(action()).resolves.toEqual(expected) - }) - - describe('when authenticated', () => { - it('returns true', async () => { - user = { id: '123' } - const expected = expect.objectContaining({ data: { isLoggedIn: true } }) - await expect(action()).resolves.toEqual(expected) - }) - }) -}) diff --git a/backend/src/server.ts b/backend/src/server.ts index 0522f5fc8..f56b01f34 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -1,61 +1,65 @@ -import express from 'express' -import http from 'http' -import helmet from 'helmet' +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable import/no-named-as-default-member */ +import http from 'node:http' + import { ApolloServer } from 'apollo-server-express' -import CONFIG from './config' -import middleware from './middleware' -import { getNeode, getDriver } from './db/neo4j' -import decode from './jwt/decode' -import schema from './schema' -import { RedisPubSub } from 'graphql-redis-subscriptions' -import { PubSub } from 'graphql-subscriptions' -import Redis from 'ioredis' import bodyParser from 'body-parser' +import express from 'express' import { graphqlUploadExpress } from 'graphql-upload' +import helmet from 'helmet' -export const NOTIFICATION_ADDED = 'NOTIFICATION_ADDED' -export const CHAT_MESSAGE_ADDED = 'CHAT_MESSAGE_ADDED' -export const ROOM_COUNT_UPDATED = 'ROOM_COUNT_UPDATED' -const { REDIS_DOMAIN, REDIS_PORT, REDIS_PASSWORD } = CONFIG -let prodPubsub, devPubsub -const options = { - host: REDIS_DOMAIN, - port: REDIS_PORT, - password: REDIS_PASSWORD, - retryStrategy: (times) => { - return Math.min(times * 50, 2000) - }, -} -if (options.host && options.port && options.password) { - prodPubsub = new RedisPubSub({ - publisher: new Redis(options), - subscriber: new Redis(options), - }) -} else { - devPubsub = new PubSub() -} -export const pubsub = prodPubsub || devPubsub -const driver = getDriver() -const neode = getNeode() +import databaseContext from '@context/database' +import pubsubContext from '@context/pubsub' -const getContext = async (req) => { - const user = await decode(driver, req.headers.authorization) - return { - driver, - neode, - user, - req, - cypherParams: { - currentUserId: user ? user.id : null, - }, +import CONFIG from './config' +import schema from './graphql/schema' +import decode from './jwt/decode' +// eslint-disable-next-line import/no-cycle +import middleware from './middleware' + +const serverDatabase = databaseContext() +const serverPubsub = pubsubContext() + +const databaseUser = async (req) => decode(serverDatabase.driver, req.headers.authorization) + +export const getContext = + ( + { + database = serverDatabase, + pubsub = serverPubsub, + user = databaseUser, + }: { + database?: ReturnType + pubsub?: ReturnType + user?: (any) => Promise + } = { database: serverDatabase, pubsub: serverPubsub, user: databaseUser }, + ) => + async (req) => { + const u = await user(req) + return { + database, + driver: database.driver, + neode: database.neode, + pubsub, + user: u, + req, + cypherParams: { + currentUserId: u ? u.id : null, + }, + } } -} + export const context = async (options) => { const { connection, req } = options if (connection) { return connection.context } else { - return getContext(req) + return getContext()(req) } } @@ -64,9 +68,7 @@ const createServer = (options?) => { context, schema: middleware(schema), subscriptions: { - onConnect: (connectionParams, webSocket) => { - return getContext(connectionParams) - }, + onConnect: (connectionParams) => getContext()(connectionParams), }, debug: !!CONFIG.DEBUG, uploads: false, @@ -78,11 +80,10 @@ const createServer = (options?) => { return error }, } - const server = new ApolloServer(Object.assign({}, defaults, options)) + const server = new ApolloServer(Object.assign(defaults, options)) const app = express() - app.set('driver', driver) // TODO: this exception is required for the graphql playground, since the playground loads external resources // See: https://github.com/graphql/graphql-playground/issues/1283 app.use( @@ -102,3 +103,4 @@ const createServer = (options?) => { } export default createServer +export type Context = Awaited>> diff --git a/backend/test/setup.ts b/backend/test/setup.ts index d2f24bd40..d1d32be5b 100644 --- a/backend/test/setup.ts +++ b/backend/test/setup.ts @@ -1,8 +1,2 @@ -// Polyfill missing encoders in jsdom -// https://stackoverflow.com/questions/68468203/why-am-i-getting-textencoder-is-not-defined-in-jest -import { TextEncoder, TextDecoder } from 'util' -global.TextEncoder = TextEncoder -global.TextDecoder = TextDecoder as any - // Metascraper takes longer nowadays, double time -jest.setTimeout(10000) \ No newline at end of file +jest.setTimeout(10000) diff --git a/backend/tools/replace-constants.sh b/backend/tools/replace-constants.sh index e7cee6ee3..5454d3ae8 100755 --- a/backend/tools/replace-constants.sh +++ b/backend/tools/replace-constants.sh @@ -4,4 +4,5 @@ [ -f src/config/tmp/emails.js ] && mv src/config/tmp/emails.js src/config/emails.ts [ -f src/config/tmp/logos.js ] && mv src/config/tmp/logos.js src/config/logos.ts [ -f src/config/tmp/metadata.js ] && mv src/config/tmp/metadata.js src/config/metadata.ts +[ -f src/constants/categories.js ] && mv src/constants/categories.js src/constants/categories.ts exit 0 diff --git a/backend/tsconfig.json b/backend/tsconfig.json index b6f3526a3..7da05a2f0 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -11,7 +11,7 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ @@ -25,11 +25,23 @@ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ /* Modules */ - "module": "commonjs", /* Specify what module code is generated. */ + "module": "commonjs" /* Specify what module code is generated. */, // "rootDir": "./", /* Specify the root folder within your source files. */ // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + /* Specify a set of entries that re-map imports to additional lookup locations. */ + "paths": { + "@config/*": ["./src/config/*"], + "@constants/*": ["./src/constants/*"], + "@context/*": ["./src/context/*"], + "@db/*": ["./src/db/*"], + "@graphql/*": ["./src/graphql/*"], + "@helpers/*": ["./src/helpers/*"], + "@jwt/*": ["./src/jwt/*"], + "@middleware/*": ["./src/middleware/*"], + "@src/*": ["./src/*"], + "@root/*": ["./*"] + }, // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ // "types": [], /* Specify type package names to be included without being referenced in a source file. */ @@ -55,7 +67,7 @@ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./build", /* Specify an output folder for all emitted files. */ + "outDir": "./build" /* Specify an output folder for all emitted files. */, // "removeComments": true, /* Disable emitting comments. */ // "noEmit": true, /* Disable emitting files from a compilation. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ @@ -77,19 +89,19 @@ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + "strict": true /* Enable all strict type-checking options. */, + "noImplicitAny": false /* Enable error reporting for expressions and declarations with an implied 'any' type. */, // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - "useUnknownInCatchVariables": false, /* Default catch clause variables as 'unknown' instead of 'any'. */ + "useUnknownInCatchVariables": false /* Default catch clause variables as 'unknown' instead of 'any'. */, // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ @@ -104,6 +116,6 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - }, + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } } diff --git a/backend/yarn.lock b/backend/yarn.lock index ab611aea5..f5164b1a9 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -64,21 +64,579 @@ "@csstools/css-tokenizer" "^3.0.3" lru-cache "^10.4.3" -"@babel/cli@~7.27.0": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.27.0.tgz#076603b25fc7dd88298ea94ab249c8237c7e71cc" - integrity sha512-bZfxn8DRxwiVzDO5CEeV+7IqXeCkzI4yYnrQbpwjT76CUyossQc6RYE7n+xfm0/2k40lPaCpW0FhxYs7EBAetw== +"@aws-crypto/crc32@5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/crc32/-/crc32-5.2.0.tgz#cfcc22570949c98c6689cfcbd2d693d36cdae2e1" + integrity sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg== dependencies: - "@jridgewell/trace-mapping" "^0.3.25" - commander "^6.2.0" - convert-source-map "^2.0.0" - fs-readdir-recursive "^1.1.0" - glob "^7.2.0" - make-dir "^2.1.0" - slash "^2.0.0" - optionalDependencies: - "@nicolo-ribaudo/chokidar-2" "2.1.8-no-fsevents.3" - chokidar "^3.6.0" + "@aws-crypto/util" "^5.2.0" + "@aws-sdk/types" "^3.222.0" + tslib "^2.6.2" + +"@aws-crypto/crc32c@5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz#4e34aab7f419307821509a98b9b08e84e0c1917e" + integrity sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag== + dependencies: + "@aws-crypto/util" "^5.2.0" + "@aws-sdk/types" "^3.222.0" + tslib "^2.6.2" + +"@aws-crypto/sha1-browser@5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz#b0ee2d2821d3861f017e965ef3b4cb38e3b6a0f4" + integrity sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg== + dependencies: + "@aws-crypto/supports-web-crypto" "^5.2.0" + "@aws-crypto/util" "^5.2.0" + "@aws-sdk/types" "^3.222.0" + "@aws-sdk/util-locate-window" "^3.0.0" + "@smithy/util-utf8" "^2.0.0" + tslib "^2.6.2" + +"@aws-crypto/sha256-browser@5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz#153895ef1dba6f9fce38af550e0ef58988eb649e" + integrity sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw== + dependencies: + "@aws-crypto/sha256-js" "^5.2.0" + "@aws-crypto/supports-web-crypto" "^5.2.0" + "@aws-crypto/util" "^5.2.0" + "@aws-sdk/types" "^3.222.0" + "@aws-sdk/util-locate-window" "^3.0.0" + "@smithy/util-utf8" "^2.0.0" + tslib "^2.6.2" + +"@aws-crypto/sha256-js@5.2.0", "@aws-crypto/sha256-js@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz#c4fdb773fdbed9a664fc1a95724e206cf3860042" + integrity sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA== + dependencies: + "@aws-crypto/util" "^5.2.0" + "@aws-sdk/types" "^3.222.0" + tslib "^2.6.2" + +"@aws-crypto/supports-web-crypto@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz#a1e399af29269be08e695109aa15da0a07b5b5fb" + integrity sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg== + dependencies: + tslib "^2.6.2" + +"@aws-crypto/util@5.2.0", "@aws-crypto/util@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/util/-/util-5.2.0.tgz#71284c9cffe7927ddadac793c14f14886d3876da" + integrity sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ== + dependencies: + "@aws-sdk/types" "^3.222.0" + "@smithy/util-utf8" "^2.0.0" + tslib "^2.6.2" + +"@aws-sdk/client-s3@^3.826.0": + version "3.826.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.826.0.tgz#e6fdc589d5691f2c1212653ec6bb37f47b383897" + integrity sha512-odX3C3CEbcBoxB06vgBjJ9jQheFsIFwHmvCIMXn8duuVyIL/klgp14+ICzbEwIgPv7xVjSlycaiURcKS876QHA== + dependencies: + "@aws-crypto/sha1-browser" "5.2.0" + "@aws-crypto/sha256-browser" "5.2.0" + "@aws-crypto/sha256-js" "5.2.0" + "@aws-sdk/core" "3.826.0" + "@aws-sdk/credential-provider-node" "3.826.0" + "@aws-sdk/middleware-bucket-endpoint" "3.821.0" + "@aws-sdk/middleware-expect-continue" "3.821.0" + "@aws-sdk/middleware-flexible-checksums" "3.826.0" + "@aws-sdk/middleware-host-header" "3.821.0" + "@aws-sdk/middleware-location-constraint" "3.821.0" + "@aws-sdk/middleware-logger" "3.821.0" + "@aws-sdk/middleware-recursion-detection" "3.821.0" + "@aws-sdk/middleware-sdk-s3" "3.826.0" + "@aws-sdk/middleware-ssec" "3.821.0" + "@aws-sdk/middleware-user-agent" "3.826.0" + "@aws-sdk/region-config-resolver" "3.821.0" + "@aws-sdk/signature-v4-multi-region" "3.826.0" + "@aws-sdk/types" "3.821.0" + "@aws-sdk/util-endpoints" "3.821.0" + "@aws-sdk/util-user-agent-browser" "3.821.0" + "@aws-sdk/util-user-agent-node" "3.826.0" + "@aws-sdk/xml-builder" "3.821.0" + "@smithy/config-resolver" "^4.1.4" + "@smithy/core" "^3.5.3" + "@smithy/eventstream-serde-browser" "^4.0.4" + "@smithy/eventstream-serde-config-resolver" "^4.1.2" + "@smithy/eventstream-serde-node" "^4.0.4" + "@smithy/fetch-http-handler" "^5.0.4" + "@smithy/hash-blob-browser" "^4.0.4" + "@smithy/hash-node" "^4.0.4" + "@smithy/hash-stream-node" "^4.0.4" + "@smithy/invalid-dependency" "^4.0.4" + "@smithy/md5-js" "^4.0.4" + "@smithy/middleware-content-length" "^4.0.4" + "@smithy/middleware-endpoint" "^4.1.11" + "@smithy/middleware-retry" "^4.1.12" + "@smithy/middleware-serde" "^4.0.8" + "@smithy/middleware-stack" "^4.0.4" + "@smithy/node-config-provider" "^4.1.3" + "@smithy/node-http-handler" "^4.0.6" + "@smithy/protocol-http" "^5.1.2" + "@smithy/smithy-client" "^4.4.3" + "@smithy/types" "^4.3.1" + "@smithy/url-parser" "^4.0.4" + "@smithy/util-base64" "^4.0.0" + "@smithy/util-body-length-browser" "^4.0.0" + "@smithy/util-body-length-node" "^4.0.0" + "@smithy/util-defaults-mode-browser" "^4.0.19" + "@smithy/util-defaults-mode-node" "^4.0.19" + "@smithy/util-endpoints" "^3.0.6" + "@smithy/util-middleware" "^4.0.4" + "@smithy/util-retry" "^4.0.5" + "@smithy/util-stream" "^4.2.2" + "@smithy/util-utf8" "^4.0.0" + "@smithy/util-waiter" "^4.0.5" + tslib "^2.6.2" + +"@aws-sdk/client-sso@3.826.0": + version "3.826.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.826.0.tgz#8e0aeb3d830c2cf54ecf43d6052ae9bfd75b276b" + integrity sha512-/FEKnUC3xPkLL4RuRydwzx+y4b55HIX6qLPbGnyIs+sNmCUyc/62ijtV1Ml+b++YzEF6jWNBsJOxeyZdgrJ3Ig== + dependencies: + "@aws-crypto/sha256-browser" "5.2.0" + "@aws-crypto/sha256-js" "5.2.0" + "@aws-sdk/core" "3.826.0" + "@aws-sdk/middleware-host-header" "3.821.0" + "@aws-sdk/middleware-logger" "3.821.0" + "@aws-sdk/middleware-recursion-detection" "3.821.0" + "@aws-sdk/middleware-user-agent" "3.826.0" + "@aws-sdk/region-config-resolver" "3.821.0" + "@aws-sdk/types" "3.821.0" + "@aws-sdk/util-endpoints" "3.821.0" + "@aws-sdk/util-user-agent-browser" "3.821.0" + "@aws-sdk/util-user-agent-node" "3.826.0" + "@smithy/config-resolver" "^4.1.4" + "@smithy/core" "^3.5.3" + "@smithy/fetch-http-handler" "^5.0.4" + "@smithy/hash-node" "^4.0.4" + "@smithy/invalid-dependency" "^4.0.4" + "@smithy/middleware-content-length" "^4.0.4" + "@smithy/middleware-endpoint" "^4.1.11" + "@smithy/middleware-retry" "^4.1.12" + "@smithy/middleware-serde" "^4.0.8" + "@smithy/middleware-stack" "^4.0.4" + "@smithy/node-config-provider" "^4.1.3" + "@smithy/node-http-handler" "^4.0.6" + "@smithy/protocol-http" "^5.1.2" + "@smithy/smithy-client" "^4.4.3" + "@smithy/types" "^4.3.1" + "@smithy/url-parser" "^4.0.4" + "@smithy/util-base64" "^4.0.0" + "@smithy/util-body-length-browser" "^4.0.0" + "@smithy/util-body-length-node" "^4.0.0" + "@smithy/util-defaults-mode-browser" "^4.0.19" + "@smithy/util-defaults-mode-node" "^4.0.19" + "@smithy/util-endpoints" "^3.0.6" + "@smithy/util-middleware" "^4.0.4" + "@smithy/util-retry" "^4.0.5" + "@smithy/util-utf8" "^4.0.0" + tslib "^2.6.2" + +"@aws-sdk/core@3.826.0": + version "3.826.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.826.0.tgz#da55a524e09775b2a97e4b5d12a3137dd68547fa" + integrity sha512-BGbQYzWj3ps+dblq33FY5tz/SsgJCcXX0zjQlSC07tYvU1jHTUvsefphyig+fY38xZ4wdKjbTop+KUmXUYrOXw== + dependencies: + "@aws-sdk/types" "3.821.0" + "@aws-sdk/xml-builder" "3.821.0" + "@smithy/core" "^3.5.3" + "@smithy/node-config-provider" "^4.1.3" + "@smithy/property-provider" "^4.0.4" + "@smithy/protocol-http" "^5.1.2" + "@smithy/signature-v4" "^5.1.2" + "@smithy/smithy-client" "^4.4.3" + "@smithy/types" "^4.3.1" + "@smithy/util-base64" "^4.0.0" + "@smithy/util-body-length-browser" "^4.0.0" + "@smithy/util-middleware" "^4.0.4" + "@smithy/util-utf8" "^4.0.0" + fast-xml-parser "4.4.1" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-env@3.826.0": + version "3.826.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.826.0.tgz#213d08a1324a2970a2785151bcb6975b2f88716c" + integrity sha512-DK3pQY8+iKK3MGDdC3uOZQ2psU01obaKlTYhEwNu4VWzgwQL4Vi3sWj4xSWGEK41vqZxiRLq6fOq7ysRI+qEZA== + dependencies: + "@aws-sdk/core" "3.826.0" + "@aws-sdk/types" "3.821.0" + "@smithy/property-provider" "^4.0.4" + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-http@3.826.0": + version "3.826.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.826.0.tgz#507591b684b3ed8d24cfa179995c1f93efc914cc" + integrity sha512-N+IVZBh+yx/9GbMZTKO/gErBi/FYZQtcFRItoLbY+6WU+0cSWyZYfkoeOxHmQV3iX9k65oljERIWUmL9x6OSQg== + dependencies: + "@aws-sdk/core" "3.826.0" + "@aws-sdk/types" "3.821.0" + "@smithy/fetch-http-handler" "^5.0.4" + "@smithy/node-http-handler" "^4.0.6" + "@smithy/property-provider" "^4.0.4" + "@smithy/protocol-http" "^5.1.2" + "@smithy/smithy-client" "^4.4.3" + "@smithy/types" "^4.3.1" + "@smithy/util-stream" "^4.2.2" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-ini@3.826.0": + version "3.826.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.826.0.tgz#806bb1287d88e75b1c1679308f555dfaf313fee3" + integrity sha512-g7n+qSklq/Lzjxe2Ke5QFNCgYn26a3ydZnbFIk8QqYin4pzG+qiunaqJjpV3c/EeHMlfK8bBc7MXAylKzGRccQ== + dependencies: + "@aws-sdk/core" "3.826.0" + "@aws-sdk/credential-provider-env" "3.826.0" + "@aws-sdk/credential-provider-http" "3.826.0" + "@aws-sdk/credential-provider-process" "3.826.0" + "@aws-sdk/credential-provider-sso" "3.826.0" + "@aws-sdk/credential-provider-web-identity" "3.826.0" + "@aws-sdk/nested-clients" "3.826.0" + "@aws-sdk/types" "3.821.0" + "@smithy/credential-provider-imds" "^4.0.6" + "@smithy/property-provider" "^4.0.4" + "@smithy/shared-ini-file-loader" "^4.0.4" + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-node@3.826.0": + version "3.826.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.826.0.tgz#eab3b67bb0b99ee47b87174dd84b1cc7ceaffb3d" + integrity sha512-UfIJXxHjmSxH6bea00HBPLkjNI2D04enQA/xNLZvB+4xtzt1/gYdCis1P4/73f5aGVVVB4/zQMobBbnjkrmbQw== + dependencies: + "@aws-sdk/credential-provider-env" "3.826.0" + "@aws-sdk/credential-provider-http" "3.826.0" + "@aws-sdk/credential-provider-ini" "3.826.0" + "@aws-sdk/credential-provider-process" "3.826.0" + "@aws-sdk/credential-provider-sso" "3.826.0" + "@aws-sdk/credential-provider-web-identity" "3.826.0" + "@aws-sdk/types" "3.821.0" + "@smithy/credential-provider-imds" "^4.0.6" + "@smithy/property-provider" "^4.0.4" + "@smithy/shared-ini-file-loader" "^4.0.4" + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-process@3.826.0": + version "3.826.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.826.0.tgz#3b7e54994cf04c8ba20a90caf4f79af9f1335ea4" + integrity sha512-kURrc4amu3NLtw1yZw7EoLNEVhmOMRUTs+chaNcmS+ERm3yK0nKjaJzmKahmwlTQTSl3wJ8jjK7x962VPo+zWw== + dependencies: + "@aws-sdk/core" "3.826.0" + "@aws-sdk/types" "3.821.0" + "@smithy/property-provider" "^4.0.4" + "@smithy/shared-ini-file-loader" "^4.0.4" + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-sso@3.826.0": + version "3.826.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.826.0.tgz#1daab189052eff0b7bfed934a0be19a89912d894" + integrity sha512-F19J3zcfoom6OnQ0MyAtvduVKQXPgkz9i5ExSO01J2CzjbyMhCDA99qAjHYe+LwhW+W7P/jzBPd0+uOQ2Nhh9Q== + dependencies: + "@aws-sdk/client-sso" "3.826.0" + "@aws-sdk/core" "3.826.0" + "@aws-sdk/token-providers" "3.826.0" + "@aws-sdk/types" "3.821.0" + "@smithy/property-provider" "^4.0.4" + "@smithy/shared-ini-file-loader" "^4.0.4" + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-web-identity@3.826.0": + version "3.826.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.826.0.tgz#0c02845b2af3eb22bc3ef795b2edf17892021cb2" + integrity sha512-o27GZ6Hy7qhuvMFVUL2eFEpBzf33Jaa/x3u3SHwU0nL7ko7jmbpeF0x4+wmagpI9X2IvVlUxIs0VaQ3YayPLEA== + dependencies: + "@aws-sdk/core" "3.826.0" + "@aws-sdk/nested-clients" "3.826.0" + "@aws-sdk/types" "3.821.0" + "@smithy/property-provider" "^4.0.4" + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@aws-sdk/lib-storage@^3.817.0": + version "3.817.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/lib-storage/-/lib-storage-3.817.0.tgz#579f330a22175ccc7aa1366fcfb2432001028680" + integrity sha512-2zOO8+2EmiS049PjLSNdqmmZMQj7fzE1hZJ70A94vO+KNaVhVZYuMOOiOmwMw6ePkTCcFwK40vZIIXwEQQ1v1g== + dependencies: + "@smithy/abort-controller" "^4.0.2" + "@smithy/middleware-endpoint" "^4.1.6" + "@smithy/smithy-client" "^4.2.6" + buffer "5.6.0" + events "3.3.0" + stream-browserify "3.0.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-bucket-endpoint@3.821.0": + version "3.821.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.821.0.tgz#2d7658e5b20a7712624ea33fa579a6be0339028b" + integrity sha512-cebgeytKlWOgGczLo3BPvNY9XlzAzGZQANSysgJ2/8PSldmUpXRIF+GKPXDVhXeInWYHIfB8zZi3RqrPoXcNYQ== + dependencies: + "@aws-sdk/types" "3.821.0" + "@aws-sdk/util-arn-parser" "3.804.0" + "@smithy/node-config-provider" "^4.1.3" + "@smithy/protocol-http" "^5.1.2" + "@smithy/types" "^4.3.1" + "@smithy/util-config-provider" "^4.0.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-expect-continue@3.821.0": + version "3.821.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.821.0.tgz#de4e8f5ca3727dd2dd802685aa3342ceb4e662e3" + integrity sha512-zAOoSZKe1njOrtynvK6ZORU57YGv5I7KP4+rwOvUN3ZhJbQ7QPf8gKtFUCYAPRMegaXCKF/ADPtDZBAmM+zZ9g== + dependencies: + "@aws-sdk/types" "3.821.0" + "@smithy/protocol-http" "^5.1.2" + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@aws-sdk/middleware-flexible-checksums@3.826.0": + version "3.826.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.826.0.tgz#e6764d9bdf9408b4a3e20148a83d83e0f65b370e" + integrity sha512-Fz9w8CFYPfSlHEB6feSsi06hdS+s+FB8k5pO4L7IV0tUa78mlhxF/VNlAJaVWYyOkZXl4HPH2K48aapACSQOXw== + dependencies: + "@aws-crypto/crc32" "5.2.0" + "@aws-crypto/crc32c" "5.2.0" + "@aws-crypto/util" "5.2.0" + "@aws-sdk/core" "3.826.0" + "@aws-sdk/types" "3.821.0" + "@smithy/is-array-buffer" "^4.0.0" + "@smithy/node-config-provider" "^4.1.3" + "@smithy/protocol-http" "^5.1.2" + "@smithy/types" "^4.3.1" + "@smithy/util-middleware" "^4.0.4" + "@smithy/util-stream" "^4.2.2" + "@smithy/util-utf8" "^4.0.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-host-header@3.821.0": + version "3.821.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.821.0.tgz#1dfda8da4e0f9499648dab9a989d10706e289cc7" + integrity sha512-xSMR+sopSeWGx5/4pAGhhfMvGBHioVBbqGvDs6pG64xfNwM5vq5s5v6D04e2i+uSTj4qGa71dLUs5I0UzAK3sw== + dependencies: + "@aws-sdk/types" "3.821.0" + "@smithy/protocol-http" "^5.1.2" + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@aws-sdk/middleware-location-constraint@3.821.0": + version "3.821.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.821.0.tgz#9a5b52f8874f48274e89329aa3d45a55340d267e" + integrity sha512-sKrm80k0t3R0on8aA/WhWFoMaAl4yvdk+riotmMElLUpcMcRXAd1+600uFVrxJqZdbrKQ0mjX0PjT68DlkYXLg== + dependencies: + "@aws-sdk/types" "3.821.0" + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@aws-sdk/middleware-logger@3.821.0": + version "3.821.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.821.0.tgz#87067907a25cdc6c155d3a35fe32e399c1ef87e6" + integrity sha512-0cvI0ipf2tGx7fXYEEN5fBeZDz2RnHyb9xftSgUsEq7NBxjV0yTZfLJw6Za5rjE6snC80dRN8+bTNR1tuG89zA== + dependencies: + "@aws-sdk/types" "3.821.0" + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@aws-sdk/middleware-recursion-detection@3.821.0": + version "3.821.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.821.0.tgz#bc34b08efc1e1af7b14a58023a79bfb75a0b64fa" + integrity sha512-efmaifbhBoqKG3bAoEfDdcM8hn1psF+4qa7ykWuYmfmah59JBeqHLfz5W9m9JoTwoKPkFcVLWZxnyZzAnVBOIg== + dependencies: + "@aws-sdk/types" "3.821.0" + "@smithy/protocol-http" "^5.1.2" + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@aws-sdk/middleware-sdk-s3@3.826.0": + version "3.826.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.826.0.tgz#98392d5729f8df62af21d3144bacdc9ec65065d1" + integrity sha512-8F0qWaYKfvD/de1AKccXuigM+gb/IZSncCqxdnFWqd+TFzo9qI9Hh+TpUhWOMYSgxsMsYQ8ipmLzlD/lDhjrmA== + dependencies: + "@aws-sdk/core" "3.826.0" + "@aws-sdk/types" "3.821.0" + "@aws-sdk/util-arn-parser" "3.804.0" + "@smithy/core" "^3.5.3" + "@smithy/node-config-provider" "^4.1.3" + "@smithy/protocol-http" "^5.1.2" + "@smithy/signature-v4" "^5.1.2" + "@smithy/smithy-client" "^4.4.3" + "@smithy/types" "^4.3.1" + "@smithy/util-config-provider" "^4.0.0" + "@smithy/util-middleware" "^4.0.4" + "@smithy/util-stream" "^4.2.2" + "@smithy/util-utf8" "^4.0.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-ssec@3.821.0": + version "3.821.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-ssec/-/middleware-ssec-3.821.0.tgz#4d4c2ba22e5bbf5ac618a1f679cbe256eaaf3d35" + integrity sha512-YYi1Hhr2AYiU/24cQc8HIB+SWbQo6FBkMYojVuz/zgrtkFmALxENGF/21OPg7f/QWd+eadZJRxCjmRwh5F2Cxg== + dependencies: + "@aws-sdk/types" "3.821.0" + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@aws-sdk/middleware-user-agent@3.826.0": + version "3.826.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.826.0.tgz#715ef8f7207eeb0c66b5dd31f72e8a1bdc18c994" + integrity sha512-j404+EcfBbtTlAhyObjXbdKwwDXO1pCxHvR5Fw8FXNvp/H330j6YnXgs3SJ6d3bZUwUJ/ztPx2S5AlBbLVLDFw== + dependencies: + "@aws-sdk/core" "3.826.0" + "@aws-sdk/types" "3.821.0" + "@aws-sdk/util-endpoints" "3.821.0" + "@smithy/core" "^3.5.3" + "@smithy/protocol-http" "^5.1.2" + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@aws-sdk/nested-clients@3.826.0": + version "3.826.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/nested-clients/-/nested-clients-3.826.0.tgz#ef7eaf6546dc7f04187f74a297f6f6d57eb9d8cc" + integrity sha512-p7olPq0uTtHqGuXI1GSc/gzKDvV55PMbLtnmupEDfnY9SoRu+QatbWQ6da9sI1lhOcNmRMgiNQBXFzaUFrG+SQ== + dependencies: + "@aws-crypto/sha256-browser" "5.2.0" + "@aws-crypto/sha256-js" "5.2.0" + "@aws-sdk/core" "3.826.0" + "@aws-sdk/middleware-host-header" "3.821.0" + "@aws-sdk/middleware-logger" "3.821.0" + "@aws-sdk/middleware-recursion-detection" "3.821.0" + "@aws-sdk/middleware-user-agent" "3.826.0" + "@aws-sdk/region-config-resolver" "3.821.0" + "@aws-sdk/types" "3.821.0" + "@aws-sdk/util-endpoints" "3.821.0" + "@aws-sdk/util-user-agent-browser" "3.821.0" + "@aws-sdk/util-user-agent-node" "3.826.0" + "@smithy/config-resolver" "^4.1.4" + "@smithy/core" "^3.5.3" + "@smithy/fetch-http-handler" "^5.0.4" + "@smithy/hash-node" "^4.0.4" + "@smithy/invalid-dependency" "^4.0.4" + "@smithy/middleware-content-length" "^4.0.4" + "@smithy/middleware-endpoint" "^4.1.11" + "@smithy/middleware-retry" "^4.1.12" + "@smithy/middleware-serde" "^4.0.8" + "@smithy/middleware-stack" "^4.0.4" + "@smithy/node-config-provider" "^4.1.3" + "@smithy/node-http-handler" "^4.0.6" + "@smithy/protocol-http" "^5.1.2" + "@smithy/smithy-client" "^4.4.3" + "@smithy/types" "^4.3.1" + "@smithy/url-parser" "^4.0.4" + "@smithy/util-base64" "^4.0.0" + "@smithy/util-body-length-browser" "^4.0.0" + "@smithy/util-body-length-node" "^4.0.0" + "@smithy/util-defaults-mode-browser" "^4.0.19" + "@smithy/util-defaults-mode-node" "^4.0.19" + "@smithy/util-endpoints" "^3.0.6" + "@smithy/util-middleware" "^4.0.4" + "@smithy/util-retry" "^4.0.5" + "@smithy/util-utf8" "^4.0.0" + tslib "^2.6.2" + +"@aws-sdk/region-config-resolver@3.821.0": + version "3.821.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/region-config-resolver/-/region-config-resolver-3.821.0.tgz#2f1cd54ca140cbdc821a604d8b20444f9b0b77cf" + integrity sha512-t8og+lRCIIy5nlId0bScNpCkif8sc0LhmtaKsbm0ZPm3sCa/WhCbSZibjbZ28FNjVCV+p0D9RYZx0VDDbtWyjw== + dependencies: + "@aws-sdk/types" "3.821.0" + "@smithy/node-config-provider" "^4.1.3" + "@smithy/types" "^4.3.1" + "@smithy/util-config-provider" "^4.0.0" + "@smithy/util-middleware" "^4.0.4" + tslib "^2.6.2" + +"@aws-sdk/signature-v4-multi-region@3.826.0": + version "3.826.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.826.0.tgz#14a786feee118abc7b1c1b655f46dc54cff497cc" + integrity sha512-3fEi/zy6tpMzomYosksGtu7jZqGFcdBXoL7YRsG7OEeQzBbOW9B+fVaQZ4jnsViSjzA/yKydLahMrfPnt+iaxg== + dependencies: + "@aws-sdk/middleware-sdk-s3" "3.826.0" + "@aws-sdk/types" "3.821.0" + "@smithy/protocol-http" "^5.1.2" + "@smithy/signature-v4" "^5.1.2" + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@aws-sdk/token-providers@3.826.0": + version "3.826.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.826.0.tgz#c6901e2d92e11b6c9cdc395b7dab3eb68200cbec" + integrity sha512-iCOcVAqGPSHtQL8ZBXifZMEcHyUl9wJ8HvLZ5l1ohA/3ZNP+dqEPGi7jfhR5jZKs+xyp2jxByFqfil9PjI9c5A== + dependencies: + "@aws-sdk/core" "3.826.0" + "@aws-sdk/nested-clients" "3.826.0" + "@aws-sdk/types" "3.821.0" + "@smithy/property-provider" "^4.0.4" + "@smithy/shared-ini-file-loader" "^4.0.4" + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@aws-sdk/types@3.821.0", "@aws-sdk/types@^3.222.0": + version "3.821.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.821.0.tgz#edfd4595208e4e9f24f397fbc8cb82e3ec336649" + integrity sha512-Znroqdai1a90TlxGaJ+FK1lwC0fHpo97Xjsp5UKGR5JODYm7f9+/fF17ebO1KdoBr/Rm0UIFiF5VmI8ts9F1eA== + dependencies: + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@aws-sdk/util-arn-parser@3.804.0": + version "3.804.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-arn-parser/-/util-arn-parser-3.804.0.tgz#d0b52bf5f9ae5b2c357a635551e5844dcad074c8" + integrity sha512-wmBJqn1DRXnZu3b4EkE6CWnoWMo1ZMvlfkqU5zPz67xx1GMaXlDCchFvKAXMjk4jn/L1O3tKnoFDNsoLV1kgNQ== + dependencies: + tslib "^2.6.2" + +"@aws-sdk/util-endpoints@3.821.0": + version "3.821.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.821.0.tgz#8883370bc3218e532fb9b7358e23369dc0a77201" + integrity sha512-Uknt/zUZnLE76zaAAPEayOeF5/4IZ2puTFXvcSCWHsi9m3tqbb9UozlnlVqvCZLCRWfQryZQoG2W4XSS3qgk5A== + dependencies: + "@aws-sdk/types" "3.821.0" + "@smithy/types" "^4.3.1" + "@smithy/util-endpoints" "^3.0.6" + tslib "^2.6.2" + +"@aws-sdk/util-locate-window@^3.0.0": + version "3.804.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-locate-window/-/util-locate-window-3.804.0.tgz#a2ee8dc5d9c98276986e8e1ba03c0c84d9afb0f5" + integrity sha512-zVoRfpmBVPodYlnMjgVjfGoEZagyRF5IPn3Uo6ZvOZp24chnW/FRstH7ESDHDDRga4z3V+ElUQHKpFDXWyBW5A== + dependencies: + tslib "^2.6.2" + +"@aws-sdk/util-user-agent-browser@3.821.0": + version "3.821.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.821.0.tgz#32962fd3ae20986da128944b88a231508e017f5b" + integrity sha512-irWZHyM0Jr1xhC+38OuZ7JB6OXMLPZlj48thElpsO1ZSLRkLZx5+I7VV6k3sp2yZ7BYbKz/G2ojSv4wdm7XTLw== + dependencies: + "@aws-sdk/types" "3.821.0" + "@smithy/types" "^4.3.1" + bowser "^2.11.0" + tslib "^2.6.2" + +"@aws-sdk/util-user-agent-node@3.826.0": + version "3.826.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.826.0.tgz#dab7b0865545db4a0f60e3b89a51ce2e8ce8b12b" + integrity sha512-wHw6bZQWIMcFF/8r03aY9Itp6JLBYY4absGGhCDK1dc3tPEfi8NVSdb05a/Oz+g4TVaDdxLo0OQ/OKMS1DFRHQ== + dependencies: + "@aws-sdk/middleware-user-agent" "3.826.0" + "@aws-sdk/types" "3.821.0" + "@smithy/node-config-provider" "^4.1.3" + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@aws-sdk/xml-builder@3.821.0": + version "3.821.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.821.0.tgz#ff89bf1276fca41276ed508b9c8ae21978d91177" + integrity sha512-DIIotRnefVL6DiaHtO6/21DhJ4JZnnIwdNbpwiAhdt/AVbttcE4yw925gsjur0OGv5BTYXQXU3YnANBYnZjuQA== + dependencies: + "@smithy/types" "^4.3.1" + tslib "^2.6.2" "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.26.2": version "7.26.2" @@ -89,12 +647,12 @@ js-tokens "^4.0.0" picocolors "^1.0.0" -"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.26.5", "@babel/compat-data@^7.26.8": +"@babel/compat-data@^7.26.5": version "7.26.8" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.8.tgz#821c1d35641c355284d4a870b8a4a7b0c141e367" integrity sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ== -"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.23.9", "@babel/core@^7.26.10": +"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.23.9": version "7.26.10" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.10.tgz#5c876f83c8c4dcb233ee4b670c0606f2ac3000f9" integrity sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ== @@ -126,21 +684,7 @@ "@jridgewell/trace-mapping" "^0.3.25" jsesc "^3.0.2" -"@babel/helper-annotate-as-pure@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" - integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg== - dependencies: - "@babel/types" "^7.22.5" - -"@babel/helper-annotate-as-pure@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz#d8eac4d2dc0d7b6e11fa6e535332e0d3184f06b4" - integrity sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g== - dependencies: - "@babel/types" "^7.25.9" - -"@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.25.9", "@babel/helper-compilation-targets@^7.26.5": +"@babel/helper-compilation-targets@^7.26.5": version "7.26.5" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz#75d92bb8d8d51301c0d49e52a65c9a7fe94514d8" integrity sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA== @@ -151,67 +695,6 @@ lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-create-class-features-plugin@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz#7644147706bb90ff613297d49ed5266bde729f83" - integrity sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.25.9" - "@babel/helper-member-expression-to-functions" "^7.25.9" - "@babel/helper-optimise-call-expression" "^7.25.9" - "@babel/helper-replace-supers" "^7.25.9" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" - "@babel/traverse" "^7.25.9" - semver "^6.3.1" - -"@babel/helper-create-regexp-features-plugin@^7.18.6": - version "7.22.9" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.9.tgz#9d8e61a8d9366fe66198f57c40565663de0825f6" - integrity sha512-+svjVa/tFwsNSG4NEy1h85+HQ5imbT92Q5/bgtS7P0GTQlP8WuFdqsiABmQouhiFGyV66oGxZFpeYHza1rNsKw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - regexpu-core "^5.3.1" - semver "^6.3.1" - -"@babel/helper-create-regexp-features-plugin@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.9.tgz#3e8999db94728ad2b2458d7a470e7770b7764e26" - integrity sha512-ORPNZ3h6ZRkOyAa/SaHU+XsLZr0UQzRwuDQ0cczIA17nAzZ+85G5cVkOJIj7QavLZGSe8QXUmNFxSZzjcZF9bw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.25.9" - regexpu-core "^6.1.1" - semver "^6.3.1" - -"@babel/helper-define-polyfill-provider@^0.6.1": - version "0.6.1" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.1.tgz#fadc63f0c2ff3c8d02ed905dcea747c5b0fb74fd" - integrity sha512-o7SDgTJuvx5vLKD6SFvkydkSMBvahDKGiNJzG22IZYXhiqoe9efY7zocICBgzHV4IRg5wdgl2nEL/tulKIEIbA== - dependencies: - "@babel/helper-compilation-targets" "^7.22.6" - "@babel/helper-plugin-utils" "^7.22.5" - debug "^4.1.1" - lodash.debounce "^4.0.8" - resolve "^1.14.2" - -"@babel/helper-define-polyfill-provider@^0.6.3": - version "0.6.3" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz#f4f2792fae2ef382074bc2d713522cf24e6ddb21" - integrity sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg== - dependencies: - "@babel/helper-compilation-targets" "^7.22.6" - "@babel/helper-plugin-utils" "^7.22.5" - debug "^4.1.1" - lodash.debounce "^4.0.8" - resolve "^1.14.2" - -"@babel/helper-member-expression-to-functions@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz#9dfffe46f727005a5ea29051ac835fb735e4c1a3" - integrity sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ== - dependencies: - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.25.9" - "@babel/helper-module-imports@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz#e7f8d20602ebdbf9ebbea0a0751fb0f2a4141715" @@ -220,7 +703,7 @@ "@babel/traverse" "^7.25.9" "@babel/types" "^7.25.9" -"@babel/helper-module-transforms@^7.25.9", "@babel/helper-module-transforms@^7.26.0": +"@babel/helper-module-transforms@^7.26.0": version "7.26.0" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz#8ce54ec9d592695e58d84cd884b7b5c6a2fdeeae" integrity sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw== @@ -229,44 +712,11 @@ "@babel/helper-validator-identifier" "^7.25.9" "@babel/traverse" "^7.25.9" -"@babel/helper-optimise-call-expression@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz#3324ae50bae7e2ab3c33f60c9a877b6a0146b54e" - integrity sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ== - dependencies: - "@babel/types" "^7.25.9" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.24.0", "@babel/helper-plugin-utils@^7.25.9", "@babel/helper-plugin-utils@^7.26.5", "@babel/helper-plugin-utils@^7.8.0": +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.24.0", "@babel/helper-plugin-utils@^7.8.0": version "7.26.5" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz#18580d00c9934117ad719392c4f6585c9333cc35" integrity sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg== -"@babel/helper-remap-async-to-generator@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz#e53956ab3d5b9fb88be04b3e2f31b523afd34b92" - integrity sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.25.9" - "@babel/helper-wrap-function" "^7.25.9" - "@babel/traverse" "^7.25.9" - -"@babel/helper-replace-supers@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz#ba447224798c3da3f8713fc272b145e33da6a5c5" - integrity sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ== - dependencies: - "@babel/helper-member-expression-to-functions" "^7.25.9" - "@babel/helper-optimise-call-expression" "^7.25.9" - "@babel/traverse" "^7.25.9" - -"@babel/helper-skip-transparent-expression-wrappers@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz#0b2e1b62d560d6b1954893fd2b705dc17c91f0c9" - integrity sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA== - dependencies: - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.25.9" - "@babel/helper-string-parser@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" @@ -282,15 +732,6 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72" integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw== -"@babel/helper-wrap-function@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz#d99dfd595312e6c894bd7d237470025c85eea9d0" - integrity sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g== - dependencies: - "@babel/template" "^7.25.9" - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.25.9" - "@babel/helpers@^7.26.10": version "7.27.0" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.27.0.tgz#53d156098defa8243eab0f32fa17589075a1b808" @@ -299,76 +740,13 @@ "@babel/template" "^7.27.0" "@babel/types" "^7.27.0" -"@babel/node@~7.26.0": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/node/-/node-7.26.0.tgz#4b126f4eb56a05a97990a650fae0a24ad9b0c583" - integrity sha512-5ASMjh42hbnqyCOK68Q5chh1jKAqn91IswFTN+niwt4FLABhEWCT1tEuuo6mlNQ4WG/oFQLvJ71PaHAKtWtJyA== - dependencies: - "@babel/register" "^7.25.9" - commander "^6.2.0" - core-js "^3.30.2" - node-environment-flags "^1.0.5" - regenerator-runtime "^0.14.0" - v8flags "^3.1.1" - -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.26.10", "@babel/parser@^7.27.0", "@babel/parser@^7.7.0": +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.26.10", "@babel/parser@^7.27.0", "@babel/parser@^7.6.0", "@babel/parser@^7.9.6": version "7.27.0" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.0.tgz#3d7d6ee268e41d2600091cbd4e145ffee85a44ec" integrity sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg== dependencies: "@babel/types" "^7.27.0" -"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz#cc2e53ebf0a0340777fff5ed521943e253b4d8fe" - integrity sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/traverse" "^7.25.9" - -"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz#af9e4fb63ccb8abcb92375b2fcfe36b60c774d30" - integrity sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz#e8dc26fcd616e6c5bf2bd0d5a2c151d4f92a9137" - integrity sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz#807a667f9158acac6f6164b4beb85ad9ebc9e1d1" - integrity sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" - "@babel/plugin-transform-optional-chaining" "^7.25.9" - -"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz#de7093f1e7deaf68eadd7cc6b07f2ab82543269e" - integrity sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/traverse" "^7.25.9" - -"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": - version "7.21.0-placeholder-for-preset-env.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" - integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== - -"@babel/plugin-proposal-throw-expressions@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-throw-expressions/-/plugin-proposal-throw-expressions-7.25.9.tgz#9bfba4a4b775dbadfc5ca91a9f22097754142b56" - integrity sha512-Zw62DP6cdbXXEtTNMWYY10rIOPGAWPk8qdqM+AT3JbHtFq8ook0JXJCWdQJTlSVACHo0R6lvoNKO9B1ZVkjClg== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" @@ -390,20 +768,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.12.13" -"@babel/plugin-syntax-import-assertions@^7.26.0": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz#620412405058efa56e4a564903b79355020f445f" - integrity sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-syntax-import-attributes@^7.26.0": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz#3b1412847699eea739b4f2602c74ce36f6b0b0f7" - integrity sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/plugin-syntax-import-meta@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" @@ -481,501 +845,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.19.0" -"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" - integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-arrow-functions@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz#7821d4410bee5daaadbb4cdd9a6649704e176845" - integrity sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-async-generator-functions@^7.26.8": - version "7.26.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.26.8.tgz#5e3991135e3b9c6eaaf5eff56d1ae5a11df45ff8" - integrity sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg== - dependencies: - "@babel/helper-plugin-utils" "^7.26.5" - "@babel/helper-remap-async-to-generator" "^7.25.9" - "@babel/traverse" "^7.26.8" - -"@babel/plugin-transform-async-to-generator@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz#c80008dacae51482793e5a9c08b39a5be7e12d71" - integrity sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ== - dependencies: - "@babel/helper-module-imports" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-remap-async-to-generator" "^7.25.9" - -"@babel/plugin-transform-block-scoped-functions@^7.26.5": - version "7.26.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz#3dc4405d31ad1cbe45293aa57205a6e3b009d53e" - integrity sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ== - dependencies: - "@babel/helper-plugin-utils" "^7.26.5" - -"@babel/plugin-transform-block-scoping@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz#c33665e46b06759c93687ca0f84395b80c0473a1" - integrity sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-class-properties@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz#a8ce84fedb9ad512549984101fa84080a9f5f51f" - integrity sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-class-static-block@^7.26.0": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz#6c8da219f4eb15cae9834ec4348ff8e9e09664a0" - integrity sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-classes@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz#7152457f7880b593a63ade8a861e6e26a4469f52" - integrity sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg== - dependencies: - "@babel/helper-annotate-as-pure" "^7.25.9" - "@babel/helper-compilation-targets" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-replace-supers" "^7.25.9" - "@babel/traverse" "^7.25.9" - globals "^11.1.0" - -"@babel/plugin-transform-computed-properties@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz#db36492c78460e534b8852b1d5befe3c923ef10b" - integrity sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/template" "^7.25.9" - -"@babel/plugin-transform-destructuring@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz#966ea2595c498224340883602d3cfd7a0c79cea1" - integrity sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-dotall-regex@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz#bad7945dd07734ca52fe3ad4e872b40ed09bb09a" - integrity sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-duplicate-keys@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz#8850ddf57dce2aebb4394bb434a7598031059e6d" - integrity sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz#6f7259b4de127721a08f1e5165b852fcaa696d31" - integrity sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-dynamic-import@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz#23e917de63ed23c6600c5dd06d94669dce79f7b8" - integrity sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-exponentiation-operator@^7.26.3": - version "7.26.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz#e29f01b6de302c7c2c794277a48f04a9ca7f03bc" - integrity sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-export-namespace-from@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz#90745fe55053394f554e40584cda81f2c8a402a2" - integrity sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-for-of@^7.26.9": - version "7.26.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.26.9.tgz#27231f79d5170ef33b5111f07fe5cafeb2c96a56" - integrity sha512-Hry8AusVm8LW5BVFgiyUReuoGzPUpdHQQqJY5bZnbbf+ngOHWuCuYFKw/BqaaWlvEUrF91HMhDtEaI1hZzNbLg== - dependencies: - "@babel/helper-plugin-utils" "^7.26.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" - -"@babel/plugin-transform-function-name@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz#939d956e68a606661005bfd550c4fc2ef95f7b97" - integrity sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA== - dependencies: - "@babel/helper-compilation-targets" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/traverse" "^7.25.9" - -"@babel/plugin-transform-json-strings@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz#c86db407cb827cded902a90c707d2781aaa89660" - integrity sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-literals@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz#1a1c6b4d4aa59bc4cad5b6b3a223a0abd685c9de" - integrity sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-logical-assignment-operators@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz#b19441a8c39a2fda0902900b306ea05ae1055db7" - integrity sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-member-expression-literals@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz#63dff19763ea64a31f5e6c20957e6a25e41ed5de" - integrity sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-modules-amd@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz#49ba478f2295101544abd794486cd3088dddb6c5" - integrity sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw== - dependencies: - "@babel/helper-module-transforms" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-modules-commonjs@^7.26.3": - version "7.26.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz#8f011d44b20d02c3de44d8850d971d8497f981fb" - integrity sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ== - dependencies: - "@babel/helper-module-transforms" "^7.26.0" - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-modules-systemjs@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz#8bd1b43836269e3d33307151a114bcf3ba6793f8" - integrity sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA== - dependencies: - "@babel/helper-module-transforms" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-validator-identifier" "^7.25.9" - "@babel/traverse" "^7.25.9" - -"@babel/plugin-transform-modules-umd@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz#6710079cdd7c694db36529a1e8411e49fcbf14c9" - integrity sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw== - dependencies: - "@babel/helper-module-transforms" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-named-capturing-groups-regex@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz#454990ae6cc22fd2a0fa60b3a2c6f63a38064e6a" - integrity sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-new-target@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz#42e61711294b105c248336dcb04b77054ea8becd" - integrity sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-nullish-coalescing-operator@^7.26.6": - version "7.26.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz#fbf6b3c92cb509e7b319ee46e3da89c5bedd31fe" - integrity sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw== - dependencies: - "@babel/helper-plugin-utils" "^7.26.5" - -"@babel/plugin-transform-numeric-separator@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz#bfed75866261a8b643468b0ccfd275f2033214a1" - integrity sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-object-rest-spread@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz#0203725025074164808bcf1a2cfa90c652c99f18" - integrity sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg== - dependencies: - "@babel/helper-compilation-targets" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/plugin-transform-parameters" "^7.25.9" - -"@babel/plugin-transform-object-super@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz#385d5de135162933beb4a3d227a2b7e52bb4cf03" - integrity sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-replace-supers" "^7.25.9" - -"@babel/plugin-transform-optional-catch-binding@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz#10e70d96d52bb1f10c5caaac59ac545ea2ba7ff3" - integrity sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-optional-chaining@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz#e142eb899d26ef715435f201ab6e139541eee7dd" - integrity sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" - -"@babel/plugin-transform-parameters@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz#b856842205b3e77e18b7a7a1b94958069c7ba257" - integrity sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-private-methods@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz#847f4139263577526455d7d3223cd8bda51e3b57" - integrity sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-private-property-in-object@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz#9c8b73e64e6cc3cbb2743633885a7dd2c385fe33" - integrity sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.25.9" - "@babel/helper-create-class-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-property-literals@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz#d72d588bd88b0dec8b62e36f6fda91cedfe28e3f" - integrity sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-regenerator@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz#03a8a4670d6cebae95305ac6defac81ece77740b" - integrity sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - regenerator-transform "^0.15.2" - -"@babel/plugin-transform-regexp-modifiers@^7.26.0": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz#2f5837a5b5cd3842a919d8147e9903cc7455b850" - integrity sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-reserved-words@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz#0398aed2f1f10ba3f78a93db219b27ef417fb9ce" - integrity sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-shorthand-properties@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz#bb785e6091f99f826a95f9894fc16fde61c163f2" - integrity sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-spread@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz#24a35153931b4ba3d13cec4a7748c21ab5514ef9" - integrity sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" - -"@babel/plugin-transform-sticky-regex@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz#c7f02b944e986a417817b20ba2c504dfc1453d32" - integrity sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-template-literals@^7.26.8": - version "7.26.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.26.8.tgz#966b15d153a991172a540a69ad5e1845ced990b5" - integrity sha512-OmGDL5/J0CJPJZTHZbi2XpO0tyT2Ia7fzpW5GURwdtp2X3fMmN8au/ej6peC/T33/+CRiIpA8Krse8hFGVmT5Q== - dependencies: - "@babel/helper-plugin-utils" "^7.26.5" - -"@babel/plugin-transform-typeof-symbol@^7.26.7": - version "7.26.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.26.7.tgz#d0e33acd9223744c1e857dbd6fa17bd0a3786937" - integrity sha512-jfoTXXZTgGg36BmhqT3cAYK5qkmqvJpvNrPhaK/52Vgjhw4Rq29s9UqpWWV0D6yuRmgiFH/BUVlkl96zJWqnaw== - dependencies: - "@babel/helper-plugin-utils" "^7.26.5" - -"@babel/plugin-transform-unicode-escapes@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz#a75ef3947ce15363fccaa38e2dd9bc70b2788b82" - integrity sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-unicode-property-regex@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz#a901e96f2c1d071b0d1bb5dc0d3c880ce8f53dd3" - integrity sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-unicode-regex@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz#5eae747fe39eacf13a8bd006a4fb0b5d1fa5e9b1" - integrity sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-transform-unicode-sets-regex@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz#65114c17b4ffc20fa5b163c63c70c0d25621fabe" - integrity sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/preset-env@~7.26.9": - version "7.26.9" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.26.9.tgz#2ec64e903d0efe743699f77a10bdf7955c2123c3" - integrity sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ== - dependencies: - "@babel/compat-data" "^7.26.8" - "@babel/helper-compilation-targets" "^7.26.5" - "@babel/helper-plugin-utils" "^7.26.5" - "@babel/helper-validator-option" "^7.25.9" - "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.25.9" - "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.25.9" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.25.9" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.25.9" - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.25.9" - "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" - "@babel/plugin-syntax-import-assertions" "^7.26.0" - "@babel/plugin-syntax-import-attributes" "^7.26.0" - "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" - "@babel/plugin-transform-arrow-functions" "^7.25.9" - "@babel/plugin-transform-async-generator-functions" "^7.26.8" - "@babel/plugin-transform-async-to-generator" "^7.25.9" - "@babel/plugin-transform-block-scoped-functions" "^7.26.5" - "@babel/plugin-transform-block-scoping" "^7.25.9" - "@babel/plugin-transform-class-properties" "^7.25.9" - "@babel/plugin-transform-class-static-block" "^7.26.0" - "@babel/plugin-transform-classes" "^7.25.9" - "@babel/plugin-transform-computed-properties" "^7.25.9" - "@babel/plugin-transform-destructuring" "^7.25.9" - "@babel/plugin-transform-dotall-regex" "^7.25.9" - "@babel/plugin-transform-duplicate-keys" "^7.25.9" - "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.25.9" - "@babel/plugin-transform-dynamic-import" "^7.25.9" - "@babel/plugin-transform-exponentiation-operator" "^7.26.3" - "@babel/plugin-transform-export-namespace-from" "^7.25.9" - "@babel/plugin-transform-for-of" "^7.26.9" - "@babel/plugin-transform-function-name" "^7.25.9" - "@babel/plugin-transform-json-strings" "^7.25.9" - "@babel/plugin-transform-literals" "^7.25.9" - "@babel/plugin-transform-logical-assignment-operators" "^7.25.9" - "@babel/plugin-transform-member-expression-literals" "^7.25.9" - "@babel/plugin-transform-modules-amd" "^7.25.9" - "@babel/plugin-transform-modules-commonjs" "^7.26.3" - "@babel/plugin-transform-modules-systemjs" "^7.25.9" - "@babel/plugin-transform-modules-umd" "^7.25.9" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.25.9" - "@babel/plugin-transform-new-target" "^7.25.9" - "@babel/plugin-transform-nullish-coalescing-operator" "^7.26.6" - "@babel/plugin-transform-numeric-separator" "^7.25.9" - "@babel/plugin-transform-object-rest-spread" "^7.25.9" - "@babel/plugin-transform-object-super" "^7.25.9" - "@babel/plugin-transform-optional-catch-binding" "^7.25.9" - "@babel/plugin-transform-optional-chaining" "^7.25.9" - "@babel/plugin-transform-parameters" "^7.25.9" - "@babel/plugin-transform-private-methods" "^7.25.9" - "@babel/plugin-transform-private-property-in-object" "^7.25.9" - "@babel/plugin-transform-property-literals" "^7.25.9" - "@babel/plugin-transform-regenerator" "^7.25.9" - "@babel/plugin-transform-regexp-modifiers" "^7.26.0" - "@babel/plugin-transform-reserved-words" "^7.25.9" - "@babel/plugin-transform-shorthand-properties" "^7.25.9" - "@babel/plugin-transform-spread" "^7.25.9" - "@babel/plugin-transform-sticky-regex" "^7.25.9" - "@babel/plugin-transform-template-literals" "^7.26.8" - "@babel/plugin-transform-typeof-symbol" "^7.26.7" - "@babel/plugin-transform-unicode-escapes" "^7.25.9" - "@babel/plugin-transform-unicode-property-regex" "^7.25.9" - "@babel/plugin-transform-unicode-regex" "^7.25.9" - "@babel/plugin-transform-unicode-sets-regex" "^7.25.9" - "@babel/preset-modules" "0.1.6-no-external-plugins" - babel-plugin-polyfill-corejs2 "^0.4.10" - babel-plugin-polyfill-corejs3 "^0.11.0" - babel-plugin-polyfill-regenerator "^0.6.1" - core-js-compat "^3.40.0" - semver "^6.3.1" - -"@babel/preset-modules@0.1.6-no-external-plugins": - version "0.1.6-no-external-plugins" - resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz#ccb88a2c49c817236861fee7826080573b8a923a" - integrity sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/types" "^7.4.4" - esutils "^2.0.2" - -"@babel/register@^7.23.7", "@babel/register@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.25.9.tgz#1c465acf7dc983d70ccc318eb5b887ecb04f021b" - integrity sha512-8D43jXtGsYmEeDvm4MWHYUpWf8iiXgWYx3fW7E7Wb7Oe6FWqJPl5K6TuFW0dOwNZzEE5rjlaSJYH9JjrUKJszA== - dependencies: - clone-deep "^4.0.1" - find-cache-dir "^2.0.0" - make-dir "^2.1.0" - pirates "^4.0.6" - source-map-support "^0.5.16" - -"@babel/regjsgen@^0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" - integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== - "@babel/runtime-corejs2@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.5.5.tgz#c3214c08ef20341af4187f1c9fbdc357fbec96b2" @@ -992,14 +861,14 @@ core-js-pure "^3.30.2" regenerator-runtime "^0.14.0" -"@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.12.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== dependencies: regenerator-runtime "^0.13.4" -"@babel/template@^7.25.9", "@babel/template@^7.26.9", "@babel/template@^7.27.0", "@babel/template@^7.3.3": +"@babel/template@^7.26.9", "@babel/template@^7.27.0", "@babel/template@^7.3.3": version "7.27.0" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.0.tgz#b253e5406cc1df1c57dcd18f11760c2dbf40c0b4" integrity sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA== @@ -1008,7 +877,7 @@ "@babel/parser" "^7.27.0" "@babel/types" "^7.27.0" -"@babel/traverse@^7.25.9", "@babel/traverse@^7.26.10", "@babel/traverse@^7.26.8", "@babel/traverse@^7.7.0": +"@babel/traverse@^7.25.9", "@babel/traverse@^7.26.10": version "7.27.0" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.27.0.tgz#11d7e644779e166c0442f9a07274d02cd91d4a70" integrity sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA== @@ -1021,7 +890,7 @@ debug "^4.3.1" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.22.5", "@babel/types@^7.25.9", "@babel/types@^7.26.10", "@babel/types@^7.27.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.0": +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.25.9", "@babel/types@^7.26.10", "@babel/types@^7.27.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.6.1", "@babel/types@^7.9.6": version "7.27.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.0.tgz#ef9acb6b06c3173f6632d993ecb6d4ae470b4559" integrity sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg== @@ -1069,36 +938,49 @@ resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz#a5502c8539265fecbd873c1e395a890339f119c2" integrity sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw== -"@emnapi/core@^1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.4.0.tgz#8844b02d799198158ac1fea21ae2bc81b881da9a" - integrity sha512-H+N/FqT07NmLmt6OFFtDfwe8PNygprzBikrEMyQfgqSmT0vzE515Pz7R8izwB9q/zsH/MA64AKoul3sA6/CzVg== +"@emnapi/core@^1.4.3": + version "1.4.3" + resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.4.3.tgz#9ac52d2d5aea958f67e52c40a065f51de59b77d6" + integrity sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g== dependencies: - "@emnapi/wasi-threads" "1.0.1" + "@emnapi/wasi-threads" "1.0.2" tslib "^2.4.0" -"@emnapi/runtime@^1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.4.0.tgz#8f509bf1059a5551c8fe829a1c4e91db35fdfbee" - integrity sha512-64WYIf4UYcdLnbKn/umDlNjQDSS8AgZrI/R9+x5ilkUVFxXcA1Ebl+gQLc/6mERA4407Xof0R7wEyEuj091CVw== +"@emnapi/runtime@^1.4.3": + version "1.4.3" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.4.3.tgz#c0564665c80dc81c448adac23f9dfbed6c838f7d" + integrity sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ== dependencies: tslib "^2.4.0" -"@emnapi/wasi-threads@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.0.1.tgz#d7ae71fd2166b1c916c6cd2d0df2ef565a2e1a5b" - integrity sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw== +"@emnapi/wasi-threads@1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz#977f44f844eac7d6c138a415a123818c655f874c" + integrity sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA== dependencies: tslib "^2.4.0" -"@eslint-community/eslint-utils@^4.1.2", "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": - version "4.4.0" - resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" - integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== +"@eslint-community/eslint-plugin-eslint-comments@^4.5.0": + version "4.5.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-4.5.0.tgz#4ffa576583bd99dfbaf74c893635e2c76acba048" + integrity sha512-MAhuTKlr4y/CE3WYX26raZjy+I/kS2PLKSzvfmDCGrBLTFHOYwqROZdr4XwPgXwX3K9rjzMr4pSmUWGnzsUyMg== dependencies: - eslint-visitor-keys "^3.3.0" + escape-string-regexp "^4.0.0" + ignore "^5.2.4" -"@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.6.0", "@eslint-community/regexpp@^4.6.1": +"@eslint-community/eslint-utils@^4.1.2", "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0", "@eslint-community/eslint-utils@^4.5.0", "@eslint-community/eslint-utils@^4.5.1", "@eslint-community/eslint-utils@^4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz#607084630c6c033992a082de6e6fbc1a8b52175a" + integrity sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw== + dependencies: + eslint-visitor-keys "^3.4.3" + +"@eslint-community/regexpp@^4.11.0": + version "4.12.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" + integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== + +"@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.6.1": version "4.10.0" resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== @@ -1123,10 +1005,10 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== -"@faker-js/faker@9.6.0": - version "9.6.0" - resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-9.6.0.tgz#64235d20330b142eef3d1d1638ba56c083b4bf1d" - integrity sha512-3vm4by+B5lvsFPSyep3ELWmZfE3kicDtmemVpuwl1yH7tqtnHdsA6hG8fbXedMVdkzgtvzWoRgjSB4Q+FHnZiw== +"@faker-js/faker@9.8.0": + version "9.8.0" + resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-9.8.0.tgz#3344284028d1c9dc98dee2479f82939310370d88" + integrity sha512-U9wpuSrJC93jZBxx/Qq2wPjCuYISBueyVUGK7qqdmj7r/nxaxwW8AQDCLeRO7wZnjj94sh3p246cAYjUKuqgfg== "@graphql-toolkit/common@0.10.4": version "0.10.4" @@ -1161,6 +1043,13 @@ resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5" integrity sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ== +"@hapi/boom@^10.0.0": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-10.0.1.tgz#ebb14688275ae150aa6af788dbe482e6a6062685" + integrity sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA== + dependencies: + "@hapi/hoek" "^11.0.2" + "@hapi/bourne@1.x.x": version "1.3.2" resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-1.3.2.tgz#0a7095adea067243ce3283e1b56b8a8f453b242a" @@ -1171,6 +1060,11 @@ resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-8.5.1.tgz#fde96064ca446dec8c55a8c2f130957b070c6e06" integrity sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow== +"@hapi/hoek@^11.0.2": + version "11.0.7" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-11.0.7.tgz#56a920793e0a42d10e530da9a64cc0d3919c4002" + integrity sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ== + "@hapi/joi@^15.1.1": version "15.1.1" resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-15.1.1.tgz#c675b8a71296f02833f8d6d243b34c57b8ce19d7" @@ -1506,6 +1400,77 @@ resolved "https://registry.yarnpkg.com/@kikobeats/time-span/-/time-span-1.0.5.tgz#9f7c5d48b08da02115dbf3d85ca11a6a6f8bfdeb" integrity sha512-txRAdmi35N1wnsLS1AO5mTlbY5Cv5/61WXqek2y3L9Q7u4mgdUVq819so5xe753hL5gYeLzlWoJ/VJfXg9nx8g== +"@ladjs/consolidate@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@ladjs/consolidate/-/consolidate-1.0.4.tgz#31d9604a0e3de6616aeba062c4390c5aa0e5c04d" + integrity sha512-ErvBg5acSqns86V/xW7gjqqnBBs6thnpMB0gGc3oM7WHsV8PWrnBtKI6dumHDT3UT/zEOfGzp7dmSFqWoCXKWQ== + +"@ladjs/country-language@^0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@ladjs/country-language/-/country-language-0.2.1.tgz#553f776fa1eb295d0344ed06525a945f94cdafaa" + integrity sha512-e3AmT7jUnfNE6e2mx2+cPYiWdFW3McySDGRhQEYE6SksjZTMj0PTp+R9x1xG89tHRTsyMNJFl9J4HtZPWZzi1Q== + dependencies: + underscore "~1.13.1" + underscore.deep "~0.5.1" + +"@ladjs/country-language@^1.0.1": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@ladjs/country-language/-/country-language-1.0.3.tgz#1131b524c6242567dfc4ce61401ff7a62e91b155" + integrity sha512-FJROu9/hh4eqVAGDyfL8vpv6Vb0qKHX1ozYLRZ+beUzD5xFf+3r0J+SVIWKviEa7W524Qvqou+ta1WrsRgzxGw== + +"@ladjs/i18n@^8.0.3": + version "8.0.3" + resolved "https://registry.yarnpkg.com/@ladjs/i18n/-/i18n-8.0.3.tgz#e2abb0726ff24fd9a8d6e37d5ca351b079974069" + integrity sha512-QYeYGz6uJaH41ZVyNoI2Lt2NyfcpKwpDIBMx3psaE1NBJn8P+jk1m0EIjphfYvnRMnl/QyBpn98FfcTUjTkuBw== + dependencies: + "@hapi/boom" "^10.0.0" + "@ladjs/country-language" "^1.0.1" + boolean "3.2.0" + i18n "^0.15.0" + i18n-locales "^0.0.5" + lodash "^4.17.21" + multimatch "5" + punycode "^2.1.1" + qs "^6.11.0" + titleize "2" + tlds "^1.231.0" + +"@messageformat/core@^3.0.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@messageformat/core/-/core-3.4.0.tgz#2814c23383dec7bddf535d54f2a03e410165ca9f" + integrity sha512-NgCFubFFIdMWJGN5WuQhHCNmzk7QgiVfrViFxcS99j7F5dDS5EP6raR54I+2ydhe4+5/XTn/YIEppFaqqVWHsw== + dependencies: + "@messageformat/date-skeleton" "^1.0.0" + "@messageformat/number-skeleton" "^1.0.0" + "@messageformat/parser" "^5.1.0" + "@messageformat/runtime" "^3.0.1" + make-plural "^7.0.0" + safe-identifier "^0.4.1" + +"@messageformat/date-skeleton@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@messageformat/date-skeleton/-/date-skeleton-1.1.0.tgz#3bad068cbf5873d14592cfc7a73dd4d8615e2739" + integrity sha512-rmGAfB1tIPER+gh3p/RgA+PVeRE/gxuQ2w4snFWPF5xtb5mbWR7Cbw7wCOftcUypbD6HVoxrVdyyghPm3WzP5A== + +"@messageformat/number-skeleton@^1.0.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@messageformat/number-skeleton/-/number-skeleton-1.2.0.tgz#e7c245c41a1b2722bc59dad68f4d454f761bc9b4" + integrity sha512-xsgwcL7J7WhlHJ3RNbaVgssaIwcEyFkBqxHdcdaiJzwTZAWEOD8BuUFxnxV9k5S0qHN3v/KzUpq0IUpjH1seRg== + +"@messageformat/parser@^5.1.0": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@messageformat/parser/-/parser-5.1.1.tgz#ca7d6c18e9f3f6b6bc984a465dac16da00106055" + integrity sha512-3p0YRGCcTUCYvBKLIxtDDyrJ0YijGIwrTRu1DT8gIviIDZru8H23+FkY6MJBzM1n9n20CiM4VeDYuBsrrwnLjg== + dependencies: + moo "^0.5.1" + +"@messageformat/runtime@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@messageformat/runtime/-/runtime-3.0.1.tgz#94d1f6c43265c28ef7aed98ecfcc0968c6c849ac" + integrity sha512-6RU5ol2lDtO8bD9Yxe6CZkl0DArdv0qkuoZC+ZwowU+cdRlVE1157wjCmlA5Rsf1Xc/brACnsZa5PZpEDfTFFg== + dependencies: + make-plural "^7.0.0" + "@metascraper/helpers@5.46.11", "@metascraper/helpers@^5.34.4": version "5.46.11" resolved "https://registry.yarnpkg.com/@metascraper/helpers/-/helpers-5.46.11.tgz#d55f77623227887a1ee52be3f4ea20174c36ec72" @@ -1559,20 +1524,15 @@ url-regex "~4.1.1" video-extensions "~1.1.0" -"@napi-rs/wasm-runtime@^0.2.7": - version "0.2.8" - resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.8.tgz#642e8390ee78ed21d6b79c467aa610e249224ed6" - integrity sha512-OBlgKdX7gin7OIq4fadsjpg+cp2ZphvAIKucHsNfTdJiqdOmOEwQd/bHi0VwNrcw5xpBJyUw6cK/QilCqy1BSg== +"@napi-rs/wasm-runtime@^0.2.11": + version "0.2.11" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz#192c1610e1625048089ab4e35bc0649ce478500e" + integrity sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA== dependencies: - "@emnapi/core" "^1.4.0" - "@emnapi/runtime" "^1.4.0" + "@emnapi/core" "^1.4.3" + "@emnapi/runtime" "^1.4.3" "@tybys/wasm-util" "^0.9.0" -"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3": - version "2.1.8-no-fsevents.3" - resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz#323d72dd25103d0c4fbdce89dadf574a787b1f9b" - integrity sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ== - "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1622,10 +1582,10 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@pkgr/core@^0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.0.tgz#8dff61038cb5884789d8b323d9869e5363b976f7" - integrity sha512-vsJDAkYR6qCPu+ioGScGiMYR7LvZYIXh/dlQeviqoTWNCVfKTLYD/LkNWH4Mxsv2a5vpIRc77FN5DnmK1eBggQ== +"@pkgr/core@^0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.4.tgz#d897170a2b0ba51f78a099edccd968f7b103387c" + integrity sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw== "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" @@ -1685,6 +1645,14 @@ resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== +"@selderee/plugin-htmlparser2@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz#d5b5e29a7ba6d3958a1972c7be16f4b2c188c517" + integrity sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ== + dependencies: + domhandler "^5.0.3" + selderee "^0.11.0" + "@sentry/apm@5.15.4": version "5.15.4" resolved "https://registry.yarnpkg.com/@sentry/apm/-/apm-5.15.4.tgz#59af766d2bb4c9d98eda5ddba7a32a79ecc807a2" @@ -1788,6 +1756,506 @@ dependencies: "@sinonjs/commons" "^3.0.0" +"@smithy/abort-controller@^4.0.2": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@smithy/abort-controller/-/abort-controller-4.0.3.tgz#53a53dabc5a46fec70857acb07653d658c79b916" + integrity sha512-AqXFf6DXnuRBXy4SoK/n1mfgHaKaq36bmkphmD1KO0nHq6xK/g9KHSW4HEsPQUBCGdIEfuJifGHwxFXPIFay9Q== + dependencies: + "@smithy/types" "^4.3.0" + tslib "^2.6.2" + +"@smithy/abort-controller@^4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@smithy/abort-controller/-/abort-controller-4.0.4.tgz#ab991d521fc78b5c7f24907fcd6803c0f2da51d9" + integrity sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA== + dependencies: + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@smithy/chunked-blob-reader-native@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.0.0.tgz#33cbba6deb8a3c516f98444f65061784f7cd7f8c" + integrity sha512-R9wM2yPmfEMsUmlMlIgSzOyICs0x9uu7UTHoccMyt7BWw8shcGM8HqB355+BZCPBcySvbTYMs62EgEQkNxz2ig== + dependencies: + "@smithy/util-base64" "^4.0.0" + tslib "^2.6.2" + +"@smithy/chunked-blob-reader@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.0.0.tgz#3f6ea5ff4e2b2eacf74cefd737aa0ba869b2e0f6" + integrity sha512-+sKqDBQqb036hh4NPaUiEkYFkTUGYzRsn3EuFhyfQfMy6oGHEUJDurLP9Ufb5dasr/XiAmPNMr6wa9afjQB+Gw== + dependencies: + tslib "^2.6.2" + +"@smithy/config-resolver@^4.1.4": + version "4.1.4" + resolved "https://registry.yarnpkg.com/@smithy/config-resolver/-/config-resolver-4.1.4.tgz#05d8eab8bb8eb73bec90c222fc19ac5608b1384e" + integrity sha512-prmU+rDddxHOH0oNcwemL+SwnzcG65sBF2yXRO7aeXIn/xTlq2pX7JLVbkBnVLowHLg4/OL4+jBmv9hVrVGS+w== + dependencies: + "@smithy/node-config-provider" "^4.1.3" + "@smithy/types" "^4.3.1" + "@smithy/util-config-provider" "^4.0.0" + "@smithy/util-middleware" "^4.0.4" + tslib "^2.6.2" + +"@smithy/core@^3.5.3": + version "3.5.3" + resolved "https://registry.yarnpkg.com/@smithy/core/-/core-3.5.3.tgz#39969839e7cfd656be38fed09951d1691525f8d5" + integrity sha512-xa5byV9fEguZNofCclv6v9ra0FYh5FATQW/da7FQUVTic94DfrN/NvmKZjrMyzbpqfot9ZjBaO8U1UeTbmSLuA== + dependencies: + "@smithy/middleware-serde" "^4.0.8" + "@smithy/protocol-http" "^5.1.2" + "@smithy/types" "^4.3.1" + "@smithy/util-base64" "^4.0.0" + "@smithy/util-body-length-browser" "^4.0.0" + "@smithy/util-middleware" "^4.0.4" + "@smithy/util-stream" "^4.2.2" + "@smithy/util-utf8" "^4.0.0" + tslib "^2.6.2" + +"@smithy/credential-provider-imds@^4.0.6": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.6.tgz#4cfd79a619cdbc9a75fcdc51a1193685f6a8944e" + integrity sha512-hKMWcANhUiNbCJouYkZ9V3+/Qf9pteR1dnwgdyzR09R4ODEYx8BbUysHwRSyex4rZ9zapddZhLFTnT4ZijR4pw== + dependencies: + "@smithy/node-config-provider" "^4.1.3" + "@smithy/property-provider" "^4.0.4" + "@smithy/types" "^4.3.1" + "@smithy/url-parser" "^4.0.4" + tslib "^2.6.2" + +"@smithy/eventstream-codec@^4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-codec/-/eventstream-codec-4.0.4.tgz#35abc26d6829cc61a0d14950857ccc5320bf92d2" + integrity sha512-7XoWfZqWb/QoR/rAU4VSi0mWnO2vu9/ltS6JZ5ZSZv0eovLVfDfu0/AX4ub33RsJTOth3TiFWSHS5YdztvFnig== + dependencies: + "@aws-crypto/crc32" "5.2.0" + "@smithy/types" "^4.3.1" + "@smithy/util-hex-encoding" "^4.0.0" + tslib "^2.6.2" + +"@smithy/eventstream-serde-browser@^4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.0.4.tgz#0c57cf0b66862106100a796751003733ce3f5273" + integrity sha512-3fb/9SYaYqbpy/z/H3yIi0bYKyAa89y6xPmIqwr2vQiUT2St+avRt8UKwsWt9fEdEasc5d/V+QjrviRaX1JRFA== + dependencies: + "@smithy/eventstream-serde-universal" "^4.0.4" + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@smithy/eventstream-serde-config-resolver@^4.1.2": + version "4.1.2" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.1.2.tgz#4d41c1ecad1a9b1c694f32865a2f0d4b5bc0162d" + integrity sha512-JGtambizrWP50xHgbzZI04IWU7LdI0nh/wGbqH3sJesYToMi2j/DcoElqyOcqEIG/D4tNyxgRuaqBXWE3zOFhQ== + dependencies: + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@smithy/eventstream-serde-node@^4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.0.4.tgz#0fbd0ac288f02bf485eb307a14254ea8d8767746" + integrity sha512-RD6UwNZ5zISpOWPuhVgRz60GkSIp0dy1fuZmj4RYmqLVRtejFqQ16WmfYDdoSoAjlp1LX+FnZo+/hkdmyyGZ1w== + dependencies: + "@smithy/eventstream-serde-universal" "^4.0.4" + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@smithy/eventstream-serde-universal@^4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.0.4.tgz#48b2b416dc0f576917c36373efaa4012f7310ab0" + integrity sha512-UeJpOmLGhq1SLox79QWw/0n2PFX+oPRE1ZyRMxPIaFEfCqWaqpB7BU9C8kpPOGEhLF7AwEqfFbtwNxGy4ReENA== + dependencies: + "@smithy/eventstream-codec" "^4.0.4" + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@smithy/fetch-http-handler@^5.0.4": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.4.tgz#c68601b4676787e049b5d464d5f4b825dbb44013" + integrity sha512-AMtBR5pHppYMVD7z7G+OlHHAcgAN7v0kVKEpHuTO4Gb199Gowh0taYi9oDStFeUhetkeP55JLSVlTW1n9rFtUw== + dependencies: + "@smithy/protocol-http" "^5.1.2" + "@smithy/querystring-builder" "^4.0.4" + "@smithy/types" "^4.3.1" + "@smithy/util-base64" "^4.0.0" + tslib "^2.6.2" + +"@smithy/hash-blob-browser@^4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@smithy/hash-blob-browser/-/hash-blob-browser-4.0.4.tgz#34adda037d324123d77032b3ad59c16e6d4949bb" + integrity sha512-WszRiACJiQV3QG6XMV44i5YWlkrlsM5Yxgz4jvsksuu7LDXA6wAtypfPajtNTadzpJy3KyJPoWehYpmZGKUFIQ== + dependencies: + "@smithy/chunked-blob-reader" "^5.0.0" + "@smithy/chunked-blob-reader-native" "^4.0.0" + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@smithy/hash-node@^4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@smithy/hash-node/-/hash-node-4.0.4.tgz#f867cfe6b702ed8893aacd3e097f8ca8ecba579e" + integrity sha512-qnbTPUhCVnCgBp4z4BUJUhOEkVwxiEi1cyFM+Zj6o+aY8OFGxUQleKWq8ltgp3dujuhXojIvJWdoqpm6dVO3lQ== + dependencies: + "@smithy/types" "^4.3.1" + "@smithy/util-buffer-from" "^4.0.0" + "@smithy/util-utf8" "^4.0.0" + tslib "^2.6.2" + +"@smithy/hash-stream-node@^4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@smithy/hash-stream-node/-/hash-stream-node-4.0.4.tgz#02c023590e09529e940e0a0243d32c02c4e6c645" + integrity sha512-wHo0d8GXyVmpmMh/qOR0R7Y46/G1y6OR8U+bSTB4ppEzRxd1xVAQ9xOE9hOc0bSjhz0ujCPAbfNLkLrpa6cevg== + dependencies: + "@smithy/types" "^4.3.1" + "@smithy/util-utf8" "^4.0.0" + tslib "^2.6.2" + +"@smithy/invalid-dependency@^4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@smithy/invalid-dependency/-/invalid-dependency-4.0.4.tgz#8c2c539b2f22e857b4652bd2427a3d7a8befd610" + integrity sha512-bNYMi7WKTJHu0gn26wg8OscncTt1t2b8KcsZxvOv56XA6cyXtOAAAaNP7+m45xfppXfOatXF3Sb1MNsLUgVLTw== + dependencies: + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@smithy/is-array-buffer@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz#f84f0d9f9a36601a9ca9381688bd1b726fd39111" + integrity sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA== + dependencies: + tslib "^2.6.2" + +"@smithy/is-array-buffer@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz#55a939029321fec462bcc574890075cd63e94206" + integrity sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw== + dependencies: + tslib "^2.6.2" + +"@smithy/md5-js@^4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@smithy/md5-js/-/md5-js-4.0.4.tgz#d7cb70b08c2a4d809d5cb905feab74fc9726a2f2" + integrity sha512-uGLBVqcOwrLvGh/v/jw423yWHq/ofUGK1W31M2TNspLQbUV1Va0F5kTxtirkoHawODAZcjXTSGi7JwbnPcDPJg== + dependencies: + "@smithy/types" "^4.3.1" + "@smithy/util-utf8" "^4.0.0" + tslib "^2.6.2" + +"@smithy/middleware-content-length@^4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@smithy/middleware-content-length/-/middleware-content-length-4.0.4.tgz#fad1f125779daf8d5f261dae6dbebba0f60c234b" + integrity sha512-F7gDyfI2BB1Kc+4M6rpuOLne5LOcEknH1n6UQB69qv+HucXBR1rkzXBnQTB2q46sFy1PM/zuSJOB532yc8bg3w== + dependencies: + "@smithy/protocol-http" "^5.1.2" + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@smithy/middleware-endpoint@^4.1.11", "@smithy/middleware-endpoint@^4.1.6": + version "4.1.11" + resolved "https://registry.yarnpkg.com/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.11.tgz#bf23781c55cc3768c5d0f8866d2428bbce786bb4" + integrity sha512-zDogwtRLzKl58lVS8wPcARevFZNBOOqnmzWWxVe9XiaXU2CADFjvJ9XfNibgkOWs08sxLuSr81NrpY4mgp9OwQ== + dependencies: + "@smithy/core" "^3.5.3" + "@smithy/middleware-serde" "^4.0.8" + "@smithy/node-config-provider" "^4.1.3" + "@smithy/shared-ini-file-loader" "^4.0.4" + "@smithy/types" "^4.3.1" + "@smithy/url-parser" "^4.0.4" + "@smithy/util-middleware" "^4.0.4" + tslib "^2.6.2" + +"@smithy/middleware-retry@^4.1.12": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@smithy/middleware-retry/-/middleware-retry-4.1.12.tgz#4d0b60bba95201539c99911c0a36f9275d973802" + integrity sha512-wvIH70c4e91NtRxdaLZF+mbLZ/HcC6yg7ySKUiufL6ESp6zJUSnJucZ309AvG9nqCFHSRB5I6T3Ez1Q9wCh0Ww== + dependencies: + "@smithy/node-config-provider" "^4.1.3" + "@smithy/protocol-http" "^5.1.2" + "@smithy/service-error-classification" "^4.0.5" + "@smithy/smithy-client" "^4.4.3" + "@smithy/types" "^4.3.1" + "@smithy/util-middleware" "^4.0.4" + "@smithy/util-retry" "^4.0.5" + tslib "^2.6.2" + uuid "^9.0.1" + +"@smithy/middleware-serde@^4.0.8": + version "4.0.8" + resolved "https://registry.yarnpkg.com/@smithy/middleware-serde/-/middleware-serde-4.0.8.tgz#3704c8cc46acd0a7f910a78ee1d2f23ce928701f" + integrity sha512-iSSl7HJoJaGyMIoNn2B7czghOVwJ9nD7TMvLhMWeSB5vt0TnEYyRRqPJu/TqW76WScaNvYYB8nRoiBHR9S1Ddw== + dependencies: + "@smithy/protocol-http" "^5.1.2" + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@smithy/middleware-stack@^4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@smithy/middleware-stack/-/middleware-stack-4.0.4.tgz#58e0c6a0d7678c6ad4d6af8dd9a00f749ffac7c5" + integrity sha512-kagK5ggDrBUCCzI93ft6DjteNSfY8Ulr83UtySog/h09lTIOAJ/xUSObutanlPT0nhoHAkpmW9V5K8oPyLh+QA== + dependencies: + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@smithy/node-config-provider@^4.1.3": + version "4.1.3" + resolved "https://registry.yarnpkg.com/@smithy/node-config-provider/-/node-config-provider-4.1.3.tgz#6626fe26c6fe7b0df34f71cb72764ccba414a815" + integrity sha512-HGHQr2s59qaU1lrVH6MbLlmOBxadtzTsoO4c+bF5asdgVik3I8o7JIOzoeqWc5MjVa+vD36/LWE0iXKpNqooRw== + dependencies: + "@smithy/property-provider" "^4.0.4" + "@smithy/shared-ini-file-loader" "^4.0.4" + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@smithy/node-http-handler@^4.0.6": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-4.0.6.tgz#a022da499ba3af4b6b4c815104fde973c0eccc40" + integrity sha512-NqbmSz7AW2rvw4kXhKGrYTiJVDHnMsFnX4i+/FzcZAfbOBauPYs2ekuECkSbtqaxETLLTu9Rl/ex6+I2BKErPA== + dependencies: + "@smithy/abort-controller" "^4.0.4" + "@smithy/protocol-http" "^5.1.2" + "@smithy/querystring-builder" "^4.0.4" + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@smithy/property-provider@^4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@smithy/property-provider/-/property-provider-4.0.4.tgz#303a8fd99665fff61eeb6ec3922eee53838962c5" + integrity sha512-qHJ2sSgu4FqF4U/5UUp4DhXNmdTrgmoAai6oQiM+c5RZ/sbDwJ12qxB1M6FnP+Tn/ggkPZf9ccn4jqKSINaquw== + dependencies: + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@smithy/protocol-http@^5.1.2": + version "5.1.2" + resolved "https://registry.yarnpkg.com/@smithy/protocol-http/-/protocol-http-5.1.2.tgz#8094860c2407f250b80c95899e0385112d6eb98b" + integrity sha512-rOG5cNLBXovxIrICSBm95dLqzfvxjEmuZx4KK3hWwPFHGdW3lxY0fZNXfv2zebfRO7sJZ5pKJYHScsqopeIWtQ== + dependencies: + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@smithy/querystring-builder@^4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@smithy/querystring-builder/-/querystring-builder-4.0.4.tgz#f7546efd59d457b3d2525a330c6137e5f907864c" + integrity sha512-SwREZcDnEYoh9tLNgMbpop+UTGq44Hl9tdj3rf+yeLcfH7+J8OXEBaMc2kDxtyRHu8BhSg9ADEx0gFHvpJgU8w== + dependencies: + "@smithy/types" "^4.3.1" + "@smithy/util-uri-escape" "^4.0.0" + tslib "^2.6.2" + +"@smithy/querystring-parser@^4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@smithy/querystring-parser/-/querystring-parser-4.0.4.tgz#307ab95ee5f1a142ab46c2eddebeae68cb2f703d" + integrity sha512-6yZf53i/qB8gRHH/l2ZwUG5xgkPgQF15/KxH0DdXMDHjesA9MeZje/853ifkSY0x4m5S+dfDZ+c4x439PF0M2w== + dependencies: + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@smithy/service-error-classification@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@smithy/service-error-classification/-/service-error-classification-4.0.5.tgz#cd912cdd0510de9369db6a4d34dc36f36de54a59" + integrity sha512-LvcfhrnCBvCmTee81pRlh1F39yTS/+kYleVeLCwNtkY8wtGg8V/ca9rbZZvYIl8OjlMtL6KIjaiL/lgVqHD2nA== + dependencies: + "@smithy/types" "^4.3.1" + +"@smithy/shared-ini-file-loader@^4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.4.tgz#33c63468b95cfd5e7d642c8131d7acc034025e00" + integrity sha512-63X0260LoFBjrHifPDs+nM9tV0VMkOTl4JRMYNuKh/f5PauSjowTfvF3LogfkWdcPoxsA9UjqEOgjeYIbhb7Nw== + dependencies: + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@smithy/signature-v4@^5.1.2": + version "5.1.2" + resolved "https://registry.yarnpkg.com/@smithy/signature-v4/-/signature-v4-5.1.2.tgz#5afd9d428bd26bb660bee8075b6e89fe93600c22" + integrity sha512-d3+U/VpX7a60seHziWnVZOHuEgJlclufjkS6zhXvxcJgkJq4UWdH5eOBLzHRMx6gXjsdT9h6lfpmLzbrdupHgQ== + dependencies: + "@smithy/is-array-buffer" "^4.0.0" + "@smithy/protocol-http" "^5.1.2" + "@smithy/types" "^4.3.1" + "@smithy/util-hex-encoding" "^4.0.0" + "@smithy/util-middleware" "^4.0.4" + "@smithy/util-uri-escape" "^4.0.0" + "@smithy/util-utf8" "^4.0.0" + tslib "^2.6.2" + +"@smithy/smithy-client@^4.2.6", "@smithy/smithy-client@^4.4.3": + version "4.4.3" + resolved "https://registry.yarnpkg.com/@smithy/smithy-client/-/smithy-client-4.4.3.tgz#37499b5bdec39d9a738f3ac1566a49bcb5cad255" + integrity sha512-xxzNYgA0HD6ETCe5QJubsxP0hQH3QK3kbpJz3QrosBCuIWyEXLR/CO5hFb2OeawEKUxMNhz3a1nuJNN2np2RMA== + dependencies: + "@smithy/core" "^3.5.3" + "@smithy/middleware-endpoint" "^4.1.11" + "@smithy/middleware-stack" "^4.0.4" + "@smithy/protocol-http" "^5.1.2" + "@smithy/types" "^4.3.1" + "@smithy/util-stream" "^4.2.2" + tslib "^2.6.2" + +"@smithy/types@^4.3.0", "@smithy/types@^4.3.1": + version "4.3.1" + resolved "https://registry.yarnpkg.com/@smithy/types/-/types-4.3.1.tgz#c11276ea16235d798f47a68aef9f44d3dbb70dd4" + integrity sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA== + dependencies: + tslib "^2.6.2" + +"@smithy/url-parser@^4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@smithy/url-parser/-/url-parser-4.0.4.tgz#049143f4c156356e177bd69242675db26fe4f4db" + integrity sha512-eMkc144MuN7B0TDA4U2fKs+BqczVbk3W+qIvcoCY6D1JY3hnAdCuhCZODC+GAeaxj0p6Jroz4+XMUn3PCxQQeQ== + dependencies: + "@smithy/querystring-parser" "^4.0.4" + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@smithy/util-base64@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-base64/-/util-base64-4.0.0.tgz#8345f1b837e5f636e5f8470c4d1706ae0c6d0358" + integrity sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg== + dependencies: + "@smithy/util-buffer-from" "^4.0.0" + "@smithy/util-utf8" "^4.0.0" + tslib "^2.6.2" + +"@smithy/util-body-length-browser@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz#965d19109a4b1e5fe7a43f813522cce718036ded" + integrity sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA== + dependencies: + tslib "^2.6.2" + +"@smithy/util-body-length-node@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz#3db245f6844a9b1e218e30c93305bfe2ffa473b3" + integrity sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg== + dependencies: + tslib "^2.6.2" + +"@smithy/util-buffer-from@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz#6fc88585165ec73f8681d426d96de5d402021e4b" + integrity sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA== + dependencies: + "@smithy/is-array-buffer" "^2.2.0" + tslib "^2.6.2" + +"@smithy/util-buffer-from@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz#b23b7deb4f3923e84ef50c8b2c5863d0dbf6c0b9" + integrity sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug== + dependencies: + "@smithy/is-array-buffer" "^4.0.0" + tslib "^2.6.2" + +"@smithy/util-config-provider@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz#e0c7c8124c7fba0b696f78f0bd0ccb060997d45e" + integrity sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w== + dependencies: + tslib "^2.6.2" + +"@smithy/util-defaults-mode-browser@^4.0.19": + version "4.0.19" + resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.19.tgz#4deaa41201458d353166ab05ffa465b30898d671" + integrity sha512-mvLMh87xSmQrV5XqnUYEPoiFFeEGYeAKIDDKdhE2ahqitm8OHM3aSvhqL6rrK6wm1brIk90JhxDf5lf2hbrLbQ== + dependencies: + "@smithy/property-provider" "^4.0.4" + "@smithy/smithy-client" "^4.4.3" + "@smithy/types" "^4.3.1" + bowser "^2.11.0" + tslib "^2.6.2" + +"@smithy/util-defaults-mode-node@^4.0.19": + version "4.0.19" + resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.19.tgz#4150b5c807ca90cac7e40a5d29f2e30e3cdb1f34" + integrity sha512-8tYnx+LUfj6m+zkUUIrIQJxPM1xVxfRBvoGHua7R/i6qAxOMjqR6CpEpDwKoIs1o0+hOjGvkKE23CafKL0vJ9w== + dependencies: + "@smithy/config-resolver" "^4.1.4" + "@smithy/credential-provider-imds" "^4.0.6" + "@smithy/node-config-provider" "^4.1.3" + "@smithy/property-provider" "^4.0.4" + "@smithy/smithy-client" "^4.4.3" + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@smithy/util-endpoints@^3.0.6": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@smithy/util-endpoints/-/util-endpoints-3.0.6.tgz#a24b0801a1b94c0de26ad83da206b9add68117f2" + integrity sha512-YARl3tFL3WgPuLzljRUnrS2ngLiUtkwhQtj8PAL13XZSyUiNLQxwG3fBBq3QXFqGFUXepIN73pINp3y8c2nBmA== + dependencies: + "@smithy/node-config-provider" "^4.1.3" + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@smithy/util-hex-encoding@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz#dd449a6452cffb37c5b1807ec2525bb4be551e8d" + integrity sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw== + dependencies: + tslib "^2.6.2" + +"@smithy/util-middleware@^4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@smithy/util-middleware/-/util-middleware-4.0.4.tgz#8f639de049082c687841ea5e69c6c36e12e31a3c" + integrity sha512-9MLKmkBmf4PRb0ONJikCbCwORACcil6gUWojwARCClT7RmLzF04hUR4WdRprIXal7XVyrddadYNfp2eF3nrvtQ== + dependencies: + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@smithy/util-retry@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@smithy/util-retry/-/util-retry-4.0.5.tgz#58eea5bb1869745dac28a3f81a5904f225ec1207" + integrity sha512-V7MSjVDTlEt/plmOFBn1762Dyu5uqMrV2Pl2X0dYk4XvWfdWJNe9Bs5Bzb56wkCuiWjSfClVMGcsuKrGj7S/yg== + dependencies: + "@smithy/service-error-classification" "^4.0.5" + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + +"@smithy/util-stream@^4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@smithy/util-stream/-/util-stream-4.2.2.tgz#beeb1edf690db9b7d7983f46ca4fb66e22253608" + integrity sha512-aI+GLi7MJoVxg24/3J1ipwLoYzgkB4kUfogZfnslcYlynj3xsQ0e7vk4TnTro9hhsS5PvX1mwmkRqqHQjwcU7w== + dependencies: + "@smithy/fetch-http-handler" "^5.0.4" + "@smithy/node-http-handler" "^4.0.6" + "@smithy/types" "^4.3.1" + "@smithy/util-base64" "^4.0.0" + "@smithy/util-buffer-from" "^4.0.0" + "@smithy/util-hex-encoding" "^4.0.0" + "@smithy/util-utf8" "^4.0.0" + tslib "^2.6.2" + +"@smithy/util-uri-escape@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz#a96c160c76f3552458a44d8081fade519d214737" + integrity sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg== + dependencies: + tslib "^2.6.2" + +"@smithy/util-utf8@^2.0.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@smithy/util-utf8/-/util-utf8-2.3.0.tgz#dd96d7640363259924a214313c3cf16e7dd329c5" + integrity sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A== + dependencies: + "@smithy/util-buffer-from" "^2.2.0" + tslib "^2.6.2" + +"@smithy/util-utf8@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-utf8/-/util-utf8-4.0.0.tgz#09ca2d9965e5849e72e347c130f2a29d5c0c863c" + integrity sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow== + dependencies: + "@smithy/util-buffer-from" "^4.0.0" + tslib "^2.6.2" + +"@smithy/util-waiter@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@smithy/util-waiter/-/util-waiter-4.0.5.tgz#cc7c65c86f5f8330445e27f9cc47d42c53c69bb7" + integrity sha512-4QvC49HTteI1gfemu0I1syWovJgPvGn7CVUoN9ZFkdvr/cCFkrEL7qNCdx/2eICqDWEGnnr68oMdSIPCLAriSQ== + dependencies: + "@smithy/abort-controller" "^4.0.4" + "@smithy/types" "^4.3.1" + tslib "^2.6.2" + "@szmarczak/http-timer@^4.0.5": version "4.0.6" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz#b4a914bb62e7c272d4e5989fe4440f812ab1d807" @@ -1909,6 +2377,15 @@ dependencies: "@types/express" "*" +"@types/email-templates@^10.0.4": + version "10.0.4" + resolved "https://registry.yarnpkg.com/@types/email-templates/-/email-templates-10.0.4.tgz#3181fd540e76e6b90b8b3e0a5a1afbc803ef7797" + integrity sha512-8O2bdGPO6RYgH2DrnFAcuV++s+8KNA5e2Erjl6UxgKRVsBH9zXu2YLrLyOBRMn2VyEYmzgF+6QQUslpVhj0y/g== + dependencies: + "@types/html-to-text" "*" + "@types/nodemailer" "*" + juice "^8.0.0" + "@types/express-serve-static-core@*": version "4.17.7" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.7.tgz#dfe61f870eb549dc6d7e12050901847c7d7e915b" @@ -1962,6 +2439,11 @@ "@types/koa" "*" graphql "^14.5.3" +"@types/html-to-text@*": + version "9.0.4" + resolved "https://registry.yarnpkg.com/@types/html-to-text/-/html-to-text-9.0.4.tgz#4a83dd8ae8bfa91457d0b1ffc26f4d0537eff58c" + integrity sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ== + "@types/http-assert@*": version "1.5.1" resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.1.tgz#d775e93630c2469c2f980fc27e3143240335db3b" @@ -2041,16 +2523,31 @@ "@types/koa-compose" "*" "@types/node" "*" +"@types/lodash@^4.17.17": + version "4.17.17" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.17.tgz#fb85a04f47e9e4da888384feead0de05f7070355" + integrity sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ== + "@types/long@^4.0.0": version "4.0.1" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== +"@types/mime-types@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@types/mime-types/-/mime-types-2.1.4.tgz#93a1933e24fed4fb9e4adc5963a63efcbb3317a2" + integrity sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w== + "@types/mime@*": version "2.0.2" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.2.tgz#857a118d8634c84bba7ae14088e4508490cd5da5" integrity sha512-4kPlzbljFcsttWEq6aBW0OZe6BDajAmyvr2xknBG92tejQnvdGtT9+kXSZ580DqpxY9qG2xeQVF9Dq0ymUTo5Q== +"@types/minimatch@^3.0.3": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" + integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ== + "@types/node-fetch@2.5.7": version "2.5.7" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.7.tgz#20a2afffa882ab04d44ca786449a276f9f6bbf3c" @@ -2059,10 +2556,10 @@ "@types/node" "*" form-data "^3.0.0" -"@types/node@*", "@types/node@>=6", "@types/node@^22.14.0": - version "22.14.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.14.0.tgz#d3bfa3936fef0dbacd79ea3eb17d521c628bb47e" - integrity sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA== +"@types/node@*", "@types/node@^22.15.30": + version "22.15.30" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.30.tgz#3a20431783e28dd0b0326f84ab386a2ec81d921d" + integrity sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA== dependencies: undici-types "~6.21.0" @@ -2071,6 +2568,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.26.tgz#a8a119960bff16b823be4c617da028570779bcfd" integrity sha512-myMwkO2Cr82kirHY8uknNRHEVtn0wV3DTQfkrjx17jmkstDRZ24gNUdl8AHXVyVclTYI/bNjgTPTAWvWLqXqkw== +"@types/nodemailer@*": + version "6.4.17" + resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.17.tgz#5c82a42aee16a3dd6ea31446a1bd6a447f1ac1a4" + integrity sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww== + dependencies: + "@types/node" "*" + "@types/qs@*": version "6.9.3" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.3.tgz#b755a0934564a200d3efdf88546ec93c369abd03" @@ -2101,11 +2605,21 @@ "@types/express-serve-static-core" "*" "@types/mime" "*" +"@types/slug@^5.0.9": + version "5.0.9" + resolved "https://registry.yarnpkg.com/@types/slug/-/slug-5.0.9.tgz#e5b213a9d7797d40d362ba85e2a7bbcd4df4ed40" + integrity sha512-6Yp8BSplP35Esa/wOG1wLNKiqXevpQTEF/RcL/NV6BBQaMmZh4YlDwCgrrFSoUE4xAGvnKd5c+lkQJmPrBAzfQ== + "@types/stack-utils@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/uuid@~9.0.1": + version "9.0.8" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba" + integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA== + "@types/ws@^7.0.0": version "7.2.5" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.2.5.tgz#513f28b04a1ea1aa9dc2cad3f26e8e37c88aae49" @@ -2130,11 +2644,6 @@ resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.34.tgz#199329f9fee5074a7385b4a4a25d1559db628aef" integrity sha512-/zH/Yuwl2vC5fgPE+Qtv67iI/o50qLJJsR0KVc86cpDlY2IsRv3yJop1v/9hfNrgy7J8J5BpJM4BMhyFE3QE4w== -"@types/zen-observable@^0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" - integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg== - "@typescript-eslint/eslint-plugin@^5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz#aeef0328d172b9e37d9bab6dbc13b87ed88977db" @@ -2161,6 +2670,15 @@ "@typescript-eslint/typescript-estree" "5.62.0" debug "^4.3.4" +"@typescript-eslint/project-service@8.34.0": + version "8.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.34.0.tgz#449119b72fe9fae185013a6bdbaf1ffbfee6bcaf" + integrity sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw== + dependencies: + "@typescript-eslint/tsconfig-utils" "^8.34.0" + "@typescript-eslint/types" "^8.34.0" + debug "^4.3.4" + "@typescript-eslint/scope-manager@5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz#d9457ccc6a0b8d6b37d0eb252a23022478c5460c" @@ -2169,13 +2687,18 @@ "@typescript-eslint/types" "5.62.0" "@typescript-eslint/visitor-keys" "5.62.0" -"@typescript-eslint/scope-manager@8.4.0": - version "8.4.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.4.0.tgz#8a13d3c0044513d7960348db6f4789d2a06fa4b4" - integrity sha512-n2jFxLeY0JmKfUqy3P70rs6vdoPjHK8P/w+zJcV3fk0b0BwRXC/zxRTEnAsgYT7MwdQDt/ZEbtdzdVC+hcpF0A== +"@typescript-eslint/scope-manager@8.34.0": + version "8.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.34.0.tgz#9fedaec02370cf79c018a656ab402eb00dc69e67" + integrity sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw== dependencies: - "@typescript-eslint/types" "8.4.0" - "@typescript-eslint/visitor-keys" "8.4.0" + "@typescript-eslint/types" "8.34.0" + "@typescript-eslint/visitor-keys" "8.34.0" + +"@typescript-eslint/tsconfig-utils@8.34.0", "@typescript-eslint/tsconfig-utils@^8.34.0": + version "8.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.0.tgz#97d0a24e89a355e9308cebc8e23f255669bf0979" + integrity sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA== "@typescript-eslint/type-utils@5.62.0": version "5.62.0" @@ -2192,10 +2715,10 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== -"@typescript-eslint/types@8.4.0": - version "8.4.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.4.0.tgz#b44d6a90a317a6d97a3e5fabda5196089eec6171" - integrity sha512-T1RB3KQdskh9t3v/qv7niK6P8yvn7ja1mS7QK7XfRVL6wtZ8/mFs/FHf4fKvTA0rKnqnYxl/uHFNbnEt0phgbw== +"@typescript-eslint/types@8.34.0", "@typescript-eslint/types@^8.34.0": + version "8.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.34.0.tgz#18000f205c59c9aff7f371fc5426b764cf2890fb" + integrity sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA== "@typescript-eslint/typescript-estree@5.62.0": version "5.62.0" @@ -2210,19 +2733,21 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@8.4.0": - version "8.4.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.4.0.tgz#00ed79ae049e124db37315cde1531a900a048482" - integrity sha512-kJ2OIP4dQw5gdI4uXsaxUZHRwWAGpREJ9Zq6D5L0BweyOrWsL6Sz0YcAZGWhvKnH7fm1J5YFE1JrQL0c9dd53A== +"@typescript-eslint/typescript-estree@8.34.0": + version "8.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.0.tgz#c9f3feec511339ef64e9e4884516c3e558f1b048" + integrity sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg== dependencies: - "@typescript-eslint/types" "8.4.0" - "@typescript-eslint/visitor-keys" "8.4.0" + "@typescript-eslint/project-service" "8.34.0" + "@typescript-eslint/tsconfig-utils" "8.34.0" + "@typescript-eslint/types" "8.34.0" + "@typescript-eslint/visitor-keys" "8.34.0" debug "^4.3.4" fast-glob "^3.3.2" is-glob "^4.0.3" minimatch "^9.0.4" semver "^7.6.0" - ts-api-utils "^1.3.0" + ts-api-utils "^2.1.0" "@typescript-eslint/utils@5.62.0": version "5.62.0" @@ -2238,15 +2763,15 @@ eslint-scope "^5.1.1" semver "^7.3.7" -"@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0": - version "8.4.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.4.0.tgz#35c552a404858c853a1f62ba6df2214f1988afc3" - integrity sha512-swULW8n1IKLjRAgciCkTCafyTHHfwVQFt8DovmaF69sKbOxTSFMmIZaSHjqO9i/RV0wIblaawhzvtva8Nmm7lQ== +"@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/utils@^8.26.1": + version "8.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.34.0.tgz#7844beebc1153b4d3ec34135c2da53a91e076f8d" + integrity sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ== dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.4.0" - "@typescript-eslint/types" "8.4.0" - "@typescript-eslint/typescript-estree" "8.4.0" + "@eslint-community/eslint-utils" "^4.7.0" + "@typescript-eslint/scope-manager" "8.34.0" + "@typescript-eslint/types" "8.34.0" + "@typescript-eslint/typescript-estree" "8.34.0" "@typescript-eslint/visitor-keys@5.62.0": version "5.62.0" @@ -2256,103 +2781,105 @@ "@typescript-eslint/types" "5.62.0" eslint-visitor-keys "^3.3.0" -"@typescript-eslint/visitor-keys@8.4.0": - version "8.4.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.4.0.tgz#1e8a8b8fd3647db1e42361fdd8de3e1679dec9d2" - integrity sha512-zTQD6WLNTre1hj5wp09nBIDiOc2U5r/qmzo7wxPn4ZgAjHql09EofqhF9WF+fZHzL5aCyaIpPcT2hyxl73kr9A== +"@typescript-eslint/visitor-keys@8.34.0": + version "8.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.0.tgz#c7a149407be31d755dba71980617d638a40ac099" + integrity sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA== dependencies: - "@typescript-eslint/types" "8.4.0" - eslint-visitor-keys "^3.4.3" + "@typescript-eslint/types" "8.34.0" + eslint-visitor-keys "^4.2.0" "@ungap/structured-clone@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@unrs/resolver-binding-darwin-arm64@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.3.3.tgz#394065916f98cdc1897cf7234adfdee395725fa8" - integrity sha512-EpRILdWr3/xDa/7MoyfO7JuBIJqpBMphtu4+80BK1bRfFcniVT74h3Z7q1+WOc92FuIAYatB1vn9TJR67sORGw== +"@unrs/resolver-binding-darwin-arm64@1.7.12": + version "1.7.12" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.7.12.tgz#5365169bcc84361ce71fac3ed0d5a26715bee3b1" + integrity sha512-C//UObaqVcGKpRMMThzBCDxbqM9YQg2dtWy3OwcERLu+qzLa781AqvGdgqwqakRO+cWCK6dl75ebAcsSozmARg== -"@unrs/resolver-binding-darwin-x64@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.3.3.tgz#6a3c75ca984342261c7346db53293b0002e8cde1" - integrity sha512-ntj/g7lPyqwinMJWZ+DKHBse8HhVxswGTmNgFKJtdgGub3M3zp5BSZ3bvMP+kBT6dnYJLSVlDqdwOq1P8i0+/g== +"@unrs/resolver-binding-darwin-x64@1.7.12": + version "1.7.12" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.7.12.tgz#4dad7ad7b3fd745ec7169373387aab1dc0902a3d" + integrity sha512-eRXO0uPaZtWIogCeVlpSCfzKr3ZJynQl3IRzhFucrA+efdjAylS+ZemWFfnhGbQgWv4lItKCfCpxGuZsosudWw== -"@unrs/resolver-binding-freebsd-x64@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.3.3.tgz#6532b8d4fecaca6c4424791c82f7a27aac94fcd5" - integrity sha512-l6BT8f2CU821EW7U8hSUK8XPq4bmyTlt9Mn4ERrfjJNoCw0/JoHAh9amZZtV3cwC3bwwIat+GUnrcHTG9+qixw== +"@unrs/resolver-binding-freebsd-x64@1.7.12": + version "1.7.12" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.7.12.tgz#8cd285da4b6a98b8a3fba8cf1673adaa971cf6ae" + integrity sha512-VUdT2CwMoyWy9Jolavu2fWTcusiA9FePjSyMLIrZvAeC2PMnM9msF3HJkO/j0S2fZkzgZy+UBBZjJwG0Mfds0g== -"@unrs/resolver-binding-linux-arm-gnueabihf@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.3.3.tgz#69a8e430095fcf6a76f7350cc27b83464f8cbb91" - integrity sha512-8ScEc5a4y7oE2BonRvzJ+2GSkBaYWyh0/Ko4Q25e/ix6ANpJNhwEPZvCR6GVRmsQAYMIfQvYLdM6YEN+qRjnAQ== +"@unrs/resolver-binding-linux-arm-gnueabihf@1.7.12": + version "1.7.12" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.7.12.tgz#feb25431a2b9c62f2404f5f49963523a9bed18e2" + integrity sha512-hbWi7U2UlglpT1LQZbm7He3YpSRYGoHwFMMKC+oCD9UzPImFJZOFrQUL4FQVsOaxxz0ggWK1Q/ZcK23LpG2STg== -"@unrs/resolver-binding-linux-arm-musleabihf@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.3.3.tgz#e1fc8440e54929b1f0f6aff6f6e3e9e19ac4a73c" - integrity sha512-8qQ6l1VTzLNd3xb2IEXISOKwMGXDCzY/UNy/7SovFW2Sp0K3YbL7Ao7R18v6SQkLqQlhhqSBIFRk+u6+qu5R5A== +"@unrs/resolver-binding-linux-arm-musleabihf@1.7.12": + version "1.7.12" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.7.12.tgz#76fc53f47a0cad563d4c00c2346fe25a4e346730" + integrity sha512-KBblhYFUhUTVSkTKxxaw4cFS3qgQMs2oM1DUSNrsFX7uRBG6SxXkLXGKua+uWq+G0vT7pp30BPXJ7c4I6vRGcw== -"@unrs/resolver-binding-linux-arm64-gnu@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.3.3.tgz#1249e18b5fa1419addda637d62ef201ce9bcf5a4" - integrity sha512-v81R2wjqcWXJlQY23byqYHt9221h4anQ6wwN64oMD/WAE+FmxPHFZee5bhRkNVtzqO/q7wki33VFWlhiADwUeQ== +"@unrs/resolver-binding-linux-arm64-gnu@1.7.12": + version "1.7.12" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.7.12.tgz#5d7e6ee9fb9e985bdb9833aefda269267b226a75" + integrity sha512-A5jGMNiY5F/KyeLkph5/gaNXNs/P/FUJvhKIP79mIOn9KUqjqx+UbhZQ1UrRuGHsh0gXPVSnu2UJdhnvJsnEyw== -"@unrs/resolver-binding-linux-arm64-musl@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.3.3.tgz#9af549ce9dde57b31c32a36cbe9eafa05f96befd" - integrity sha512-cAOx/j0u5coMg4oct/BwMzvWJdVciVauUvsd+GQB/1FZYKQZmqPy0EjJzJGbVzFc6gbnfEcSqvQE6gvbGf2N8Q== +"@unrs/resolver-binding-linux-arm64-musl@1.7.12": + version "1.7.12" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.7.12.tgz#6fdfafe4ff11cf43f18fe0ab8e9f9c98dc757d3d" + integrity sha512-Gv/duj5YStydJTNu2vSHFbSrRimpA6mnVmAnKTe1xMfhqDCm10EP/U6u7NII1jAjbpaRmqtnvFhtndzGxDyfhA== -"@unrs/resolver-binding-linux-ppc64-gnu@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.3.3.tgz#45aab52319f3e3b2627038a80c0331b0793a4be3" - integrity sha512-mq2blqwErgDJD4gtFDlTX/HZ7lNP8YCHYFij2gkXPtMzrXxPW1hOtxL6xg4NWxvnj4bppppb0W3s/buvM55yfg== +"@unrs/resolver-binding-linux-ppc64-gnu@1.7.12": + version "1.7.12" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.7.12.tgz#f1f36dc330d9410984c319fbcb982f31c180dc85" + integrity sha512-BTjdqhVVl1Q8dZCdNkVXZrfFyqNRdWizFuY5N0eHP7zgtNmqwJ3F/eJF/60GnabIcmWHvWvugby2VqHZtW/bQw== -"@unrs/resolver-binding-linux-s390x-gnu@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.3.3.tgz#7d2fe5c43e291d42e66d74fce07d9cf0050b4241" - integrity sha512-u0VRzfFYysarYHnztj2k2xr+eu9rmgoTUUgCCIT37Nr+j0A05Xk2c3RY8Mh5+DhCl2aYibihnaAEJHeR0UOFIQ== +"@unrs/resolver-binding-linux-riscv64-gnu@1.7.12": + version "1.7.12" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.7.12.tgz#a2e3acadc3731cdd55d26bc57c870ae8b16c4313" + integrity sha512-YkjQuWGi1o174Xz2R+oQOOYQgViCwkSdpsHGmLr0QRYgQclJCQ4ug6qT+EGTYi1g4994q3BAaFVgV0GzEM1YSQ== -"@unrs/resolver-binding-linux-x64-gnu@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.3.3.tgz#be54ff88c581610c42d8614475c0560f043d7ded" - integrity sha512-OrVo5ZsG29kBF0Ug95a2KidS16PqAMmQNozM6InbquOfW/udouk063e25JVLqIBhHLB2WyBnixOQ19tmeC/hIg== +"@unrs/resolver-binding-linux-riscv64-musl@1.7.12": + version "1.7.12" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.7.12.tgz#cf940218bf0fbcd5dc6b7a5ece75b67d16bd4bcd" + integrity sha512-9ud5x0qYBuk1rGdGzdjKQq/o7tObgI3IdjaufxKLD6kJIQi6vqww1mfoJklYw2OR5JXOWc6WLNKHa0Rr9aFZsw== -"@unrs/resolver-binding-linux-x64-musl@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.3.3.tgz#4efa7a1e4f7bf231098ed23df1e19174d360c24f" - integrity sha512-PYnmrwZ4HMp9SkrOhqPghY/aoL+Rtd4CQbr93GlrRTjK6kDzfMfgz3UH3jt6elrQAfupa1qyr1uXzeVmoEAxUA== +"@unrs/resolver-binding-linux-s390x-gnu@1.7.12": + version "1.7.12" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.7.12.tgz#9efb533f226da434709bd43180ff7718b88816e9" + integrity sha512-3CNVBpgsvZ4Vrr18JAxQ8Qxz+k4PqTJR05/xn5Tczs9jFEuxPDxZKFskv9QnK3flJtx+ur9MayiTGgFZQAa7hA== -"@unrs/resolver-binding-wasm32-wasi@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.3.3.tgz#6df454b4a9b28d47850bcb665d243f09101b782c" - integrity sha512-81AnQY6fShmktQw4hWDUIilsKSdvr/acdJ5azAreu2IWNlaJOKphJSsUVWE+yCk6kBMoQyG9ZHCb/krb5K0PEA== +"@unrs/resolver-binding-linux-x64-gnu@1.7.12": + version "1.7.12" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.7.12.tgz#fc3094d404838720ab04b9c34ecf3b3de1211dc6" + integrity sha512-bPACcY7lEp3M8IItjXEppQEsQ2N54a1aLb1yCWD16lccl8OG9aXQvji9x9VVcmdqR4JV4oWXzr0uIrZ+oFNvOw== + +"@unrs/resolver-binding-linux-x64-musl@1.7.12": + version "1.7.12" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.7.12.tgz#d224c9c531ae53db1d1482f59175ce554cc51522" + integrity sha512-86WuRbj+0tK3qWPthfsR952CHxE23lDTjbKfHOczIkjRvKP/ggAzaiNMOEljxB5iel4HhGTQZBp1lx61aw2q/g== + +"@unrs/resolver-binding-wasm32-wasi@1.7.12": + version "1.7.12" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.7.12.tgz#604ba5dd33678df88c3537bea98e1f6ace96ccf4" + integrity sha512-RzWV0OyeARtKRNHSbVZyj869P+WHzT2OFEgigs+5qEIM8X3QzbQ90Ye/1hCvgu0zi/cDCXtKWp8Utr1Oym2UIA== dependencies: - "@napi-rs/wasm-runtime" "^0.2.7" + "@napi-rs/wasm-runtime" "^0.2.11" -"@unrs/resolver-binding-win32-arm64-msvc@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.3.3.tgz#fb19e118350e1392993a0a6565b427d38c1c1760" - integrity sha512-X/42BMNw7cW6xrB9syuP5RusRnWGoq+IqvJO8IDpp/BZg64J1uuIW6qA/1Cl13Y4LyLXbJVYbYNSKwR/FiHEng== +"@unrs/resolver-binding-win32-arm64-msvc@1.7.12": + version "1.7.12" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.7.12.tgz#a60f4256560350ada479b39da0e2db22ecd85d9f" + integrity sha512-s9FdWj2QFT6PLNY/jPPmd7jF1Fn2HNSuLbZqUpdcSaMdeBGaDvy2C/eBYgGhrK7g8kIYUecT1LdT+SuDs6h+YA== -"@unrs/resolver-binding-win32-ia32-msvc@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.3.3.tgz#23a9c4b5621bba2d472bc78fadde7273a8c4548d" - integrity sha512-EGNnNGQxMU5aTN7js3ETYvuw882zcO+dsVjs+DwO2j/fRVKth87C8e2GzxW1L3+iWAXMyJhvFBKRavk9Og1Z6A== +"@unrs/resolver-binding-win32-ia32-msvc@1.7.12": + version "1.7.12" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.7.12.tgz#918d19beb57d6475ac843092d3917910352c5205" + integrity sha512-Fo4Op3Il/6HZ8Gm+YhqBkRZpTGe/QJZWAsCB/CLYBDqJ2NjXptgFsuIvlgXv95+WUkoTw6ifRgxE9gwtcAk5YA== -"@unrs/resolver-binding-win32-x64-msvc@1.3.3": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.3.3.tgz#eee226e5b4c4d91c862248afd24452c8698ed542" - integrity sha512-GraLbYqOJcmW1qY3osB+2YIiD62nVf2/bVLHZmrb4t/YSUwE03l7TwcDJl08T/Tm3SVhepX8RQkpzWbag/Sb4w== - -"@wry/context@^0.4.0": - version "0.4.4" - resolved "https://registry.yarnpkg.com/@wry/context/-/context-0.4.4.tgz#e50f5fa1d6cfaabf2977d1fda5ae91717f8815f8" - integrity sha512-LrKVLove/zw6h2Md/KZyWxIkFM6AoyKp71OqpH9Hiip1csjPVoD3tPxlbQUNxEnHENks3UGgNpSBCAfq9KWuag== - dependencies: - "@types/node" ">=6" - tslib "^1.9.3" +"@unrs/resolver-binding-win32-x64-msvc@1.7.12": + version "1.7.12" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.7.12.tgz#a8b737e111b72608e7abebe8d6c223d07304e687" + integrity sha512-00cVr73Qizmx7qycr9aO5NBofoAHuQIhNsuqj+I2Bun/yMddLfpXk86K3GWj096jXLzk0u/77u3qUTJPhuYWiw== "@wry/equality@^0.1.2": version "0.1.11" @@ -2379,6 +2906,14 @@ accepts@^1.3.5, accepts@~1.3.8: mime-types "~2.1.34" negotiator "0.6.3" +accepts@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895" + integrity sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng== + dependencies: + mime-types "^3.0.0" + negotiator "^1.0.0" + acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -2389,6 +2924,16 @@ acorn-walk@^8.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== +acorn@^7.1.1: + version "7.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== + +acorn@^8.14.0, acorn@^8.5.0: + version "8.14.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb" + integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== + acorn@^8.4.1: version "8.9.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.9.0.tgz#78a16e3b2bcc198c10822786fa6679e245db5b59" @@ -2435,6 +2980,19 @@ ajv@^6.12.3, ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +alce@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/alce/-/alce-1.2.0.tgz#a8be2dacaac42494612f18dc09db691f3dea4aab" + integrity sha512-XppPf2S42nO2WhvKzlwzlfcApcXHzjlod30pKmcWjRgLOtqoe5DMuqdiYoM6AgyXksc6A6pV4v1L/WW217e57w== + dependencies: + esprima "^1.2.0" + estraverse "^1.5.0" + +ansi-colors@^4.1.1: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== + ansi-escapes@^4.2.1: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" @@ -2447,11 +3005,6 @@ ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -ansi-regex@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" - integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== - ansi-styles@^3.1.0: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -2471,11 +3024,6 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== -ansi-styles@^6.1.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" - integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== - anymatch@^3.0.3, anymatch@~3.1.2: version "3.1.3" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" @@ -2492,39 +3040,6 @@ apollo-cache-control@^0.11.4: apollo-server-env "^2.4.5" apollo-server-plugin-base "^0.10.2" -apollo-cache-inmemory@~1.6.6: - version "1.6.6" - resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.6.tgz#56d1f2a463a6b9db32e9fa990af16d2a008206fd" - integrity sha512-L8pToTW/+Xru2FFAhkZ1OA9q4V4nuvfoPecBM34DecAugUZEBhI2Hmpgnzq2hTKZ60LAMrlqiASm0aqAY6F8/A== - dependencies: - apollo-cache "^1.3.5" - apollo-utilities "^1.3.4" - optimism "^0.10.0" - ts-invariant "^0.4.0" - tslib "^1.10.0" - -apollo-cache@1.3.5, apollo-cache@^1.3.5: - version "1.3.5" - resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.3.5.tgz#9dbebfc8dbe8fe7f97ba568a224bca2c5d81f461" - integrity sha512-1XoDy8kJnyWY/i/+gLTEbYLnoiVtS8y7ikBr/IfmML4Qb+CM7dEEbIUOjnY716WqmZ/UpXIxTfJsY7rMcqiCXA== - dependencies: - apollo-utilities "^1.3.4" - tslib "^1.10.0" - -apollo-client@~2.6.10: - version "2.6.10" - resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.6.10.tgz#86637047b51d940c8eaa771a4ce1b02df16bea6a" - integrity sha512-jiPlMTN6/5CjZpJOkGeUV0mb4zxx33uXWdj/xQCfAMkuNAC3HN7CvYDyMHHEzmcQ5GV12LszWoQ/VlxET24CtA== - dependencies: - "@types/zen-observable" "^0.8.0" - apollo-cache "1.3.5" - apollo-link "^1.0.0" - apollo-utilities "1.3.4" - symbol-observable "^1.0.2" - ts-invariant "^0.4.0" - tslib "^1.10.0" - zen-observable "^0.8.0" - apollo-datasource@^0.7.2: version "0.7.2" resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.7.2.tgz#1662ee93453a9b89af6f73ce561bde46b41ebf31" @@ -2566,15 +3081,7 @@ apollo-graphql@^0.6.0: apollo-env "^0.6.5" lodash.sortby "^4.7.0" -apollo-link-context@~1.0.20: - version "1.0.20" - resolved "https://registry.yarnpkg.com/apollo-link-context/-/apollo-link-context-1.0.20.tgz#1939ac5dc65d6dff0c855ee53521150053c24676" - integrity sha512-MLLPYvhzNb8AglNsk2NcL9AvhO/Vc9hn2ZZuegbhRHGet3oGr0YH9s30NS9+ieoM0sGT11p7oZ6oAILM/kiRBA== - dependencies: - apollo-link "^1.2.14" - tslib "^1.9.3" - -apollo-link-http-common@^0.2.14, apollo-link-http-common@^0.2.16: +apollo-link-http-common@^0.2.14: version "0.2.16" resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.16.tgz#756749dafc732792c8ca0923f9a40564b7c59ecc" integrity sha512-2tIhOIrnaF4UbQHf7kjeQA/EmSorB7+HyJIIrUjJOKBgnXwuexi8aMecRlqTIDWcyVXCeqLhUnztMa6bOH/jTg== @@ -2583,16 +3090,7 @@ apollo-link-http-common@^0.2.14, apollo-link-http-common@^0.2.16: ts-invariant "^0.4.0" tslib "^1.9.3" -apollo-link-http@~1.5.17: - version "1.5.17" - resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.17.tgz#499e9f1711bf694497f02c51af12d82de5d8d8ba" - integrity sha512-uWcqAotbwDEU/9+Dm9e1/clO7hTB2kQ/94JYcGouBVLjoKmTeJTUPQKcJGpPwUjZcSqgYicbFqQSoJIW0yrFvg== - dependencies: - apollo-link "^1.2.14" - apollo-link-http-common "^0.2.16" - tslib "^1.9.3" - -apollo-link@^1.0.0, apollo-link@^1.2.12, apollo-link@^1.2.14: +apollo-link@^1.2.12, apollo-link@^1.2.14: version "1.2.14" resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.14.tgz#3feda4b47f9ebba7f4160bef8b977ba725b684d9" integrity sha512-p67CMEFP7kOG1JZ0ZkYZwRDa369w5PIjtMjvrQd/HnIV8FRsHRqLqK+oAZQnFa1DDdZtOtHTi+aMIW6EatC2jg== @@ -2759,7 +3257,7 @@ apollo-upload-client@^13.0.0: apollo-link-http-common "^0.2.14" extract-files "^8.0.0" -apollo-utilities@1.3.4, apollo-utilities@^1.0.1, apollo-utilities@^1.3.0, apollo-utilities@^1.3.4: +apollo-utilities@^1.0.1, apollo-utilities@^1.3.0: version "1.3.4" resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.4.tgz#6129e438e8be201b6c55b0f13ce49d2c7175c9cf" integrity sha512-pk2hiWrCXMAy2fRPwEyhvka+mqwzeP60Jr1tRYi5xru+3ko94HI9o6lK0CT33/w4RDlxWchmdhDCrvdr+pHCig== @@ -2812,6 +3310,11 @@ array-buffer-byte-length@^1.0.1: call-bind "^1.0.5" is-array-buffer "^3.0.4" +array-differ@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" + integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg== + array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -2893,6 +3396,16 @@ arraybuffer.prototype.slice@^1.0.3: is-array-buffer "^3.0.4" is-shared-array-buffer "^1.0.2" +arrify@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" + integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== + +asap@~2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== + asn1@~0.2.3: version "0.2.6" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" @@ -2900,6 +3413,11 @@ asn1@~0.2.3: dependencies: safer-buffer "~2.1.0" +assert-never@^1.2.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/assert-never/-/assert-never-1.4.0.tgz#b0d4988628c87f35eb94716cc54422a63927e175" + integrity sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA== + assert-plus@1.0.0, assert-plus@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" @@ -2962,22 +3480,6 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" -aws-sdk@^2.1692.0: - version "2.1692.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1692.0.tgz#9dac5f7bfcc5ab45825cc8591b12753aa7d2902c" - integrity sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw== - dependencies: - buffer "4.9.2" - events "1.1.1" - ieee754 "1.1.13" - jmespath "0.16.0" - querystring "0.2.0" - sax "1.2.1" - url "0.10.3" - util "^0.12.4" - uuid "8.0.0" - xml2js "0.6.2" - aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -2988,24 +3490,7 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.12.0.tgz#ce1c9d143389679e253b314241ea9aa5cec980d3" integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg== -babel-core@~7.0.0-0: - version "7.0.0-bridge.0" - resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-7.0.0-bridge.0.tgz#95a492ddd90f9b4e9a4a1da14eb335b87b634ece" - integrity sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg== - -babel-eslint@~10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.1.0.tgz#6968e568a910b78fb3779cdd8b6ac2f479943232" - integrity sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg== - dependencies: - "@babel/code-frame" "^7.0.0" - "@babel/parser" "^7.7.0" - "@babel/traverse" "^7.7.0" - "@babel/types" "^7.7.0" - eslint-visitor-keys "^1.0.0" - resolve "^1.12.0" - -babel-jest@^29.7.0, babel-jest@~29.7.0: +babel-jest@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg== @@ -3039,37 +3524,6 @@ babel-plugin-jest-hoist@^29.6.3: "@types/babel__core" "^7.1.14" "@types/babel__traverse" "^7.0.6" -babel-plugin-polyfill-corejs2@^0.4.10: - version "0.4.10" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.10.tgz#276f41710b03a64f6467433cab72cbc2653c38b1" - integrity sha512-rpIuu//y5OX6jVU+a5BCn1R5RSZYWAl2Nar76iwaOdycqb6JPxediskWFMMl7stfwNJR4b7eiQvh5fB5TEQJTQ== - dependencies: - "@babel/compat-data" "^7.22.6" - "@babel/helper-define-polyfill-provider" "^0.6.1" - semver "^6.3.1" - -babel-plugin-polyfill-corejs3@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz#4e4e182f1bb37c7ba62e2af81d8dd09df31344f6" - integrity sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.6.3" - core-js-compat "^3.40.0" - -babel-plugin-polyfill-regenerator@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.1.tgz#4f08ef4c62c7a7f66a35ed4c0d75e30506acc6be" - integrity sha512-JfTApdE++cgcTWjsiCQlLyFBMbTUft9ja17saCc93lgV33h4tuCVj7tlvu//qpLwaG+3yEz7/KhahGrUMkVq9g== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.6.1" - -babel-plugin-transform-runtime@^6.23.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.23.0.tgz#88490d446502ea9b8e7efb0fe09ec4d99479b1ee" - integrity sha1-iEkNRGUC6puOfvsP4J7E2ZR5se4= - dependencies: - babel-runtime "^6.22.0" - babel-preset-current-node-syntax@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" @@ -3096,13 +3550,12 @@ babel-preset-jest@^29.6.3: babel-plugin-jest-hoist "^29.6.3" babel-preset-current-node-syntax "^1.0.0" -babel-runtime@^6.22.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" - integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= +babel-walk@3.0.0-canary-5: + version "3.0.0-canary-5" + resolved "https://registry.yarnpkg.com/babel-walk/-/babel-walk-3.0.0-canary-5.tgz#f66ecd7298357aee44955f235a6ef54219104b11" + integrity sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw== dependencies: - core-js "^2.4.0" - regenerator-runtime "^0.11.0" + "@babel/types" "^7.9.6" backo2@^1.0.2: version "1.0.2" @@ -3131,17 +3584,17 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" -bcryptjs@~2.4.3: - version "2.4.3" - resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb" - integrity sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms= +bcryptjs@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-3.0.2.tgz#caadcca1afefe372ed6e20f86db8e8546361c1ca" + integrity sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog== binary-extensions@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== -body-parser@1.20.3, body-parser@^1.18.3: +body-parser@1.20.3, body-parser@^1.18.3, body-parser@^1.20.3: version "1.20.3" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== @@ -3159,11 +3612,36 @@ body-parser@1.20.3, body-parser@^1.18.3: type-is "~1.6.18" unpipe "1.0.0" +body-parser@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.2.0.tgz#f7a9656de305249a715b549b7b8fd1ab9dfddcfa" + integrity sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg== + dependencies: + bytes "^3.1.2" + content-type "^1.0.5" + debug "^4.4.0" + http-errors "^2.0.0" + iconv-lite "^0.6.3" + on-finished "^2.4.1" + qs "^6.14.0" + raw-body "^3.0.0" + type-is "^2.0.0" + boolbase@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= +boolean@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.2.0.tgz#9e5294af4e98314494cbb17979fa54ca159f116b" + integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw== + +bowser@^2.11.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f" + integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -3196,16 +3674,6 @@ browserslist@^4.24.0: node-releases "^2.0.18" update-browserslist-db "^1.1.0" -browserslist@^4.24.3: - version "4.24.4" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.4.tgz#c6b2865a3f08bcb860a0e827389003b9fe686e4b" - integrity sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A== - dependencies: - caniuse-lite "^1.0.30001688" - electron-to-chromium "^1.5.73" - node-releases "^2.0.19" - update-browserslist-db "^1.1.1" - bs-logger@^0.2.6: version "0.2.6" resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" @@ -3230,14 +3698,13 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer@4.9.2: - version "4.9.2" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" - integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== +buffer@5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" + integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== dependencies: base64-js "^1.0.2" ieee754 "^1.1.4" - isarray "^1.0.0" buffer@^6.0.3: version "6.0.3" @@ -3247,18 +3714,6 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" -builtin-modules@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" - integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== - -builtins@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/builtins/-/builtins-5.0.1.tgz#87f6db9ab0458be728564fa81d876d8d74552fa9" - integrity sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ== - dependencies: - semver "^7.0.0" - busboy@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.3.1.tgz#170899274c5bf38aae27d5c62b71268cd585fd1b" @@ -3266,7 +3721,7 @@ busboy@^0.3.1: dependencies: dicer "0.3.0" -bytes@3.1.2: +bytes@3.1.2, bytes@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== @@ -3343,6 +3798,14 @@ call-bind@^1.0.6, call-bind@^1.0.7: get-intrinsic "^1.2.4" set-function-length "^1.2.1" +call-bound@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -3371,11 +3834,6 @@ caniuse-lite@^1.0.30001663: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz#fda8f1d29a8bfdc42de0c170d7f34a9cf19ed7a3" integrity sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w== -caniuse-lite@^1.0.30001688: - version "1.0.30001700" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz#26cd429cf09b4fd4e745daf4916039c794d720f6" - integrity sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ== - caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -3390,6 +3848,14 @@ chalk@2.3.0: escape-string-regexp "^1.0.5" supports-color "^4.0.0" +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -3403,6 +3869,24 @@ char-regex@^1.0.2: resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== +character-parser@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/character-parser/-/character-parser-2.2.0.tgz#c7ce28f36d4bcd9744e5ffc2c5fcde1c73261fc0" + integrity sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw== + dependencies: + is-regex "^1.0.3" + +cheerio-select@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-1.6.0.tgz#489f36604112c722afa147dedd0d4609c09e1696" + integrity sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g== + dependencies: + css-select "^4.3.0" + css-what "^6.0.1" + domelementtype "^2.2.0" + domhandler "^4.3.1" + domutils "^2.8.0" + cheerio-select@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4" @@ -3415,6 +3899,32 @@ cheerio-select@^2.1.0: domhandler "^5.0.3" domutils "^3.0.1" +cheerio@1.0.0-rc.10: + version "1.0.0-rc.10" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.10.tgz#2ba3dcdfcc26e7956fc1f440e61d51c643379f3e" + integrity sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw== + dependencies: + cheerio-select "^1.5.0" + dom-serializer "^1.3.2" + domhandler "^4.2.0" + htmlparser2 "^6.1.0" + parse5 "^6.0.1" + parse5-htmlparser2-tree-adapter "^6.0.1" + tslib "^2.2.0" + +cheerio@1.0.0-rc.12: + version "1.0.0-rc.12" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.12.tgz#788bf7466506b1c6bf5fae51d24a2c4d62e47683" + integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q== + 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" + cheerio@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0.tgz#1ede4895a82f26e8af71009f961a9b8cb60d6a81" @@ -3432,7 +3942,7 @@ cheerio@~1.0.0: undici "^6.19.5" whatwg-mimetype "^4.0.0" -chokidar@^3.5.2, chokidar@^3.6.0: +chokidar@^3.5.2, chokidar@^3.5.3: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== @@ -3471,6 +3981,11 @@ ci-info@^3.2.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== +ci-info@^3.8.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== + cjs-module-lexer@^1.0.0: version "1.2.2" resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" @@ -3498,15 +4013,6 @@ cliui@^8.0.1: strip-ansi "^6.0.1" wrap-ansi "^7.0.0" -clone-deep@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" - integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== - dependencies: - is-plain-object "^2.0.4" - kind-of "^6.0.2" - shallow-clone "^3.0.0" - clone-response@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" @@ -3570,15 +4076,15 @@ commander@^2.20.3: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== -commander@^6.2.0: +commander@^6.1.0: version "6.2.1" resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== -commondir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= +commander@^9.0.0: + version "9.5.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" + integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== concat-map@0.0.1: version "0.0.1" @@ -3595,6 +4101,14 @@ condense-whitespace@~2.0.0: resolved "https://registry.yarnpkg.com/condense-whitespace/-/condense-whitespace-2.0.0.tgz#94e9644938f66aa7be4b8849f8f0b3cec97d6b3a" integrity sha512-Ath9o58/0rxZXbyoy3zZgrVMoIemi30sukG/btuMKCLyqfQt3dNOWc9N3EHEMa2Q3i0tXQPDJluYFLwy7pJuQw== +constantinople@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/constantinople/-/constantinople-4.0.1.tgz#0def113fa0e4dc8de83331a5cf79c8b325213151" + integrity sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw== + dependencies: + "@babel/parser" "^7.6.0" + "@babel/types" "^7.6.1" + content-disposition@0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" @@ -3602,7 +4116,14 @@ content-disposition@0.5.4: dependencies: safe-buffer "5.2.1" -content-type@~1.0.4, content-type@~1.0.5: +content-disposition@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.0.0.tgz#844426cb398f934caefcbb172200126bc7ceace2" + integrity sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg== + dependencies: + safe-buffer "5.2.1" + +content-type@^1.0.5, content-type@~1.0.4, content-type@~1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== @@ -3617,6 +4138,11 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= +cookie-signature@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" + integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== + cookie@0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" @@ -3627,24 +4153,22 @@ cookie@^0.3.1: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= -core-js-compat@^3.40.0: - version "3.40.0" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.40.0.tgz#7485912a5a4a4315c2fdb2cbdc623e6881c88b38" - integrity sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ== - dependencies: - browserslist "^4.24.3" +cookie@^0.7.1: + version "0.7.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== core-js-pure@^3.30.2: version "3.40.0" resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.40.0.tgz#d9a019e9160f9b042eeb6abb92242680089d486e" integrity sha512-AtDzVIgRrmRKQai62yuSIN5vNiQjcJakJb4fbhVw3ehxx7Lohphvw9SGNWKhLFqSxC4ilD0g/L1huAYFQU3Q6A== -core-js@^2.4.0, core-js@^2.6.5: +core-js@^2.6.5: version "2.6.9" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== -core-js@^3.0.1, core-js@^3.30.2: +core-js@^3.0.1: version "3.36.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.36.1.tgz#c97a7160ebd00b2de19e62f4bbd3406ab720e578" integrity sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA== @@ -3654,7 +4178,7 @@ core-util-is@1.0.2: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= -cors@^2.8.4, cors@~2.8.5: +cors@^2.8.4: version "2.8.5" resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== @@ -3696,6 +4220,17 @@ cross-spawn@^5.0.1: shebang-command "^1.2.0" which "^1.2.9" +cross-spawn@^6.0.0: + version "6.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.6.tgz#30d0efa0712ddb7eb5a76e1e8721bffafa6b5d57" + integrity sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -3714,6 +4249,17 @@ cross-spawn@^7.0.6: shebang-command "^2.0.0" which "^2.0.1" +css-select@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" + integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== + dependencies: + boolbase "^1.0.0" + css-what "^6.0.1" + domhandler "^4.3.1" + domutils "^2.8.0" + nth-check "^2.0.1" + css-select@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" @@ -3725,7 +4271,7 @@ css-select@^5.1.0: domutils "^3.0.1" nth-check "^2.0.1" -css-what@^6.1.0: +css-what@^6.0.1, css-what@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== @@ -3840,10 +4386,10 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" - integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== +debug@4, debug@^4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@^4.3.5, debug@^4.4.0, debug@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" + integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== dependencies: ms "^2.1.3" @@ -3871,6 +4417,11 @@ dedent@^1.0.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.1.tgz#4f3fc94c8b711e9bb2800d185cd6ad20f2a90aff" integrity sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg== +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + deep-is@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" @@ -3881,10 +4432,10 @@ deepmerge@4.2.2: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== -deepmerge@^4.2.2: - version "4.3.0" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.0.tgz#65491893ec47756d44719ae520e0e2609233b59b" - integrity sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og== +deepmerge@^4.2.2, deepmerge@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== defer-to-connect@^2.0.0: version "2.0.1" @@ -3938,17 +4489,12 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== -denque@^1.1.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf" - integrity sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ== - denque@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== -depd@2.0.0: +depd@2.0.0, depd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== @@ -3968,7 +4514,12 @@ destroy@1.2.0: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== -detect-newline@^3.0.0: +detect-indent@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" + integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== + +detect-newline@^3.0.0, detect-newline@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== @@ -3997,6 +4548,14 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" +display-notification@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/display-notification/-/display-notification-2.0.0.tgz#49fad2e03289b4f668c296e1855c2cf8ba893d49" + integrity sha512-TdmtlAcdqy1NU+j7zlkDdMnCL878zriLaBmoD9quOoq1ySSSGv03l0hXK5CvIFZlIfFI/hizqdQuW+Num7xuhw== + dependencies: + escape-string-applescript "^1.0.0" + run-applescript "^3.0.0" + doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -4011,6 +4570,11 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +doctypes@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9" + integrity sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ== + dom-serializer@^1.0.1: version "1.3.2" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91" @@ -4020,6 +4584,15 @@ dom-serializer@^1.0.1: domhandler "^4.2.0" entities "^2.0.0" +dom-serializer@^1.3.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" + integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.2.0" + entities "^2.0.0" + dom-serializer@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" @@ -4044,6 +4617,13 @@ domelementtype@^2.3.0: resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== +domhandler@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-3.3.0.tgz#6db7ea46e4617eb15cf875df68b2b8524ce0037a" + integrity sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA== + dependencies: + domelementtype "^2.0.1" + domhandler@^4.0.0, domhandler@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.0.tgz#f9768a5f034be60a89a27c2e4d0f74eba0d8b059" @@ -4051,6 +4631,13 @@ domhandler@^4.0.0, domhandler@^4.2.0: dependencies: domelementtype "^2.2.0" +domhandler@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" + integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== + dependencies: + domelementtype "^2.2.0" + domhandler@^5.0.2, domhandler@^5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" @@ -4058,6 +4645,15 @@ domhandler@^5.0.2, domhandler@^5.0.3: dependencies: domelementtype "^2.3.0" +domutils@^2.4.2, domutils@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + domutils@^2.5.2: version "2.7.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.7.0.tgz#8ebaf0c41ebafcf55b0b72ec31c56323712c5442" @@ -4076,10 +4672,10 @@ domutils@^3.0.1, domutils@^3.1.0: domelementtype "^2.3.0" domhandler "^5.0.3" -dotenv@^16.0.0, dotenv@~16.4.7: - version "16.4.7" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.7.tgz#0e20c5b82950140aa99be360a8a5f52335f53c26" - integrity sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ== +dotenv@^16.0.0, dotenv@~16.5.0: + version "16.5.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.5.0.tgz#092b49f25f808f020050051d1ff258e404c78692" + integrity sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg== dotenv@^4.0.0: version "4.0.0" @@ -4095,11 +4691,6 @@ dunder-proto@^1.0.1: es-errors "^1.3.0" gopd "^1.2.0" -eastasianwidth@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" - integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== - ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" @@ -4132,10 +4723,20 @@ electron-to-chromium@^1.5.28: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.41.tgz#eae1ba6c49a1a61d84cf8263351d3513b2bcc534" integrity sha512-dfdv/2xNjX0P8Vzme4cfzHqnPm5xsZXwsolTYr0eyW18IUmNyG08vL+fttvinTfhKfIKdRoqkDIC9e9iWQCNYQ== -electron-to-chromium@^1.5.73: - version "1.5.103" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.103.tgz#3d02025bc16e96e5edb3ed3ffa2538a11ae682dc" - integrity sha512-P6+XzIkfndgsrjROJWfSvVEgNHtPgbhVyTkwLjUM2HU/h7pZRORgaTlHqfAikqxKmdJMLW8fftrdGWbd/Ds0FA== +email-templates@^12.0.3: + version "12.0.3" + resolved "https://registry.yarnpkg.com/email-templates/-/email-templates-12.0.3.tgz#e35a6528460d9d3b838468d05fedde1299ad5281" + integrity sha512-tCjkmZYakXkKfL3/qZJ7esCa04KP5zIpcuEjw9EPLQrLbTUUkX6w9MMc37zGj2nJvIpFBc1lUudHi5DkZqiNJQ== + dependencies: + "@ladjs/consolidate" "^1.0.4" + "@ladjs/i18n" "^8.0.3" + get-paths "^0.0.7" + html-to-text "^9.0.5" + juice "^10.0.0" + lodash "^4.17.21" + nodemailer "^6.9.14" + optionalDependencies: + preview-email "^3.0.17" emittery@^0.13.1: version "0.13.1" @@ -4147,20 +4748,20 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -emoji-regex@^9.2.2: - version "9.2.2" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" - integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== +encodeurl@^2.0.0, encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= -encodeurl@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" - integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== +encoding-japanese@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/encoding-japanese/-/encoding-japanese-2.2.0.tgz#0ef2d2351250547f432a2dd155453555c16deb59" + integrity sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A== encoding-sniffer@^0.2.0: version "0.2.0" @@ -4184,6 +4785,14 @@ end-of-stream@^1.1.0: dependencies: once "^1.4.0" +enhanced-resolve@^5.17.1: + version "5.18.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz#728ab082f8b7b6836de51f1637aab5d3b9568faf" + integrity sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + entities@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4" @@ -4335,18 +4944,6 @@ es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.2: unbox-primitive "^1.0.2" which-typed-array "^1.1.15" -es-abstract@^1.5.1: - version "1.13.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9" - integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg== - dependencies: - es-to-primitive "^1.2.0" - function-bind "^1.1.1" - has "^1.0.3" - is-callable "^1.1.4" - is-regex "^1.0.4" - object-keys "^1.0.12" - es-define-property@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" @@ -4420,15 +5017,6 @@ es-shim-unscopables@^1.0.2: dependencies: hasown "^2.0.0" -es-to-primitive@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" - integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" @@ -4480,16 +5068,26 @@ escalade@^3.1.1: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== -escalade@^3.1.2, escalade@^3.2.0: +escalade@^3.1.2: version "3.2.0" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== -escape-html@~1.0.3: +escape-goat@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-3.0.0.tgz#e8b5fb658553fe8a3c4959c316c6ebb8c842b19c" + integrity sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw== + +escape-html@^1.0.3, escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= +escape-string-applescript@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/escape-string-applescript/-/escape-string-applescript-1.0.0.tgz#6f1c2294245d82c63bc03338dc19a94aa8428892" + integrity sha512-4/hFwoYaC6TkpDn9A3pTC52zQPArFeXuIfhUtCGYdauTzXVP9H3BDr3oO/QzQehMpLDC7srvYgfwvImPFGfvBA== + escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -4505,21 +5103,38 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-compat-utils@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/eslint-compat-utils/-/eslint-compat-utils-0.1.2.tgz#f45e3b5ced4c746c127cf724fb074cd4e730d653" - integrity sha512-Jia4JDldWnFNIru1Ehx1H5s9/yxiRHY/TimCuUc0jNexew3cF1gI6CYZil1ociakfWO3rRqFjl1mskBblB3RYg== +eslint-compat-utils@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz#7fc92b776d185a70c4070d03fd26fde3d59652e4" + integrity sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q== + dependencies: + semver "^7.5.4" -eslint-config-prettier@^10.1.1: - version "10.1.1" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.1.tgz#cf0ff6e5c4e7e15f129f1f1ce2a5ecba92dec132" - integrity sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw== +eslint-compat-utils@^0.6.4: + version "0.6.5" + resolved "https://registry.yarnpkg.com/eslint-compat-utils/-/eslint-compat-utils-0.6.5.tgz#6b06350a1c947c4514cfa64a170a6bfdbadc7ec2" + integrity sha512-vAUHYzue4YAa2hNACjB8HvUQj5yehAZgiClyFVVom9cP8z5NSFq3PwB/TtJslN2zAMgRX6FCFCjYBbQh71g5RQ== + dependencies: + semver "^7.5.4" + +eslint-config-prettier@^10.1.5: + version "10.1.5" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz#00c18d7225043b6fbce6a665697377998d453782" + integrity sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw== eslint-config-standard@^17.1.0: version "17.1.0" resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz#40ffb8595d47a6b242e07cbfd49dc211ed128975" integrity sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q== +eslint-import-context@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/eslint-import-context/-/eslint-import-context-0.1.8.tgz#4098b659f4c1a3b3cc3bc896c3baee7907ca636d" + integrity sha512-bq+F7nyc65sKpZGT09dY0S0QrOnQtuDVIfyTGQ8uuvtMIF7oHp6CEP3mouN0rrnYF3Jqo6Ke0BfU/5wASZue1w== + dependencies: + get-tsconfig "^4.10.1" + stable-hash-x "^0.1.1" + eslint-import-resolver-node@^0.3.9: version "0.3.9" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" @@ -4529,17 +5144,25 @@ eslint-import-resolver-node@^0.3.9: is-core-module "^2.13.0" resolve "^1.22.4" -eslint-import-resolver-typescript@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.3.1.tgz#6721c639716de3685363ddb284e2cec60cee60ee" - integrity sha512-/dR9YMomeBlvfuvX5q0C3Y/2PHC9OCRdT2ijFwdfq/4Bq+4m5/lqstEp9k3P6ocha1pCbhoY9fkwVYLmOqR0VQ== +eslint-import-resolver-typescript@^4.4.3: + version "4.4.3" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.3.tgz#7fdd06f8fd7cdb05656980e3fe9d2c246365b2a2" + integrity sha512-elVDn1eWKFrWlzxlWl9xMt8LltjKl161Ix50JFC50tHXI5/TRP32SNEqlJ/bo/HV+g7Rou/tlPQU2AcRtIhrOg== dependencies: - debug "^4.4.0" - get-tsconfig "^4.10.0" + debug "^4.4.1" + eslint-import-context "^0.1.8" + get-tsconfig "^4.10.1" is-bun-module "^2.0.0" - stable-hash "^0.0.5" - tinyglobby "^0.2.12" - unrs-resolver "^1.3.3" + stable-hash-x "^0.1.1" + tinyglobby "^0.2.14" + unrs-resolver "^1.7.11" + +eslint-json-compat-utils@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/eslint-json-compat-utils/-/eslint-json-compat-utils-0.2.1.tgz#32931d42c723da383712f25177a2c57b9ef5f079" + integrity sha512-YzEodbDyW8DX8bImKhAcCeu/L31Dd/70Bidx2Qex9OFUtgzXLqtfWL4Hr5fM/aCCB8QUZLuJur0S9k6UfgFkfg== + dependencies: + esquery "^1.6.0" eslint-module-utils@^2.12.0: version "2.12.0" @@ -4548,14 +5171,14 @@ eslint-module-utils@^2.12.0: dependencies: debug "^3.2.7" -eslint-plugin-es-x@^7.5.0: - version "7.5.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-es-x/-/eslint-plugin-es-x-7.5.0.tgz#d08d9cd155383e35156c48f736eb06561d07ba92" - integrity sha512-ODswlDSO0HJDzXU0XvgZ3lF3lS3XAZEossh15Q2UHjwrJggWeBoKqqEsLTZLXl+dh5eOAozG0zRcYtuE35oTuQ== +eslint-plugin-es-x@^7.8.0: + version "7.8.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-es-x/-/eslint-plugin-es-x-7.8.0.tgz#a207aa08da37a7923f2a9599e6d3eb73f3f92b74" + integrity sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ== dependencies: "@eslint-community/eslint-utils" "^4.1.2" - "@eslint-community/regexpp" "^4.6.0" - eslint-compat-utils "^0.1.2" + "@eslint-community/regexpp" "^4.11.0" + eslint-compat-utils "^0.5.1" eslint-plugin-import@^2.31.0: version "2.31.0" @@ -4582,42 +5205,62 @@ eslint-plugin-import@^2.31.0: string.prototype.trimend "^1.0.8" tsconfig-paths "^3.15.0" -eslint-plugin-jest@^28.11.0: - version "28.11.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-28.11.0.tgz#2641ecb4411941bbddb3d7cf8a8ff1163fbb510e" - integrity sha512-QAfipLcNCWLVocVbZW8GimKn5p5iiMcgGbRzz8z/P5q7xw+cNEpYqyzFMtIF/ZgF2HLOyy+dYBut+DoYolvqig== +eslint-plugin-jest@^28.13.0: + version "28.13.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-28.13.0.tgz#f215195915cefe3ed14145f841b6e7c90d60a672" + integrity sha512-4AuBcFWOriOeEqy6s4Zup/dQ7E1EPTyyfDaMYmM2YP9xEWPWwK3yYifH1dzY6aHRvyx7y53qMSIyT5s+jrorsQ== dependencies: "@typescript-eslint/utils" "^6.0.0 || ^7.0.0 || ^8.0.0" -eslint-plugin-n@^16.6.2: - version "16.6.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-n/-/eslint-plugin-n-16.6.2.tgz#6a60a1a376870064c906742272074d5d0b412b0b" - integrity sha512-6TyDmZ1HXoFQXnhCTUjVFULReoBPOAjpuiKELMkeP40yffI/1ZRO+d9ug/VC6fqISo2WkuIBk3cvuRPALaWlOQ== +eslint-plugin-jsonc@^2.20.1: + version "2.20.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsonc/-/eslint-plugin-jsonc-2.20.1.tgz#138b41e857a2add02b5408b13f3bc6f14d51d702" + integrity sha512-gUzIwQHXx7ZPypUoadcyRi4WbHW2TPixDr0kqQ4miuJBU0emJmyGTlnaT3Og9X2a8R1CDayN9BFSq5weGWbTng== dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - builtins "^5.0.1" - eslint-plugin-es-x "^7.5.0" - get-tsconfig "^4.7.0" - globals "^13.24.0" - ignore "^5.2.4" - is-builtin-module "^3.2.1" - is-core-module "^2.12.1" - minimatch "^3.1.2" - resolve "^1.22.2" - semver "^7.5.3" + "@eslint-community/eslint-utils" "^4.5.1" + eslint-compat-utils "^0.6.4" + eslint-json-compat-utils "^0.2.1" + espree "^9.6.1 || ^10.3.0" + graphemer "^1.4.0" + jsonc-eslint-parser "^2.4.0" + natural-compare "^1.4.0" + synckit "^0.6.2 || ^0.7.3 || ^0.11.5" -eslint-plugin-prettier@^5.2.6: - version "5.2.6" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.6.tgz#be39e3bb23bb3eeb7e7df0927cdb46e4d7945096" - integrity sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ== +eslint-plugin-n@^17.19.0: + version "17.19.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-n/-/eslint-plugin-n-17.19.0.tgz#3c198306c2eb9ea950c7458cff9c9a6426d02c58" + integrity sha512-qxn1NaDHtizbhVAPpbMT8wWFaLtPnwhfN/e+chdu2i6Vgzmo/tGM62tcJ1Hf7J5Ie4dhse3DOPMmDxduzfifzw== + dependencies: + "@eslint-community/eslint-utils" "^4.5.0" + "@typescript-eslint/utils" "^8.26.1" + enhanced-resolve "^5.17.1" + eslint-plugin-es-x "^7.8.0" + get-tsconfig "^4.8.1" + globals "^15.11.0" + ignore "^5.3.2" + minimatch "^9.0.5" + semver "^7.6.3" + ts-declaration-location "^1.0.6" + +eslint-plugin-no-catch-all@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-no-catch-all/-/eslint-plugin-no-catch-all-1.1.0.tgz#f2e8950cc2b0bdde5faa4ab339d0986c6ae32fb0" + integrity sha512-VkP62jLTmccPrFGN/W6V7a3SEwdtTZm+Su2k4T3uyJirtkm0OMMm97h7qd8pRFAHus/jQg9FpUpLRc7sAylBEQ== + +eslint-plugin-prettier@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.1.tgz#99b55d7dd70047886b2222fdd853665f180b36af" + integrity sha512-9dF+KuU/Ilkq27A8idRP7N2DH8iUR6qXcjF3FR2wETY21PZdBrIjwCau8oboyGj9b7etWmTGEeM8e7oOed6ZWg== dependencies: prettier-linter-helpers "^1.0.0" - synckit "^0.11.0" + synckit "^0.11.7" -eslint-plugin-promise@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz#269a3e2772f62875661220631bd4dafcb4083816" - integrity sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig== +eslint-plugin-promise@^7.2.1: + version "7.2.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-7.2.1.tgz#a0652195700aea40b926dc3c74b38e373377bfb0" + integrity sha512-SWKjd+EuvWkYaS+uN2csvj0KoP43YTu7+phKQ5v+xw6+A0gutVX2yqCeCkC3uLCJFiPfR2dD8Es5L7yUsmvEaA== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" eslint-plugin-security@^3.0.1: version "3.0.1" @@ -4642,16 +5285,16 @@ eslint-scope@^7.2.2: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-visitor-keys@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" - integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== - -eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: +eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== +eslint-visitor-keys@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45" + integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== + eslint@^8.57.1: version "8.57.1" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9" @@ -4706,7 +5349,7 @@ esniff@^2.0.1: event-emitter "^0.3.5" type "^2.7.2" -espree@^9.6.0, espree@^9.6.1: +espree@^9.0.0, espree@^9.6.0, espree@^9.6.1: version "9.6.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== @@ -4715,6 +5358,20 @@ espree@^9.6.0, espree@^9.6.1: acorn-jsx "^5.3.2" eslint-visitor-keys "^3.4.1" +"espree@^9.6.1 || ^10.3.0": + version "10.3.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.3.0.tgz#29267cf5b0cb98735b65e64ba07e0ed49d1eed8a" + integrity sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg== + dependencies: + acorn "^8.14.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^4.2.0" + +esprima@^1.2.0: + version "1.2.5" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-1.2.5.tgz#0993502feaf668138325756f30f9a51feeec11e9" + integrity sha512-S9VbPDU0adFErpDai3qDkjq8+G05ONtKzcyNrPKg/ZKa+tf879nX2KexNU95b31UoTJjRLInNBHHHjFPoCd7lQ== + esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" @@ -4727,6 +5384,13 @@ esquery@^1.4.2: dependencies: estraverse "^5.1.0" +esquery@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== + dependencies: + estraverse "^5.1.0" + esrecurse@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" @@ -4734,6 +5398,11 @@ esrecurse@^4.3.0: dependencies: estraverse "^5.2.0" +estraverse@^1.5.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44" + integrity sha512-25w1fMXQrGdoquWnScXZGckOv+Wes+JDnuN/+7ex3SauFRS72r2lFDec0EKPt2YD1wUJ/IrfEex+9yp4hfSOJA== + estraverse@^4.1.1: version "4.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" @@ -4749,7 +5418,7 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -etag@~1.8.1: +etag@^1.8.1, etag@~1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= @@ -4767,10 +5436,23 @@ eventemitter3@^3.1.0: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== -events@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" - integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= +events@3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +execa@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50" + integrity sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw== + 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" execa@^0.8.0: version "0.8.0" @@ -4821,7 +5503,7 @@ exponential-backoff@^3.1.1: resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.2.tgz#a8f26adb96bf78e8cd8ad1037928d5e5c0679d91" integrity sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA== -express@^4.0.0, express@^4.17.1, express@^4.21.2: +express@^4.0.0, express@^4.17.1: version "4.21.2" resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== @@ -4858,6 +5540,39 @@ express@^4.0.0, express@^4.17.1, express@^4.21.2: utils-merge "1.0.1" vary "~1.1.2" +express@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/express/-/express-5.1.0.tgz#d31beaf715a0016f0d53f47d3b4d7acf28c75cc9" + integrity sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA== + dependencies: + accepts "^2.0.0" + body-parser "^2.2.0" + content-disposition "^1.0.0" + content-type "^1.0.5" + cookie "^0.7.1" + cookie-signature "^1.2.1" + debug "^4.4.0" + encodeurl "^2.0.0" + escape-html "^1.0.3" + etag "^1.8.1" + finalhandler "^2.1.0" + fresh "^2.0.0" + http-errors "^2.0.0" + merge-descriptors "^2.0.0" + mime-types "^3.0.0" + on-finished "^2.4.1" + once "^1.4.0" + parseurl "^1.3.3" + proxy-addr "^2.0.7" + qs "^6.14.0" + range-parser "^1.2.1" + router "^2.2.0" + send "^1.1.0" + serve-static "^2.2.0" + statuses "^2.0.1" + type-is "^2.0.1" + vary "^1.1.2" + ext@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" @@ -4865,6 +5580,11 @@ ext@^1.7.0: dependencies: type "^2.7.2" +extend-object@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/extend-object/-/extend-object-1.0.0.tgz#42514f84015d1356caf5187969dfb2bc1bda0823" + integrity sha512-0dHDIXC7y7LDmCh/lp1oYkmv73K25AMugQI07r8eFopkW6f7Ufn1q+ETMsJjnV9Am14SlElkqy3O92r6xEaxPw== + extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" @@ -4921,6 +5641,18 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-printf@^1.6.9: + version "1.6.10" + resolved "https://registry.yarnpkg.com/fast-printf/-/fast-printf-1.6.10.tgz#c44ad871726152159d7a903a5af0d65cf3d75875" + integrity sha512-GwTgG9O4FVIdShhbVF3JxOgSBY2+ePGsu2V/UONgoCPzF9VY6ZdBMKsHKCYQHZwNk3qNouUolRDsgVxcVA5G1w== + +fast-xml-parser@4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz#86dbf3f18edf8739326447bcaac31b4ae7f6514f" + integrity sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw== + dependencies: + strnum "^1.0.5" + fastq@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.6.0.tgz#4ec8a38f4ac25f21492673adb7eae9cfef47d1c2" @@ -4935,10 +5667,10 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" -fdir@^6.4.3: - version "6.4.3" - resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.3.tgz#011cdacf837eca9b811c89dbb902df714273db72" - integrity sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw== +fdir@^6.4.4: + version "6.4.4" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.4.tgz#1cfcf86f875a883e19a8fab53622cfe992e8d2f9" + integrity sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg== file-entry-cache@^6.0.1: version "6.0.1" @@ -4979,21 +5711,17 @@ finalhandler@1.3.1: statuses "2.0.1" unpipe "~1.0.0" -find-cache-dir@^2.0.0: +finalhandler@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" - integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-2.1.0.tgz#72306373aa89d05a8242ed569ed86a1bff7c561f" + integrity sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q== dependencies: - commondir "^1.0.1" - make-dir "^2.0.0" - pkg-dir "^3.0.0" - -find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" + debug "^4.4.0" + encodeurl "^2.0.0" + escape-html "^1.0.3" + on-finished "^2.4.1" + parseurl "^1.3.3" + statuses "^2.0.1" find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" @@ -5011,6 +5739,18 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" +fixpack@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fixpack/-/fixpack-4.0.0.tgz#28b9fb8ca04f89aab382021cfa826b36dc381dfd" + integrity sha512-5SM1+H2CcuJ3gGEwTiVo/+nd/hYpNj9Ch3iMDOQ58ndY+VGQ2QdvaUTkd3otjZvYnd/8LF/HkJ5cx7PBq0orCQ== + 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" + flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -5094,6 +5834,11 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= +fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-2.0.0.tgz#8dd7df6a1b3a1b3a5cf186c05a5dd267622635a4" + integrity sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A== + fs-capacitor@^6.1.0, fs-capacitor@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-6.2.0.tgz#fa79ac6576629163cb84561995602d8999afb7f5" @@ -5113,11 +5858,6 @@ fs-minipass@^3.0.0: dependencies: minipass "^7.0.3" -fs-readdir-recursive@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27" - integrity sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA== - fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -5199,7 +5939,7 @@ get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: has-symbols "^1.0.3" hasown "^2.0.0" -get-intrinsic@^1.2.6: +get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== @@ -5220,6 +5960,18 @@ get-package-type@^0.1.0: resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== +get-paths@^0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/get-paths/-/get-paths-0.0.7.tgz#15331086752077cf130166ccd233a1cdbeefcf38" + integrity sha512-0wdJt7C1XKQxuCgouqd+ZvLJ56FQixKoki9MrFaO4EriqzXOiH9gbukaDE1ou08S8Ns3/yDzoBAISNPqj6e6tA== + dependencies: + pify "^4.0.1" + +get-port@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" + integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== + get-proto@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" @@ -5262,10 +6014,10 @@ get-symbol-description@^1.0.2: es-errors "^1.3.0" get-intrinsic "^1.2.4" -get-tsconfig@^4.10.0, get-tsconfig@^4.7.0: - version "4.10.0" - resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.10.0.tgz#403a682b373a823612475a4c2928c7326fc0f6bb" - integrity sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A== +get-tsconfig@^4.10.0, get-tsconfig@^4.10.1, get-tsconfig@^4.8.1: + version "4.10.1" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.10.1.tgz#d34c1c01f47d65a606c37aa7a177bc3e56ab4b2e" + integrity sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ== dependencies: resolve-pkg-maps "^1.0.0" @@ -5307,7 +6059,7 @@ glob@^10.2.2, glob@^10.3.10: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" -glob@^7.1.3, glob@^7.1.4, glob@^7.2.0: +glob@^7.1.3, glob@^7.1.4: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -5324,13 +6076,18 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^13.19.0, globals@^13.24.0: +globals@^13.19.0: version "13.24.0" resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== dependencies: type-fest "^0.20.2" +globals@^15.11.0: + version "15.15.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-15.15.0.tgz#7c4761299d41c32b075715a4ce1ede7897ff72a8" + integrity sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg== + globalthis@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" @@ -5350,7 +6107,7 @@ globby@11.0.0: merge2 "^1.3.0" slash "^3.0.0" -globby@^11.1.0: +globby@^11.0.4, globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== @@ -5391,6 +6148,11 @@ got@~11.8.0: p-cancelable "^2.0.0" responselike "^2.0.0" +graceful-fs@^4.2.4: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" @@ -5454,6 +6216,13 @@ graphql-subscriptions@^1.0.0: dependencies: iterall "^1.2.1" +graphql-subscriptions@^1.1.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-1.2.1.tgz#2142b2d729661ddf967b7388f7cf1dd4cf2e061d" + integrity sha512-95yD/tKi24q8xYa7Q9rhQN16AYj5wPbrb8tmHGM3WRc9EBmWrG/0kkMl+tQG8wcEuE9ibR4zyOM31p5Sdr2v4g== + dependencies: + iterall "^1.3.0" + graphql-tag@^2.9.2, graphql-tag@~2.10.3: version "2.10.3" resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.3.tgz#ea1baba5eb8fc6339e4c4cf049dabe522b0edf03" @@ -5495,6 +6264,16 @@ graphql-upload@^11.0.0, graphql-upload@^8.0.2: isobject "^4.0.0" object-path "^0.11.4" +graphql-upload@^13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/graphql-upload/-/graphql-upload-13.0.0.tgz#1a255b64d3cbf3c9f9171fa62a8fb0b9b59bb1d9" + integrity sha512-YKhx8m/uOtKu4Y1UzBFJhbBGJTlk7k4CydlUUiNrtxnwZv0WigbRHP+DVhRNKt7u7DXOtcKZeYJlGtnMXvreXA== + dependencies: + busboy "^0.3.1" + fs-capacitor "^6.2.0" + http-errors "^1.8.1" + object-path "^0.11.8" + graphql@^14.2.1, graphql@^14.5.3, graphql@^14.6.0: version "14.6.0" resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.6.0.tgz#57822297111e874ea12f5cd4419616930cd83e49" @@ -5595,7 +6374,7 @@ has-values@~2.0.1: dependencies: kind-of "^6.0.2" -has@^1.0.1, has@^1.0.3: +has@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== @@ -5614,7 +6393,7 @@ he@0.5.0: resolved "https://registry.yarnpkg.com/he/-/he-0.5.0.tgz#2c05ffaef90b68e860f3fd2b54ef580989277ee2" integrity sha1-LAX/rvkLaOhg8/0rVO9YCYknfuI= -he@^1.2.0: +he@1.2.0, he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== @@ -5624,13 +6403,6 @@ helmet@~8.1.0: resolved "https://registry.yarnpkg.com/helmet/-/helmet-8.1.0.tgz#f96d23fedc89e9476ecb5198181009c804b8b38c" integrity sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg== -homedir-polyfill@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" - integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== - dependencies: - parse-passwd "^1.0.0" - html-encoding-sniffer@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz#696df529a7cfd82446369dc5193e590a3735b448" @@ -5653,6 +6425,27 @@ html-to-text@7.1.1: htmlparser2 "^6.1.0" minimist "^1.2.5" +html-to-text@9.0.5, html-to-text@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/html-to-text/-/html-to-text-9.0.5.tgz#6149a0f618ae7a0db8085dca9bbf96d32bb8368d" + integrity sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg== + dependencies: + "@selderee/plugin-htmlparser2" "^0.11.0" + deepmerge "^4.3.1" + dom-serializer "^2.0.0" + htmlparser2 "^8.0.2" + selderee "^0.11.0" + +htmlparser2@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-5.0.1.tgz#7daa6fc3e35d6107ac95a4fc08781f091664f6e7" + integrity sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ== + dependencies: + domelementtype "^2.0.1" + domhandler "^3.3.0" + domutils "^2.4.2" + entities "^2.0.0" + htmlparser2@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" @@ -5663,7 +6456,7 @@ htmlparser2@^6.1.0: domutils "^2.5.2" entities "^2.0.0" -htmlparser2@^8.0.0: +htmlparser2@^8.0.0, htmlparser2@^8.0.1, htmlparser2@^8.0.2: version "8.0.2" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== @@ -5693,7 +6486,7 @@ http-cache-semantics@^4.1.1: resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== -http-errors@2.0.0: +http-errors@2.0.0, http-errors@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== @@ -5704,16 +6497,16 @@ http-errors@2.0.0: statuses "2.0.1" toidentifier "1.0.1" -http-errors@^1.7.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" - integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== +http-errors@^1.7.3, http-errors@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c" + integrity sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g== dependencies: depd "~1.1.2" inherits "2.0.4" - setprototypeof "1.1.1" + setprototypeof "1.2.0" statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" + toidentifier "1.0.1" http-proxy-agent@^7.0.0, http-proxy-agent@^7.0.2: version "7.0.2" @@ -5761,6 +6554,25 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +i18n-locales@^0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/i18n-locales/-/i18n-locales-0.0.5.tgz#8f587e598ab982511d7c7db910cb45b8d93cd96a" + integrity sha512-Kve1AHy6rqyfJHPy8MIvaKBKhHhHPXV+a/TgMkjp3UBhO3gfWR40ZQn8Xy7LI6g3FhmbvkFtv+GCZy6yvuyeHQ== + dependencies: + "@ladjs/country-language" "^0.2.1" + +i18n@^0.15.0: + version "0.15.1" + resolved "https://registry.yarnpkg.com/i18n/-/i18n-0.15.1.tgz#68fb8993c461cc440bc2485d82f72019f2b92de8" + integrity sha512-yue187t8MqUPMHdKjiZGrX+L+xcUsDClGO0Cz4loaKUOK9WrGw5pgan4bv130utOwX7fHE9w2iUeHFalVQWkXA== + dependencies: + "@messageformat/core" "^3.0.0" + debug "^4.3.3" + fast-printf "^1.6.9" + make-plural "^7.0.0" + math-interval-parser "^2.0.1" + mustache "^4.2.0" + iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -5775,11 +6587,6 @@ iconv-lite@0.6.3, iconv-lite@^0.6.2, iconv-lite@^0.6.3: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" -ieee754@1.1.13: - version "1.1.13" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" - integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== - ieee754@^1.1.4, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -5790,10 +6597,10 @@ ignore-by-default@^1.0.1: resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= -ignore@^5.1.4, ignore@^5.2.0, ignore@^5.2.4: - version "5.3.0" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" - integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg== +ignore@^5.1.4, ignore@^5.2.0, ignore@^5.2.4, ignore@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== image-extensions@~1.1.0: version "1.1.0" @@ -5839,7 +6646,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -5849,6 +6656,11 @@ inherits@2.0.1: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= +ini@~1.3.0: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + insane@2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/insane/-/insane-2.6.1.tgz#c7dcae7b51c20346883b71078fad6ce0483c198f" @@ -5880,25 +6692,10 @@ internal-slot@^1.0.7: hasown "^2.0.0" side-channel "^1.0.4" -ioredis@^4.16.1: - version "4.16.1" - resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.16.1.tgz#377c21d2a4fa8cc31fe9028c666f8dd16a6255bf" - integrity sha512-g76Mm9dE7BLuewncu1MimGZw5gDDjDwjoRony/VoSxSJEKAhuYncDEwYKYjtHi2NWsTNIB6XXRjE64uVa/wpKQ== - dependencies: - cluster-key-slot "^1.1.0" - debug "^4.1.1" - denque "^1.1.0" - lodash.defaults "^4.2.0" - lodash.flatten "^4.4.0" - redis-commands "1.5.0" - redis-errors "^1.2.0" - redis-parser "^3.0.0" - standard-as-callback "^2.0.1" - -ioredis@^5.3.2: - version "5.4.2" - resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.4.2.tgz#ebb6f1a10b825b2c0fb114763d7e82114a0bee6c" - integrity sha512-0SZXGNGZ+WzISQ67QDyZ2x0+wVxjjUndtD8oSeik/4ajifeiRufed8fCb8QW8VMyi4MXcS+UO1k/0NGhvq1PAg== +ioredis@^5.3.2, ioredis@^5.6.1: + version "5.6.1" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.6.1.tgz#1ed7dc9131081e77342503425afceaf7357ae599" + integrity sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA== dependencies: "@ioredis/commands" "^1.1.1" cluster-key-slot "^1.1.0" @@ -5943,14 +6740,6 @@ is-absolute-url@^3.0.0: resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.1.tgz#e315cbdcbbc3d6789532d591954ac78a0e5049f6" integrity sha512-c2QjUwuMxLsld90sj3xYzpFYWJtuxkIn1f5ua9RTEYJt/vV2IsM+Py00/6qjV7qExgifUvt7qfyBGBBKm+2iBg== -is-arguments@^1.0.4: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" - integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - is-array-buffer@^3.0.1, is-array-buffer@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.2.tgz#f2653ced8412081638ecb0ebbd0c41c6e0aecbbe" @@ -5995,13 +6784,6 @@ is-boolean-object@^1.1.0: call-bind "^1.0.2" has-tostringtag "^1.0.0" -is-builtin-module@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169" - integrity sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A== - dependencies: - builtin-modules "^3.3.0" - is-bun-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-bun-module/-/is-bun-module-2.0.0.tgz#4d7859a87c0fcac950c95e666730e745eae8bddd" @@ -6019,13 +6801,20 @@ is-callable@^1.1.4, is-callable@^1.2.0: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw== -is-core-module@^2.12.1, is-core-module@^2.13.0, is-core-module@^2.15.1: +is-core-module@^2.13.0, is-core-module@^2.15.1: version "2.15.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== dependencies: hasown "^2.0.2" +is-core-module@^2.16.0: + version "2.16.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== + dependencies: + hasown "^2.0.2" + is-data-view@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" @@ -6038,6 +6827,19 @@ is-date-object@^1.0.1: resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== +is-docker@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-expression@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-expression/-/is-expression-4.0.0.tgz#c33155962abf21d0afd2552514d67d2ec16fd2ab" + integrity sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A== + dependencies: + acorn "^7.1.1" + object-assign "^4.1.1" + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -6053,13 +6855,6 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== -is-generator-function@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" - integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== - dependencies: - has-tostringtag "^1.0.0" - is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" @@ -6099,13 +6894,6 @@ is-path-inside@^3.0.3: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== -is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - is-plain-object@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" @@ -6116,17 +6904,25 @@ is-potential-custom-element-name@^1.0.1: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== -is-promise@^2.2.2: +is-promise@^2.0.0, is-promise@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== -is-regex@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" - integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE= +is-promise@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3" + integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ== + +is-regex@^1.0.3: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" + integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g== dependencies: - has "^1.0.1" + call-bound "^1.0.2" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + hasown "^2.0.2" is-regex@^1.1.0: version "1.1.0" @@ -6213,7 +7009,7 @@ is-typed-array@^1.1.10, is-typed-array@^1.1.9: gopd "^1.0.1" has-tostringtag "^1.0.0" -is-typed-array@^1.1.12, is-typed-array@^1.1.3: +is-typed-array@^1.1.12: version "1.1.12" resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.12.tgz#d0bab5686ef4a76f7a73097b95470ab199c57d4a" integrity sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg== @@ -6255,10 +7051,12 @@ is-weakref@^1.0.2: dependencies: call-bind "^1.0.2" -isarray@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= +is-wsl@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" isarray@^2.0.5: version "2.0.5" @@ -6285,11 +7083,6 @@ iso-639-3@~2.2.0: resolved "https://registry.yarnpkg.com/iso-639-3/-/iso-639-3-2.2.0.tgz#eb01d7734d61396efec934979e8b0806550837f1" integrity sha512-v9w/U4XDSfXCrXxf4E6ertGC/lTRX8MLLv7XC1j6N5oL3ympe38jp77zgeyMsn3MbufuAAoGeVzDJbOXnPTMhQ== -isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== - isobject@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0" @@ -6740,10 +7533,10 @@ jest@^29.7.0: import-local "^3.0.2" jest-cli "^29.7.0" -jmespath@0.16.0: - version "0.16.0" - resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.16.0.tgz#b15b0a85dfd4d930d43e69ed605943c802785076" - integrity sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw== +js-stringify@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/js-stringify/-/js-stringify-1.0.2.tgz#1736fddfd9724f28a3682adc6230ae7e4e9679db" + integrity sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g== js-tokens@^4.0.0: version "4.0.0" @@ -6802,16 +7595,11 @@ jsdom@~26.0.0: ws "^8.18.0" xml-name-validator "^5.0.0" -jsesc@^3.0.2, jsesc@~3.0.2: +jsesc@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" - integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= - json-buffer@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" @@ -6849,11 +7637,21 @@ json5@^1.0.2: dependencies: minimist "^1.2.0" -json5@^2.2.3: +json5@^2.2.2, json5@^2.2.3, json5@^2.x: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +jsonc-eslint-parser@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/jsonc-eslint-parser/-/jsonc-eslint-parser-2.4.0.tgz#74ded53f9d716e8d0671bd167bf5391f452d5461" + integrity sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg== + dependencies: + acorn "^8.5.0" + eslint-visitor-keys "^3.0.0" + espree "^9.0.0" + semver "^7.3.5" + jsonwebtoken@^8.3.0, jsonwebtoken@~8.5.1: version "8.5.1" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" @@ -6880,6 +7678,36 @@ jsprim@^1.2.2: json-schema "0.4.0" verror "1.10.0" +jstransformer@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/jstransformer/-/jstransformer-1.0.0.tgz#ed8bf0921e2f3f1ed4d5c1a44f68709ed24722c3" + integrity sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A== + dependencies: + is-promise "^2.0.0" + promise "^7.0.1" + +juice@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/juice/-/juice-10.0.1.tgz#a1492091ef739e4771b9f60aad1a608b5a8ea3ba" + integrity sha512-ZhJT1soxJCkOiO55/mz8yeBKTAJhRzX9WBO+16ZTqNTONnnVlUPyVBIzQ7lDRjaBdTbid+bAnyIon/GM3yp4cA== + 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" + +juice@^8.0.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/juice/-/juice-8.1.0.tgz#4ea23362522fe06418229943237ee3751a4fca70" + integrity sha512-FLzurJrx5Iv1e7CfBSZH68dC04EEvXvvVvPYB7Vx1WAuhCp1ZPIMtqxc+WTWxVkpTIC2Ach/GAv0rQbtGf6YMA== + dependencies: + cheerio "1.0.0-rc.10" + commander "^6.1.0" + mensch "^0.3.4" + slick "^1.12.2" + web-resource-inliner "^6.0.1" + jwa@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" @@ -6919,6 +7747,11 @@ languagedetect@^2.0.0: resolved "https://registry.yarnpkg.com/languagedetect/-/languagedetect-2.0.0.tgz#4b8fa2b7593b2a3a02fb1100891041c53238936c" integrity sha512-AZb/liiQ+6ZoTj4f1J0aE6OkzhCo8fyH+tuSaPfSo8YHCWLFJrdSixhtO2TYdIkjcDQNaR4RmGaV2A5FJklDMQ== +leac@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/leac/-/leac-0.6.0.tgz#dcf136e382e666bd2475f44a1096061b70dc0912" + integrity sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg== + leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -6932,29 +7765,48 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +libbase64@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/libbase64/-/libbase64-1.3.0.tgz#053314755a05d2e5f08bbfc48d0290e9322f4406" + integrity sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg== + +libmime@5.3.6: + version "5.3.6" + resolved "https://registry.yarnpkg.com/libmime/-/libmime-5.3.6.tgz#e6dfc655b6b4614bad90e8e65817957903b56580" + integrity sha512-j9mBC7eiqi6fgBPAGvKCXJKJSIASanYF4EeA4iBzSG0HxQxmXnR3KbyWqTn4CwsKSebqCv2f5XZfAO6sKzgvwA== + dependencies: + encoding-japanese "2.2.0" + iconv-lite "0.6.3" + libbase64 "1.3.0" + libqp "2.1.1" + +libqp@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/libqp/-/libqp-2.1.1.tgz#f1be767a58f966f500597997cab72cfc1e17abfa" + integrity sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow== + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== -linkify-html@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/linkify-html/-/linkify-html-4.2.0.tgz#06f78780827d90433424e412976d656912b13fb8" - integrity sha512-bVXuLiWmGwvlH95hq6q9DFGqTsQeFSGw/nHmvvjGMZv9T3GqkxuW2d2SOgk/a4DV2ajeS4c37EqlF16cjOj7GA== +linkify-html@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/linkify-html/-/linkify-html-4.3.1.tgz#6226a2205d96eb6a3b0c59571a2b02936c6386f3" + integrity sha512-6ZNyucw7fH9Ncu17s+hvHFB2sU6fLWowqH6MqkXxtVL2kKkhnrho/DMCE3fWovmzVPgWSFGvg6zLkW+VWrVr4w== + +linkify-it@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421" + integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ== + dependencies: + uc.micro "^2.0.0" linkifyjs@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.2.0.tgz#9dd30222b9cbabec9c950e725ec00031c7fa3f08" integrity sha512-pCj3PrQyATaoTYKHrgWRF3SJwsm61udVh+vuls/Rl6SptiDhgE7ziUIudAedRY9QEfynmM7/RmLEfPUyw1HPCw== -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== - dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" - locate-path@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" @@ -6974,21 +7826,11 @@ lodash-es@^4.17.11: resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78" integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ== -lodash.debounce@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" - integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== - lodash.defaults@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= -lodash.flatten@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" - integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= - lodash.includes@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" @@ -7049,7 +7891,7 @@ lodash@4.17.15: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== -lodash@^4.17.15, lodash@~4.17.11, lodash@~4.17.21: +lodash@^4.17.15, lodash@^4.17.21, lodash@~4.17.11, lodash@~4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -7108,13 +7950,30 @@ lru_map@^0.3.3: resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" integrity sha1-tcg1G5Rky9dQM1p5ZQoOwOVhGN0= -make-dir@^2.0.0, make-dir@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" - integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== +mailparser@^3.7.1: + version "3.7.2" + resolved "https://registry.yarnpkg.com/mailparser/-/mailparser-3.7.2.tgz#00feec656e23c0ae805163581b460c2f72ca75d1" + integrity sha512-iI0p2TCcIodR1qGiRoDBBwboSSff50vQAWytM5JRggLfABa4hHYCf3YVujtuzV454xrOP352VsAPIzviqMTo4Q== dependencies: - pify "^4.0.1" - semver "^5.6.0" + encoding-japanese "2.2.0" + he "1.2.0" + html-to-text "9.0.5" + iconv-lite "0.6.3" + libmime "5.3.6" + linkify-it "5.0.0" + mailsplit "5.4.2" + nodemailer "6.9.16" + punycode.js "2.3.1" + tlds "1.255.0" + +mailsplit@5.4.2: + version "5.4.2" + resolved "https://registry.yarnpkg.com/mailsplit/-/mailsplit-5.4.2.tgz#ee2be344bb3511345c0bd6ea72e5657acb8cd83b" + integrity sha512-4cczG/3Iu3pyl8JgQ76dKkisurZTmxMrA4dj/e8d2jKYcFTZ7MxOzg1gTioTDMPuFXwTrVuN/gxhkrO7wLg7qA== + dependencies: + libbase64 "1.3.0" + libmime "5.3.6" + libqp "2.1.1" make-dir@^3.0.0: version "3.1.0" @@ -7146,6 +8005,11 @@ make-fetch-happen@^13.0.0: promise-retry "^2.0.1" ssri "^10.0.0" +make-plural@^7.0.0: + version "7.4.0" + resolved "https://registry.yarnpkg.com/make-plural/-/make-plural-7.4.0.tgz#fa6990dd550dea4de6b20163f74e5ed83d8a8d6d" + integrity sha512-4/gC9KVNTV6pvYg2gFeQYTW3mWaoJt7WZE5vrp1KnQDgW92JtYZnzmZT81oj/dUTqAIu0ufI2x3dkgu3bB1tYg== + makeerror@1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" @@ -7153,6 +8017,11 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" +math-interval-parser@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/math-interval-parser/-/math-interval-parser-2.0.1.tgz#e22cd6d15a0a7f4c03aec560db76513da615bed4" + integrity sha512-VmlAmb0UJwlvMyx8iPhXUDnVW1F9IrGEd9CIOmv+XL8AErCUUuozoDMrgImvnYt2A+53qVX/tPW6YJurMKYsvA== + math-intrinsics@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" @@ -7163,6 +8032,11 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= +media-typer@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-1.1.0.tgz#6ab74b8f2d3320f2064b2a87a38e7931ff3a5561" + integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw== + memoize-one@~6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" @@ -7182,11 +8056,21 @@ memoizee@0.4: next-tick "^1.1.0" timers-ext "^0.1.7" +mensch@^0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/mensch/-/mensch-0.3.4.tgz#770f91b46cb16ea5b204ee735768c3f0c491fecd" + integrity sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g== + merge-descriptors@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== +merge-descriptors@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz#ea922f660635a2249ee565e0449f951e6b603808" + integrity sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g== + merge-graphql-schemas@^1.7.8: version "1.7.8" resolved "https://registry.yarnpkg.com/merge-graphql-schemas/-/merge-graphql-schemas-1.7.8.tgz#11a0a672a38a61d988c09ffdebe1bd4f8418de48" @@ -7353,29 +8237,29 @@ migrate@^2.1.0: mkdirp "^3.0.1" slug "^8.2.2" -mime-db@1.43.0: - version "1.43.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" - integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== - mime-db@1.52.0: version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.12, mime-types@^2.1.35, mime-types@~2.1.19, mime-types@~2.1.34: +mime-db@^1.54.0: + version "1.54.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" + integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== + +mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.22, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: mime-db "1.52.0" -mime-types@~2.1.22, mime-types@~2.1.24: - version "2.1.26" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" - integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ== +mime-types@^3.0.0, mime-types@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.1.tgz#b1d94d6997a9b32fd69ebaed0db73de8acb519ce" + integrity sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA== dependencies: - mime-db "1.43.0" + mime-db "^1.54.0" mime@1.6.0: version "1.6.0" @@ -7387,6 +8271,11 @@ mime@3: resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== +mime@^2.4.6: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -7416,10 +8305,10 @@ minimatch@^5.0.1: dependencies: brace-expansion "^2.0.1" -minimatch@^9.0.1, minimatch@^9.0.4: - version "9.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" - integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== +minimatch@^9.0.1, minimatch@^9.0.4, minimatch@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== dependencies: brace-expansion "^2.0.1" @@ -7514,6 +8403,11 @@ moment@2.21.0: resolved "https://registry.yarnpkg.com/moment/-/moment-2.21.0.tgz#2a114b51d2a6ec9e6d83cf803f838a878d8a023a" integrity sha512-TCZ36BjURTeFTM/CwRcViQlfkMvL1/vFISuNLO5GkcVm1+QHfbSiNqZuWeMFjj1/3+uAjXswgRk30j1kkLYJBQ== +moo@^0.5.1: + version "0.5.2" + resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.2.tgz#f9fe82473bc7c184b0d32e2215d3f6e67278733c" + integrity sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -7524,26 +8418,47 @@ ms@2.1.3, ms@^2.1.1, ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +multimatch@5: + version "5.0.0" + resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-5.0.0.tgz#932b800963cea7a31a033328fa1e0c3a1874dbe6" + integrity sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA== + dependencies: + "@types/minimatch" "^3.0.3" + array-differ "^3.0.0" + array-union "^2.1.0" + arrify "^2.0.1" + minimatch "^3.0.4" + mustache@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64" integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ== +mylas@^2.1.9: + version "2.1.13" + resolved "https://registry.yarnpkg.com/mylas/-/mylas-2.1.13.tgz#1e23b37d58fdcc76e15d8a5ed23f9ae9fc0cbdf4" + integrity sha512-+MrqnJRtxdF+xngFfUUkIMQrUUL0KsxbADUkn23Z/4ibGg192Q+z+CQyiYwvWTsYjJygmMR8+w3ZDa98Zh6ESg== + n-gram@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/n-gram/-/n-gram-1.1.1.tgz#a374dc176a9063a2388d1be18ed7c35828be2a97" integrity sha512-qibRqvUghLIVsq+RTwVuwOzgOxf0l4DDZKVYAK0bMam5sG9ZzaJ6BUSJyG2Td8kTc7c/HcMUtjiN5ShobZA2bA== -nan@2.17.0, nan@^2.20.0: - version "2.17.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" - integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== +nan@^2.20.0: + version "2.22.2" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.22.2.tgz#6b504fd029fb8f38c0990e52ad5c26772fdacfbb" + integrity sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ== nanoid@^3.3.6: version "3.3.7" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== +napi-postinstall@^0.2.2: + version "0.2.4" + resolved "https://registry.yarnpkg.com/napi-postinstall/-/napi-postinstall-0.2.4.tgz#419697d0288cb524623e422f919624f22a5e4028" + integrity sha512-ZEzHJwBhZ8qQSbknHqYcdtQVr8zUgGyM/q6h6qAyhtyVMNrSgDhrC4disf03dYW0e+czXyLnZINnCTEkWy0eJg== + natural-compare-lite@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" @@ -7559,6 +8474,11 @@ negotiator@0.6.3, negotiator@^0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== +negotiator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a" + integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== + neo4j-driver-bolt-connection@4.4.11: version "4.4.11" resolved "https://registry.yarnpkg.com/neo4j-driver-bolt-connection/-/neo4j-driver-bolt-connection-4.4.11.tgz#aeaee9faa620e6309698b4cedf5b354d8898ea05" @@ -7635,6 +8555,11 @@ next-tick@^1.1.0: resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + no-case@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.3.tgz#c21b434c1ffe48b39087e86cfb4d2582e9df18f8" @@ -7643,14 +8568,6 @@ no-case@^3.0.3: lower-case "^2.0.1" tslib "^1.10.0" -node-environment-flags@^1.0.5: - version "1.0.6" - resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.6.tgz#a30ac13621f6f7d674260a54dede048c3982c088" - integrity sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw== - dependencies: - object.getownpropertydescriptors "^2.0.3" - semver "^5.7.0" - node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@^2.6.0, node-fetch@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" @@ -7684,11 +8601,6 @@ node-releases@^2.0.18: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== -node-releases@^2.0.19: - version "2.0.19" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" - integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== - nodemailer-html-to-text@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/nodemailer-html-to-text/-/nodemailer-html-to-text-3.2.0.tgz#91b959491fef8f7d91796047abb728aa86d4a12b" @@ -7696,15 +8608,20 @@ nodemailer-html-to-text@^3.2.0: dependencies: html-to-text "7.1.1" -nodemailer@^6.10.0: - version "6.10.0" - resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.10.0.tgz#1f24c9de94ad79c6206f66d132776b6503003912" - integrity sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA== +nodemailer@6.9.16: + version "6.9.16" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.16.tgz#3ebdf6c6f477c571c0facb0727b33892635e0b8b" + integrity sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ== -nodemon@~3.1.9: - version "3.1.9" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.9.tgz#df502cdc3b120e1c3c0c6e4152349019efa7387b" - integrity sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg== +nodemailer@^6.10.1, nodemailer@^6.9.13, nodemailer@^6.9.14: + version "6.10.1" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.10.1.tgz#cbc434c54238f83a51c07eabd04e2b3e832da623" + integrity sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA== + +nodemon@~3.1.10: + version "3.1.10" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.10.tgz#5015c5eb4fffcb24d98cf9454df14f4fecec9bc1" + integrity sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw== dependencies: chokidar "^3.5.2" debug "^4" @@ -7799,6 +8716,11 @@ object-inspect@^1.13.1: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== +object-inspect@^1.13.3: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + object-inspect@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" @@ -7814,10 +8736,10 @@ object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object-path@^0.11.4: - version "0.11.5" - resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.5.tgz#d4e3cf19601a5140a55a16ad712019a9c50b577a" - integrity sha512-jgSbThcoR/s+XumvGMTMf81QVBmah+/Q7K7YduKeKVWL7N111unR2d6pZZarSk6kY/caeNxUDyxOvMWyzoU2eg== +object-path@^0.11.4, object-path@^0.11.8: + version "0.11.8" + resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.8.tgz#ed002c02bbdd0070b78a27455e8ae01fc14d4742" + integrity sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA== object.assign@^4.1.0: version "4.1.0" @@ -7859,14 +8781,6 @@ object.fromentries@^2.0.8: es-abstract "^1.23.2" es-object-atoms "^1.0.0" -object.getownpropertydescriptors@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" - integrity sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY= - dependencies: - define-properties "^1.1.2" - es-abstract "^1.5.1" - object.getownpropertydescriptors@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649" @@ -7893,7 +8807,7 @@ object.values@^1.2.0: define-properties "^1.2.1" es-object-atoms "^1.0.0" -on-finished@2.4.1: +on-finished@2.4.1, on-finished@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== @@ -7914,12 +8828,13 @@ onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" -optimism@^0.10.0: - version "0.10.2" - resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.10.2.tgz#626b6fd28b0923de98ecb36a3fd2d3d4e5632dd9" - integrity sha512-zPfBIxFFWMmQboM9+Z4MSJqc1PXp82v1PFq/GfQaufI69mHKlup7ykGNnfuGIGssXJQkmhSodQ/k9EWwjd8O8A== +open@7: + version "7.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" + integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== dependencies: - "@wry/context" "^0.4.0" + is-docker "^2.0.0" + is-wsl "^2.1.1" optionator@^0.9.3: version "0.9.3" @@ -7938,18 +8853,18 @@ p-cancelable@^2.0.0: resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.0.0.tgz#4a3740f5bdaf5ed5d7c3e34882c6fb5d6b266a6e" integrity sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg== +p-event@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/p-event/-/p-event-4.2.0.tgz#af4b049c8acd91ae81083ebd1e6f5cae2044c1b5" + integrity sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ== + dependencies: + p-timeout "^3.1.0" + p-finally@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== -p-limit@^2.0.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.1.tgz#aa07a788cc3151c939b5131f63570f0dd2009537" - integrity sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg== - dependencies: - p-try "^2.0.0" - p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -7964,13 +8879,6 @@ p-limit@^3.0.2, p-limit@^3.1.0: dependencies: yocto-queue "^0.1.0" -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== - dependencies: - p-limit "^2.0.0" - p-locate@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" @@ -7997,11 +8905,25 @@ p-reflect@~2.1.0: resolved "https://registry.yarnpkg.com/p-reflect/-/p-reflect-2.1.0.tgz#5d67c7b3c577c4e780b9451fc9129675bd99fe67" integrity sha512-paHV8NUz8zDHu5lhr/ngGWQiW067DK/+IbJ+RfZ4k+s8y4EKyYCz8pGYWjxCg35eHztpJAt+NUgvN4L+GCbPlg== +p-timeout@^3.0.0, p-timeout@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" + integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== + dependencies: + p-finally "^1.0.0" + p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +p-wait-for@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/p-wait-for/-/p-wait-for-3.2.0.tgz#640429bcabf3b0dd9f492c31539c5718cb6a3f1f" + integrity sha512-wpgERjNkLrBiFmkMEjuZJEWKKDrNfHCKA1OhyN1wg1FrLkULbviEy6py1AyJUgZ72YWFbZ38FIpnqvVqAlDUwA== + dependencies: + p-timeout "^3.0.0" + package-json-from-dist@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" @@ -8029,11 +8951,6 @@ parse-ms@^2.1.0: resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d" integrity sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA== -parse-passwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" - integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= - parse-srcset@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" @@ -8049,6 +8966,13 @@ parse-uri@~1.0.3: resolved "https://registry.yarnpkg.com/parse-uri/-/parse-uri-1.0.7.tgz#287629a09328a97e398468f21b8a00c4a2d9cc73" integrity sha512-eWuZCMKNlVkXrEoANdXxbmqhu2SQO9jUMCSpdbJDObin0JxISn6e400EWsSRbr/czdKvWKkhZnMKEGUwf/Plmg== +parse5-htmlparser2-tree-adapter@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" + integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA== + dependencies: + parse5 "^6.0.1" + parse5-htmlparser2-tree-adapter@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1" @@ -8064,6 +8988,11 @@ parse5-parser-stream@^7.1.2: dependencies: parse5 "^7.0.0" +parse5@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" + integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== + parse5@^7.0.0, parse5@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" @@ -8078,7 +9007,15 @@ parse5@^7.2.1: dependencies: entities "^4.5.0" -parseurl@^1.3.2, parseurl@~1.3.3: +parseley@^0.12.0: + version "0.12.1" + resolved "https://registry.yarnpkg.com/parseley/-/parseley-0.12.1.tgz#4afd561d50215ebe259e3e7a853e62f600683aef" + integrity sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw== + dependencies: + leac "^0.6.0" + peberminta "^0.9.0" + +parseurl@^1.3.2, parseurl@^1.3.3, parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== @@ -8091,11 +9028,6 @@ pascal-case@^3.1.1: no-case "^3.0.3" tslib "^1.10.0" -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= - path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" @@ -8106,7 +9038,7 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== -path-key@^2.0.0: +path-key@^2.0.0, path-key@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw== @@ -8134,11 +9066,21 @@ path-to-regexp@0.1.12: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== +path-to-regexp@^8.0.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.2.0.tgz#73990cc29e57a3ff2a0d914095156df5db79e8b4" + integrity sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ== + path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +peberminta@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/peberminta/-/peberminta-0.9.0.tgz#8ec9bc0eb84b7d368126e71ce9033501dca2a352" + integrity sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ== + performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" @@ -8154,11 +9096,6 @@ picocolors@^1.0.1: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59" integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw== -picocolors@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" - integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== - picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" @@ -8179,18 +9116,6 @@ pirates@^4.0.4: resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== -pirates@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" - integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== - -pkg-dir@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" - integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== - dependencies: - find-up "^3.0.0" - pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" @@ -8198,6 +9123,13 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +plimit-lit@^1.2.6: + version "1.6.1" + resolved "https://registry.yarnpkg.com/plimit-lit/-/plimit-lit-1.6.1.tgz#a34594671b31ee8e93c72d505dfb6852eb72374a" + integrity sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA== + dependencies: + queue-lit "^1.5.1" + possible-typed-array-names@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" @@ -8245,6 +9177,23 @@ pretty-ms@~7.0.1: dependencies: parse-ms "^2.1.0" +preview-email@^3.0.17, preview-email@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/preview-email/-/preview-email-3.1.0.tgz#ee8525d878afef4309ae548116e4a4fe8b119a6d" + integrity sha512-ZtV1YrwscEjlrUzYrTSs6Nwo49JM3pXLM4fFOBSC3wSni+bxaWlw9/Qgk75PZO8M7cX2EybmL2iwvaV3vkAttw== + dependencies: + ci-info "^3.8.0" + display-notification "2.0.0" + fixpack "^4.0.0" + get-port "5.1.1" + mailparser "^3.7.1" + nodemailer "^6.9.13" + open "7" + p-event "4.2.0" + p-wait-for "3.2.0" + pug "^3.0.3" + uuid "^9.0.1" + proc-log@^4.1.0, proc-log@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-4.2.0.tgz#b6f461e4026e75fdfe228b265e9f7a00779d7034" @@ -8258,6 +9207,13 @@ promise-retry@^2.0.1: err-code "^2.0.2" retry "^0.12.0" +promise@^7.0.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" + integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== + dependencies: + asap "~2.0.3" + prompts@^2.0.1: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" @@ -8271,7 +9227,7 @@ property-expr@^2.0.0: resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.2.tgz#fff2a43919135553a3bc2fdd94bdb841965b2330" integrity sha512-bc/5ggaYZxNkFKj374aLbEDqVADdYaLcFo8XBkishUWbaAdjlphaBFns9TvRA2pUseVL/wMFmui9X3IdNDU37g== -proxy-addr@~2.0.7: +proxy-addr@^2.0.7, proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== @@ -8294,6 +9250,109 @@ pstree.remy@^1.1.8: resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== +pug-attrs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pug-attrs/-/pug-attrs-3.0.0.tgz#b10451e0348165e31fad1cc23ebddd9dc7347c41" + integrity sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA== + dependencies: + constantinople "^4.0.1" + js-stringify "^1.0.2" + pug-runtime "^3.0.0" + +pug-code-gen@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/pug-code-gen/-/pug-code-gen-3.0.3.tgz#58133178cb423fe1716aece1c1da392a75251520" + integrity sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw== + 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" + +pug-error@^2.0.0, pug-error@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pug-error/-/pug-error-2.1.0.tgz#17ea37b587b6443d4b8f148374ec27b54b406e55" + integrity sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg== + +pug-filters@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/pug-filters/-/pug-filters-4.0.0.tgz#d3e49af5ba8472e9b7a66d980e707ce9d2cc9b5e" + integrity sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A== + dependencies: + constantinople "^4.0.1" + jstransformer "1.0.0" + pug-error "^2.0.0" + pug-walk "^2.0.0" + resolve "^1.15.1" + +pug-lexer@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/pug-lexer/-/pug-lexer-5.0.1.tgz#ae44628c5bef9b190b665683b288ca9024b8b0d5" + integrity sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w== + dependencies: + character-parser "^2.2.0" + is-expression "^4.0.0" + pug-error "^2.0.0" + +pug-linker@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/pug-linker/-/pug-linker-4.0.0.tgz#12cbc0594fc5a3e06b9fc59e6f93c146962a7708" + integrity sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw== + dependencies: + pug-error "^2.0.0" + pug-walk "^2.0.0" + +pug-load@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pug-load/-/pug-load-3.0.0.tgz#9fd9cda52202b08adb11d25681fb9f34bd41b662" + integrity sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ== + dependencies: + object-assign "^4.1.1" + pug-walk "^2.0.0" + +pug-parser@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/pug-parser/-/pug-parser-6.0.0.tgz#a8fdc035863a95b2c1dc5ebf4ecf80b4e76a1260" + integrity sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw== + dependencies: + pug-error "^2.0.0" + token-stream "1.0.0" + +pug-runtime@^3.0.0, pug-runtime@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/pug-runtime/-/pug-runtime-3.0.1.tgz#f636976204723f35a8c5f6fad6acda2a191b83d7" + integrity sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg== + +pug-strip-comments@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz#f94b07fd6b495523330f490a7f554b4ff876303e" + integrity sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ== + dependencies: + pug-error "^2.0.0" + +pug-walk@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pug-walk/-/pug-walk-2.0.0.tgz#417aabc29232bb4499b5b5069a2b2d2a24d5f5fe" + integrity sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ== + +pug@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/pug/-/pug-3.0.3.tgz#e18324a314cd022883b1e0372b8af3a1a99f7597" + integrity sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g== + 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" + pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -8302,16 +9361,16 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" +punycode.js@2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7" + integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA== + punycode2@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/punycode2/-/punycode2-1.0.0.tgz#e2b4b9a9a8ff157d0b84438e203181ee7892dfd8" integrity sha1-4rS5qaj/FX0LhEOOIDGB7niS39g= -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= - punycode@^2.1.0, punycode@^2.1.1: version "2.3.0" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" @@ -8334,22 +9393,29 @@ qs@6.13.0: dependencies: side-channel "^1.0.6" +qs@^6.11.0, qs@^6.14.0: + version "6.14.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930" + integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w== + dependencies: + side-channel "^1.1.0" + qs@~6.5.2: version "6.5.3" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= +queue-lit@^1.5.1: + version "1.5.2" + resolved "https://registry.yarnpkg.com/queue-lit/-/queue-lit-1.5.2.tgz#83c24d4f4764802377b05a6e5c73017caf3f8747" + integrity sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw== quick-lru@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== -range-parser@~1.2.1: +range-parser@^1.2.1, range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== @@ -8364,6 +9430,26 @@ raw-body@2.5.2: iconv-lite "0.4.24" unpipe "1.0.0" +raw-body@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.0.tgz#25b3476f07a51600619dae3fe82ddc28a36e5e0f" + integrity sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.6.3" + unpipe "1.0.0" + +rc@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + re2@~1.21.4: version "1.21.4" resolved "https://registry.yarnpkg.com/re2/-/re2-1.21.4.tgz#d688edcc40da3cf542ee3a480a8b60e5900dd24d" @@ -8386,6 +9472,15 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== +readable-stream@^3.5.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -8393,11 +9488,6 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -redis-commands@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.5.0.tgz#80d2e20698fe688f227127ff9e5164a7dd17e785" - integrity sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg== - redis-errors@^1.0.0, redis-errors@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" @@ -8410,30 +9500,6 @@ redis-parser@^3.0.0: dependencies: redis-errors "^1.0.0" -regenerate-unicode-properties@^10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c" - integrity sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ== - dependencies: - regenerate "^1.4.2" - -regenerate-unicode-properties@^10.2.0: - version "10.2.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz#626e39df8c372338ea9b8028d1f99dc3fd9c3db0" - integrity sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA== - dependencies: - regenerate "^1.4.2" - -regenerate@^1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" - integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== - -regenerator-runtime@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" - integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== - regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.4: version "0.13.4" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.4.tgz#e96bf612a3362d12bb69f7e8f74ffeab25c7ac91" @@ -8444,13 +9510,6 @@ regenerator-runtime@^0.14.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== -regenerator-transform@^0.15.2: - version "0.15.2" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.2.tgz#5bbae58b522098ebdf09bca2f83838929001c7a4" - integrity sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg== - dependencies: - "@babel/runtime" "^7.8.4" - regexp-tree@~0.1.1: version "0.1.27" resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd" @@ -8475,49 +9534,6 @@ regexp.prototype.flags@^1.5.2: es-errors "^1.3.0" set-function-name "^2.0.1" -regexpu-core@^5.3.1: - version "5.3.2" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" - integrity sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ== - dependencies: - "@babel/regjsgen" "^0.8.0" - regenerate "^1.4.2" - regenerate-unicode-properties "^10.1.0" - regjsparser "^0.9.1" - unicode-match-property-ecmascript "^2.0.0" - unicode-match-property-value-ecmascript "^2.1.0" - -regexpu-core@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.1.1.tgz#b469b245594cb2d088ceebc6369dceb8c00becac" - integrity sha512-k67Nb9jvwJcJmVpw0jPttR1/zVfnKf8Km0IPatrU/zJ5XeG3+Slx0xLXs9HByJSzXzrlz5EDvN6yLNMDc2qdnw== - dependencies: - regenerate "^1.4.2" - regenerate-unicode-properties "^10.2.0" - regjsgen "^0.8.0" - regjsparser "^0.11.0" - unicode-match-property-ecmascript "^2.0.0" - unicode-match-property-value-ecmascript "^2.1.0" - -regjsgen@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.8.0.tgz#df23ff26e0c5b300a6470cad160a9d090c3a37ab" - integrity sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q== - -regjsparser@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.11.1.tgz#ae55c74f646db0c8fcb922d4da635e33da405149" - integrity sha512-1DHODs4B8p/mQHU9kr+jv8+wIC9mtG4eBHxWxIq5mhjE3D5oORhCc6deRKzTjs9DcfRFmj9BHSDguZklqCGFWQ== - dependencies: - jsesc "~3.0.2" - -regjsparser@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" - integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== - dependencies: - jsesc "~0.5.0" - remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" @@ -8554,6 +9570,13 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== +require-json5@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/require-json5/-/require-json5-1.3.0.tgz#b47d236194e458f144c615dd061bdca085628474" + integrity sha512-FkOrdR0kqHFwIqrlifaXNg6fdg2YcUL5lX9bYlaENKLlWp+g0GO/tRMAvoWIM2pYzAGp57oF/jgkVLwxGk7KyQ== + dependencies: + json5 "^2.x" + resolve-alpn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.0.tgz#058bb0888d1cd4d12474e9a4b6eb17bdd5addc44" @@ -8586,7 +9609,16 @@ resolve.exports@^2.0.0: resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== -resolve@^1.12.0, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22.2, resolve@^1.22.4: +resolve@^1.15.1: + version "1.22.10" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39" + integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== + dependencies: + is-core-module "^2.16.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +resolve@^1.20.0, resolve@^1.22.4: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -8624,11 +9656,29 @@ rosie@^2.1.1: resolved "https://registry.yarnpkg.com/rosie/-/rosie-2.1.1.tgz#f8c9b8145d581d19fb1c933cf6ac1c554ad68798" integrity sha512-2AXB7WrIZXtKMZ6Q/PlozqPF5nu/x7NEvRJZOblrJuprrPfm5gL8JVvJPj9aaib9F8IUALnLUFhzXrwEtnI5cQ== +router@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/router/-/router-2.2.0.tgz#019be620b711c87641167cc79b99090f00b146ef" + integrity sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ== + dependencies: + debug "^4.4.0" + depd "^2.0.0" + is-promise "^4.0.0" + parseurl "^1.3.3" + path-to-regexp "^8.0.0" + rrweb-cssom@^0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz#3021d1b4352fbf3b614aaeed0bc0d5739abe0bc2" integrity sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw== +run-applescript@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-3.2.0.tgz#73fb34ce85d3de8076d511ea767c30d4fdfc918b" + integrity sha512-Ep0RsvAjnRcBX1p5vogbaBdAGu/8j/ewpvGqnQYunnLd9SM0vWcPJewPKNnWFggf0hF0pwIgwV5XK7qQ7UZ8Qg== + dependencies: + execa "^0.10.0" + run-parallel@^1.1.9: version "1.1.9" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" @@ -8666,6 +9716,11 @@ safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-identifier@^0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/safe-identifier/-/safe-identifier-0.4.2.tgz#cf6bfca31c2897c588092d1750d30ef501d59fcb" + integrity sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w== + safe-regex-test@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" @@ -8696,10 +9751,10 @@ safe-regex@^2.1.1: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sanitize-html@~2.15.0: - version "2.15.0" - resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.15.0.tgz#8e7f97ee1fecdec1bb1fb2e37f6d2c6acfdbabe3" - integrity sha512-wIjst57vJGpLyBP8ioUbg6ThwJie5SuSIjHxJg53v5Fg+kUK+AXlb7bK3RNXpp315MvwM+0OBGCV6h5pPHsVhA== +sanitize-html@~2.17.0: + version "2.17.0" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.17.0.tgz#a8f66420a6be981d8fe412e3397cc753782598e4" + integrity sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA== dependencies: deepmerge "^4.2.2" escape-string-regexp "^4.0.0" @@ -8708,16 +9763,6 @@ sanitize-html@~2.15.0: parse-srcset "^1.0.2" postcss "^8.3.11" -sax@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" - integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= - -sax@>=0.6.0: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" - integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== - saxes@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5" @@ -8725,20 +9770,27 @@ saxes@^6.0.0: dependencies: xmlchars "^2.2.0" -semver@^5.6.0, semver@^5.7.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== +selderee@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/selderee/-/selderee-0.11.0.tgz#6af0c7983e073ad3e35787ffe20cefd9daf0ec8a" + integrity sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA== + dependencies: + parseley "^0.12.0" + +semver@^5.5.0, semver@^5.6.0: + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.0.0, semver@^7.3.5, semver@^7.3.7, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.7.1: - version "7.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f" - integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA== +semver@^7.3.5, semver@^7.3.7, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.3, semver@^7.7.1, semver@^7.7.2: + version "7.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" + integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== send@0.19.0: version "0.19.0" @@ -8759,6 +9811,23 @@ send@0.19.0: range-parser "~1.2.1" statuses "2.0.1" +send@^1.1.0, send@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/send/-/send-1.2.0.tgz#32a7554fb777b831dfa828370f773a3808d37212" + integrity sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw== + dependencies: + debug "^4.3.5" + encodeurl "^2.0.0" + escape-html "^1.0.3" + etag "^1.8.1" + fresh "^2.0.0" + http-errors "^2.0.0" + mime-types "^3.0.1" + ms "^2.1.3" + on-finished "^2.4.1" + range-parser "^1.2.1" + statuses "^2.0.1" + serve-static@1.16.2: version "1.16.2" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" @@ -8769,6 +9838,16 @@ serve-static@1.16.2: parseurl "~1.3.3" send "0.19.0" +serve-static@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-2.2.0.tgz#9c02564ee259bdd2251b82d659a2e7e1938d66f9" + integrity sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ== + dependencies: + encodeurl "^2.0.0" + escape-html "^1.0.3" + parseurl "^1.3.3" + send "^1.2.0" + set-function-length@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.1.1.tgz#4bc39fafb0307224a33e106a7d35ca1218d659ed" @@ -8810,11 +9889,6 @@ set-function-name@^2.0.1: functions-have-names "^1.2.3" has-property-descriptors "^1.0.2" -setprototypeof@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" - integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== - setprototypeof@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" @@ -8828,13 +9902,6 @@ sha.js@^2.4.11: inherits "^2.0.1" safe-buffer "^5.0.1" -shallow-clone@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" - integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== - dependencies: - kind-of "^6.0.2" - shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -8859,6 +9926,35 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" @@ -8878,6 +9974,17 @@ side-channel@^1.0.6: get-intrinsic "^1.2.4" object-inspect "^1.13.1" +side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + signal-exit@^3.0.0, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" @@ -8900,16 +10007,16 @@ sisteransi@^1.0.5: resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== -slash@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" - integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== - slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +slick@^1.12.2: + version "1.12.2" + resolved "https://registry.yarnpkg.com/slick/-/slick-1.12.2.tgz#bd048ddb74de7d1ca6915faa4a57570b3550c2d7" + integrity sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A== + slug@^8.2.2: version "8.2.2" resolved "https://registry.yarnpkg.com/slug/-/slug-8.2.2.tgz#33b019a857a11fc4773c1e9a9f60e3da651a9e5d" @@ -8965,14 +10072,6 @@ source-map-support@0.5.13: buffer-from "^1.0.0" source-map "^0.6.0" -source-map-support@^0.5.16: - version "0.5.16" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" - integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - source-map@^0.6.0, source-map@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" @@ -9010,10 +10109,10 @@ ssri@^10.0.0: dependencies: minipass "^7.0.3" -stable-hash@^0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/stable-hash/-/stable-hash-0.0.5.tgz#94e8837aaeac5b4d0f631d2972adef2924b40269" - integrity sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA== +stable-hash-x@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/stable-hash-x/-/stable-hash-x-0.1.1.tgz#1dc602f65183ef049a44a9be4fce249a4c71fc84" + integrity sha512-l0x1D6vhnsNUGPFVDx45eif0y6eedVC8nm5uACTrVFJFtl2mLRW17aWtVyxFCpn5t94VUPkjU8vSLwIuwwqtJQ== stack-utils@^2.0.3: version "2.0.6" @@ -9022,17 +10121,12 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" -standard-as-callback@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.0.1.tgz#ed8bb25648e15831759b6023bdb87e6b60b38126" - integrity sha512-NQOxSeB8gOI5WjSaxjBgog2QFw55FV8TkS6Y07BiB3VJ8xNTvUYm0wl0s8ObgQ5NhdpnNfigMIKjgPESzgr4tg== - standard-as-callback@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== -statuses@2.0.1: +statuses@2.0.1, statuses@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== @@ -9042,6 +10136,14 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= +stream-browserify@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" + integrity sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA== + dependencies: + inherits "~2.0.4" + readable-stream "^3.5.0" + streamsearch@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" @@ -9064,23 +10166,14 @@ string-length@^4.0.1: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== +string-width@4.2.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3, string-width@^5.1.2: + version "4.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== dependencies: emoji-regex "^8.0.0" is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^5.0.1, string-width@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" - integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== - dependencies: - eastasianwidth "^0.2.0" - emoji-regex "^9.2.2" - strip-ansi "^7.0.1" + strip-ansi "^6.0.0" string.prototype.trim@^1.2.8: version "1.2.8" @@ -9136,7 +10229,7 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -string_decoder@^1.3.0: +string_decoder@^1.1.1, string_decoder@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== @@ -9150,20 +10243,13 @@ string_decoder@^1.3.0: dependencies: ansi-regex "^5.0.1" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" -strip-ansi@^7.0.1: - version "7.1.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" - integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== - dependencies: - ansi-regex "^6.0.1" - strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -9189,7 +10275,17 @@ strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -subscriptions-transport-ws@^0.9.11, subscriptions-transport-ws@^0.9.16, subscriptions-transport-ws@^0.9.19: +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== + +strnum@^1.0.5: + version "1.1.2" + resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.1.2.tgz#57bca4fbaa6f271081715dbc9ed7cee5493e28e4" + integrity sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA== + +subscriptions-transport-ws@^0.9.11, subscriptions-transport-ws@^0.9.16: version "0.9.19" resolved "https://registry.yarnpkg.com/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.19.tgz#10ca32f7e291d5ee8eb728b9c02e43c52606cdcf" integrity sha512-dxdemxFFB0ppCLg10FTtRqH/31FNRL1y1BQv8209MK5I4CwALb7iihQg+7p65lFcIl8MHatINWBLOqpgU4Kyyw== @@ -9233,7 +10329,7 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -symbol-observable@^1.0.2, symbol-observable@^1.0.4: +symbol-observable@^1.0.4: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== @@ -9248,13 +10344,17 @@ synchronous-promise@^2.0.10: resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.10.tgz#e64c6fd3afd25f423963353043f4a68ebd397fd8" integrity sha512-6PC+JRGmNjiG3kJ56ZMNWDPL8hjyghF5cMXIFOKg+NiwwEZZIvxTWd0pinWKyD227odg9ygF8xVhhz7gb8Uq7A== -synckit@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.1.tgz#8ea44544e9d9c0540963c6bddb8f14616fef5425" - integrity sha512-fWZqNBZNNFp/7mTUy1fSsydhKsAKJ+u90Nk7kOK5Gcq9vObaqLBLjWFDBkyVU9Vvc6Y71VbOevMuGhqv02bT+Q== +synckit@^0.11.7, "synckit@^0.6.2 || ^0.7.3 || ^0.11.5": + version "0.11.8" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.8.tgz#b2aaae998a4ef47ded60773ad06e7cb821f55457" + integrity sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A== dependencies: - "@pkgr/core" "^0.2.0" - tslib "^2.8.1" + "@pkgr/core" "^0.2.4" + +tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== tar@^6.1.11: version "6.1.13" @@ -9302,12 +10402,12 @@ timers-ext@^0.1.7: es5-ext "^0.10.64" next-tick "^1.1.0" -tinyglobby@^0.2.12: - version "0.2.12" - resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.12.tgz#ac941a42e0c5773bd0b5d08f32de82e74a1a61b5" - integrity sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww== +tinyglobby@^0.2.14: + version "0.2.14" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.14.tgz#5280b0cf3f972b050e74ae88406c0a6a58f4079d" + integrity sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ== dependencies: - fdir "^6.4.3" + fdir "^6.4.4" picomatch "^4.0.2" title@~3.4.1: @@ -9325,15 +10425,25 @@ titleize@1.0.0: resolved "https://registry.yarnpkg.com/titleize/-/titleize-1.0.0.tgz#7d350722061830ba6617631e0cfd3ea08398d95a" integrity sha1-fTUHIgYYMLpmF2MeDP0+oIOY2Vo= +titleize@2: + version "2.1.0" + resolved "https://registry.yarnpkg.com/titleize/-/titleize-2.1.0.tgz#5530de07c22147a0488887172b5bd94f5b30a48f" + integrity sha512-m+apkYlfiQTKLW+sI4vqUkwMEzfgEUEYSqljx1voUE3Wz/z1ZsxyzSxvH2X8uKVrOp7QkByWt0rA6+gvhCKy6g== + +tlds@1.255.0, tlds@^1.242.0: + version "1.255.0" + resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.255.0.tgz#53c2571766c10da95928c716c48dfcf141341e3f" + integrity sha512-tcwMRIioTcF/FcxLev8MJWxCp+GUALRhFEqbDoZrnowmKSGqPrl5pqS+Sut2m8BgJ6S4FExCSSpGffZ0Tks6Aw== + tlds@^1.187.0: version "1.203.1" resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.203.1.tgz#4dc9b02f53de3315bc98b80665e13de3edfc1dfc" integrity sha512-7MUlYyGJ6rSitEZ3r1Q1QNV8uSIzapS8SmmhSusBuIc7uIxPPwsKllEP0GRp1NS6Ik6F+fRZvnjDWm3ecv2hDw== -tlds@^1.242.0: - version "1.255.0" - resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.255.0.tgz#53c2571766c10da95928c716c48dfcf141341e3f" - integrity sha512-tcwMRIioTcF/FcxLev8MJWxCp+GUALRhFEqbDoZrnowmKSGqPrl5pqS+Sut2m8BgJ6S4FExCSSpGffZ0Tks6Aw== +tlds@^1.231.0: + version "1.256.0" + resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.256.0.tgz#4285b41a7ed4fcc7c5eed8516c3a180e892fad36" + integrity sha512-ZmyVB9DAw+FFTmLElGYJgdZFsKLYd/I59Bg9NHkCGPwAbVZNRilFWDMAdX8UG+bHuv7kfursd5XGqo/9wi26lA== tldts-core@^6.1.78: version "6.1.78" @@ -9371,16 +10481,16 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -toidentifier@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" - integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== - toidentifier@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +token-stream@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/token-stream/-/token-stream-1.0.0.tgz#cc200eab2613f4166d27ff9afc7ca56d49df6eb4" + integrity sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg== + toposort@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" @@ -9460,10 +10570,17 @@ truncate@~2.0.1: resolved "https://registry.yarnpkg.com/truncate/-/truncate-2.0.1.tgz#dd1a6d15630515663d8475f6f24edf2f800ebb1b" integrity sha1-3RptFWMFFWY9hHX28k7fL4AOuxs= -ts-api-utils@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" - integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== +ts-api-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz#595f7094e46eed364c13fd23e75f9513d29baf91" + integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ== + +ts-declaration-location@^1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/ts-declaration-location/-/ts-declaration-location-1.0.7.tgz#d4068fe9975828b3b453b3ab112b4711d8267688" + integrity sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA== + dependencies: + picomatch "^4.0.2" ts-invariant@^0.4.0: version "0.4.4" @@ -9472,10 +10589,10 @@ ts-invariant@^0.4.0: dependencies: tslib "^1.9.3" -ts-jest@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.3.1.tgz#2e459e1f94a833bd8216ba4b045fac948e265937" - integrity sha512-FT2PIRtZABwl6+ZCry8IY7JZ3xMuppsEV9qFVHOVe8jDzggwUZ9TsM4chyJxL9yi6LvkqcZYU3LmapEE454zBQ== +ts-jest@^29.3.4: + version "29.3.4" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.3.4.tgz#9354472aceae1d3867a80e8e02014ea5901aee41" + integrity sha512-Iqbrm8IXOmV+ggWHOTEbjwyCf2xZlUMv5npExksXohL+tk8va4Fjhb+X2+Rt9NBmgO7bJ8WpnMLOwih/DnMlFA== dependencies: bs-logger "^0.2.6" ejs "^3.1.10" @@ -9484,8 +10601,8 @@ ts-jest@^29.3.1: json5 "^2.2.3" lodash.memoize "^4.1.2" make-error "^1.3.6" - semver "^7.7.1" - type-fest "^4.38.0" + semver "^7.7.2" + type-fest "^4.41.0" yargs-parser "^21.1.1" ts-node@^10.9.2: @@ -9507,6 +10624,19 @@ ts-node@^10.9.2: v8-compile-cache-lib "^3.0.1" yn "3.1.1" +tsc-alias@^1.8.16: + version "1.8.16" + resolved "https://registry.yarnpkg.com/tsc-alias/-/tsc-alias-1.8.16.tgz#dbc74e797071801c7284f1a478259de920f852d4" + integrity sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g== + dependencies: + chokidar "^3.5.3" + commander "^9.0.0" + get-tsconfig "^4.10.0" + globby "^11.0.4" + mylas "^2.1.9" + normalize-path "^3.0.0" + plimit-lit "^1.2.6" + tsconfig-paths@^3.15.0: version "3.15.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" @@ -9517,6 +10647,15 @@ tsconfig-paths@^3.15.0: minimist "^1.2.6" strip-bom "^3.0.0" +tsconfig-paths@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c" + integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== + dependencies: + json5 "^2.2.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + tslib@1.11.1: version "1.11.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" @@ -9527,7 +10666,7 @@ tslib@^1.10.0, tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.4.0, tslib@^2.8.1: +tslib@^2.2.0, tslib@^2.4.0, tslib@^2.6.2: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== @@ -9573,10 +10712,10 @@ type-fest@^0.21.3: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== -type-fest@^4.38.0: - version "4.39.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.39.1.tgz#7521f6944e279abaf79cf60cfbc4823f4858083e" - integrity sha512-uW9qzd66uyHYxwyVBYiwS4Oi0qZyUqwjU+Oevr6ZogYiXt99EOYtwvzMSLw1c3lYo2HzJsep/NB23iEVEgjG/w== +type-fest@^4.41.0: + version "4.41.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.41.0.tgz#6ae1c8e5731273c2bf1f58ad39cbae2c91a46c58" + integrity sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA== type-is@^1.6.16, type-is@~1.6.18: version "1.6.18" @@ -9586,6 +10725,15 @@ type-is@^1.6.16, type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +type-is@^2.0.0, type-is@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-2.0.1.tgz#64f6cf03f92fce4015c2b224793f6bdd4b068c97" + integrity sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw== + dependencies: + content-type "^1.0.5" + media-typer "^1.1.0" + mime-types "^3.0.0" + type@^2.7.2: version "2.7.3" resolved "https://registry.yarnpkg.com/type/-/type-2.7.3.tgz#436981652129285cc3ba94f392886c2637ea0486" @@ -9679,6 +10827,11 @@ typescript@^5.8.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e" integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== +uc.micro@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee" + integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== + unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" @@ -9694,6 +10847,16 @@ undefsafe@^2.0.5: resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== +underscore.deep@~0.5.1: + version "0.5.3" + resolved "https://registry.yarnpkg.com/underscore.deep/-/underscore.deep-0.5.3.tgz#210969d58025339cecabd2a2ad8c3e8925e5c095" + integrity sha512-4OuSOlFNkiVFVc3khkeG112Pdu1gbitMj7t9B9ENb61uFmN70Jq7Iluhi3oflcSgexkKfDdJ5XAJET2gEq6ikA== + +underscore@~1.13.1: + version "1.13.7" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.7.tgz#970e33963af9a7dda228f17ebe8399e5fbe63a10" + integrity sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g== + undici-types@~6.21.0: version "6.21.0" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" @@ -9704,29 +10867,6 @@ undici@^6.19.5: resolved "https://registry.yarnpkg.com/undici/-/undici-6.19.8.tgz#002d7c8a28f8cc3a44ff33c3d4be4d85e15d40e1" integrity sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g== -unicode-canonical-property-names-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" - integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== - -unicode-match-property-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" - integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== - dependencies: - unicode-canonical-property-names-ecmascript "^2.0.0" - unicode-property-aliases-ecmascript "^2.0.0" - -unicode-match-property-value-ecmascript@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0" - integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== - -unicode-property-aliases-ecmascript@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" - integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== - unique-filename@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-3.0.0.tgz#48ba7a5a16849f5080d26c760c86cf5cf05770ea" @@ -9753,26 +10893,30 @@ unpipe@1.0.0, unpipe@~1.0.0: resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= -unrs-resolver@^1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/unrs-resolver/-/unrs-resolver-1.3.3.tgz#46bd5dd2ecc650365e050055fc208b5f4ae57803" - integrity sha512-PFLAGQzYlyjniXdbmQ3dnGMZJXX5yrl2YS4DLRfR3BhgUsE1zpRIrccp9XMOGRfIHpdFvCn/nr5N1KMVda4x3A== +unrs-resolver@^1.7.11: + version "1.7.12" + resolved "https://registry.yarnpkg.com/unrs-resolver/-/unrs-resolver-1.7.12.tgz#761858822f40267224fdb42caff8f97333e51ec1" + integrity sha512-pfcdDxrVoUc5ZB3VCVJNSWbs63lgQVYLVw4k/rCr8Smi/V2Sxi1odEckVq6Zf803OtbYia1+YpiGCZoODfWLsQ== + dependencies: + napi-postinstall "^0.2.2" optionalDependencies: - "@unrs/resolver-binding-darwin-arm64" "1.3.3" - "@unrs/resolver-binding-darwin-x64" "1.3.3" - "@unrs/resolver-binding-freebsd-x64" "1.3.3" - "@unrs/resolver-binding-linux-arm-gnueabihf" "1.3.3" - "@unrs/resolver-binding-linux-arm-musleabihf" "1.3.3" - "@unrs/resolver-binding-linux-arm64-gnu" "1.3.3" - "@unrs/resolver-binding-linux-arm64-musl" "1.3.3" - "@unrs/resolver-binding-linux-ppc64-gnu" "1.3.3" - "@unrs/resolver-binding-linux-s390x-gnu" "1.3.3" - "@unrs/resolver-binding-linux-x64-gnu" "1.3.3" - "@unrs/resolver-binding-linux-x64-musl" "1.3.3" - "@unrs/resolver-binding-wasm32-wasi" "1.3.3" - "@unrs/resolver-binding-win32-arm64-msvc" "1.3.3" - "@unrs/resolver-binding-win32-ia32-msvc" "1.3.3" - "@unrs/resolver-binding-win32-x64-msvc" "1.3.3" + "@unrs/resolver-binding-darwin-arm64" "1.7.12" + "@unrs/resolver-binding-darwin-x64" "1.7.12" + "@unrs/resolver-binding-freebsd-x64" "1.7.12" + "@unrs/resolver-binding-linux-arm-gnueabihf" "1.7.12" + "@unrs/resolver-binding-linux-arm-musleabihf" "1.7.12" + "@unrs/resolver-binding-linux-arm64-gnu" "1.7.12" + "@unrs/resolver-binding-linux-arm64-musl" "1.7.12" + "@unrs/resolver-binding-linux-ppc64-gnu" "1.7.12" + "@unrs/resolver-binding-linux-riscv64-gnu" "1.7.12" + "@unrs/resolver-binding-linux-riscv64-musl" "1.7.12" + "@unrs/resolver-binding-linux-s390x-gnu" "1.7.12" + "@unrs/resolver-binding-linux-x64-gnu" "1.7.12" + "@unrs/resolver-binding-linux-x64-musl" "1.7.12" + "@unrs/resolver-binding-wasm32-wasi" "1.7.12" + "@unrs/resolver-binding-win32-arm64-msvc" "1.7.12" + "@unrs/resolver-binding-win32-ia32-msvc" "1.7.12" + "@unrs/resolver-binding-win32-x64-msvc" "1.7.12" update-browserslist-db@^1.1.0: version "1.1.0" @@ -9782,14 +10926,6 @@ update-browserslist-db@^1.1.0: escalade "^3.1.2" picocolors "^1.0.1" -update-browserslist-db@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz#97e9c96ab0ae7bcac08e9ae5151d26e6bc6b5580" - integrity sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg== - dependencies: - escalade "^3.2.0" - picocolors "^1.1.1" - uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -9813,13 +10949,10 @@ url-regex@~4.1.1: ip-regex "^1.0.1" tlds "^1.187.0" -url@0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" - integrity sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ= - dependencies: - punycode "1.3.2" - querystring "0.2.0" +util-deprecate@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== util.promisify@^1.0.0: version "1.0.1" @@ -9838,27 +10971,11 @@ util@0.10.3: dependencies: inherits "2.0.1" -util@^0.12.4: - version "0.12.5" - resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" - integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== - dependencies: - inherits "^2.0.3" - is-arguments "^1.0.4" - is-generator-function "^1.0.7" - is-typed-array "^1.1.3" - which-typed-array "^1.1.2" - utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= -uuid@8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c" - integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw== - uuid@^3.1.0, uuid@^3.3.2, uuid@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" @@ -9874,7 +10991,7 @@ uuid@^8.0.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -uuid@~9.0.1: +uuid@^9.0.1, uuid@~9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== @@ -9893,19 +11010,17 @@ v8-to-istanbul@^9.0.1: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^2.0.0" -v8flags@^3.1.1: - version "3.1.3" - resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-3.1.3.tgz#fc9dc23521ca20c5433f81cc4eb9b3033bb105d8" - integrity sha512-amh9CCg3ZxkzQ48Mhcb8iX7xpAfYJgePHxWMQCBWECpOSqJUXgY26ncA61UTV0BkPqfhcy6mzwCIoP4ygxpW8w== - dependencies: - homedir-polyfill "^1.0.1" +valid-data-url@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/valid-data-url/-/valid-data-url-3.0.1.tgz#826c1744e71b5632e847dd15dbd45b9fb38aa34f" + integrity sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA== -validator@^13.15.0: - version "13.15.0" - resolved "https://registry.yarnpkg.com/validator/-/validator-13.15.0.tgz#2dc7ce057e7513a55585109eec29b2c8e8c1aefd" - integrity sha512-36B2ryl4+oL5QxZ3AzD0t5SsMNGvTtQHpjgFO5tbNxfXbMFkY822ktCDe1MnlqV3301QQI9SLHDNJokDI+Z9pA== +validator@^13.15.15: + version "13.15.15" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.15.15.tgz#246594be5671dc09daa35caec5689fcd18c6e7e4" + integrity sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A== -vary@^1, vary@~1.1.2: +vary@^1, vary@^1.1.2, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= @@ -9929,6 +11044,11 @@ video-extensions@~1.1.0: resolved "https://registry.yarnpkg.com/video-extensions/-/video-extensions-1.1.0.tgz#eaa86b45f29a853c2b873e9d8e23b513712997d6" integrity sha1-6qhrRfKahTwrhz6djiO1E3Epl9Y= +void-elements@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" + integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== + w3c-xmlserializer@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz#f925ba26855158594d907313cedd1476c5967f6c" @@ -9943,6 +11063,18 @@ walker@^1.0.8: dependencies: makeerror "1.0.12" +web-resource-inliner@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/web-resource-inliner/-/web-resource-inliner-6.0.1.tgz#df0822f0a12028805fe80719ed52ab6526886e02" + integrity sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A== + 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" + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" @@ -10000,7 +11132,7 @@ which-boxed-primitive@^1.0.2: is-string "^1.0.5" is-symbol "^1.0.3" -which-typed-array@^1.1.11, which-typed-array@^1.1.2: +which-typed-array@^1.1.11: version "1.1.11" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.11.tgz#99d691f23c72aab6768680805a271b69761ed61a" integrity sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew== @@ -10059,6 +11191,16 @@ whoops@~5.0.1: resolved "https://registry.yarnpkg.com/whoops/-/whoops-5.0.1.tgz#ce2fd6f255aca09b6fd8ec00c99f9761420296d4" integrity sha512-H2sKu1asxnFE2mNUeRzNY0CQuvl+n6iyE6phvzOaBfZblItNKpC1EzKWy2Zx+woZ3qUFK/wbmmNiLeqXwzk+FA== +with@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/with/-/with-7.0.2.tgz#ccee3ad542d25538a7a7a80aad212b9828495bac" + integrity sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w== + dependencies: + "@babel/parser" "^7.9.6" + "@babel/types" "^7.9.6" + assert-never "^1.2.1" + babel-walk "3.0.0-canary-5" + "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" @@ -10068,7 +11210,7 @@ whoops@~5.0.1: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: +wrap-ansi@7.0.0, wrap-ansi@^7.0.0, wrap-ansi@^8.1.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -10077,15 +11219,6 @@ wrap-ansi@^7.0.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" - integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== - dependencies: - ansi-styles "^6.1.0" - string-width "^5.0.1" - strip-ansi "^7.0.1" - wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -10121,19 +11254,6 @@ xml-name-validator@^5.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz#82be9b957f7afdacf961e5980f1bf227c0bf7673" integrity sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg== -xml2js@0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.2.tgz#dd0b630083aa09c161e25a4d0901e2b2a929b499" - integrity sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA== - dependencies: - sax ">=0.6.0" - xmlbuilder "~11.0.0" - -xmlbuilder@~11.0.0: - version "11.0.1" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" - integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== - xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" diff --git a/cypress/cypress.config.js b/cypress/cypress.config.js index d08749370..52a765bf0 100644 --- a/cypress/cypress.config.js +++ b/cypress/cypress.config.js @@ -44,7 +44,7 @@ module.exports = defineConfig({ chromeWebSecurity: false, baseUrl: 'http://localhost:3000', specPattern: '**/*.feature', - supportFile: 'cypress/support/e2e.js', + supportFile: false, retries: 0, video: false, viewportHeight: 720, diff --git a/cypress/e2e/Group.Create.feature b/cypress/e2e/Group.Create.feature new file mode 100644 index 000000000..13b6d0b3a --- /dev/null +++ b/cypress/e2e/Group.Create.feature @@ -0,0 +1,27 @@ +Feature: Create a group + As an logged in user + I would like to create a group + To invite my friends + + Background: + Given the following "users" are in the database: + | slug | email | password | id | name | termsAndConditionsAgreedVersion | + | narrator | narrator@example.org | 1234 | narrator | Nathan Narrator | 0.0.4 | + And I am logged in as "narrator" + And I navigate to page "/groups" + + Scenario: Create a group + When I click on "create group button" + Then I am on page "groups/create" + When I choose "My group " as the name + And I choose "public" as the visibility + And I choose "to invite my friends" as the about + And I choose the following text as description: + """ + This is the group where I want to exchange + my views with my friends. + """ + And I choose "regional" as the action radius + And I click on "save button" + Then I am on page "/groups/.*/my-group" + And the group was saved successfully diff --git a/cypress/e2e/User.SettingNotifications.feature b/cypress/e2e/User.SettingNotifications.feature.broken similarity index 100% rename from cypress/e2e/User.SettingNotifications.feature rename to cypress/e2e/User.SettingNotifications.feature.broken diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 92e8bf1f7..f75fe9b6b 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -53,16 +53,3 @@ Cypress.Commands.add( }) }) - -// -// -// -- This is a child command -- -// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This is will overwrite an existing command -- -// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js deleted file mode 100644 index 453c8476f..000000000 --- a/cypress/support/e2e.js +++ /dev/null @@ -1,33 +0,0 @@ -// *********************************************************** -// This example support/index.js is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -// Import commands.js using ES2015 syntax: - -import './commands' -import './factories' - -// intermittent failing tests -// import 'cypress-plugin-retries' - -// Alternatively you can use CommonJS syntax: -// require('./commands') -import { WebSocket } from 'mock-socket' -before(() => { - cy.visit('/', { - onBeforeLoad(win) { - cy.stub(win, "WebSocket", url => new WebSocket(url)) - } - }) -}) diff --git a/cypress/support/step_definitions/Admin.DonationInfo/the_donation_info_contains_goal_{string}_and_progress_{string}.js b/cypress/support/step_definitions/Admin.DonationInfo/the_donation_info_contains_goal_{string}_and_progress_{string}.js index 454aea44b..a847e3e35 100644 --- a/cypress/support/step_definitions/Admin.DonationInfo/the_donation_info_contains_goal_{string}_and_progress_{string}.js +++ b/cypress/support/step_definitions/Admin.DonationInfo/the_donation_info_contains_goal_{string}_and_progress_{string}.js @@ -1,8 +1,8 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When("the donation info contains goal {string} and progress {string}", (goal, progress) => { +defineStep('the donation info contains goal {string} and progress {string}', (goal, progress) => { cy.get('.top-info-bar') .should('contain', goal) cy.get('.top-info-bar') .should('contain', progress) -}); +}) diff --git a/cypress/support/step_definitions/Admin.DonationInfo/the_donation_info_is_{string}.js b/cypress/support/step_definitions/Admin.DonationInfo/the_donation_info_is_{string}.js index da231f23a..ef6c69767 100644 --- a/cypress/support/step_definitions/Admin.DonationInfo/the_donation_info_is_{string}.js +++ b/cypress/support/step_definitions/Admin.DonationInfo/the_donation_info_is_{string}.js @@ -1,6 +1,6 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("the donation info is {string}", (visibility) => { +defineStep('the donation info is {string}', (visibility) => { cy.get('.top-info-bar') .should(visibility === 'visible' ? 'exist' : 'not.exist') }) diff --git a/cypress/support/step_definitions/Admin.PinPost/I_open_the_content_menu_of_post_{string}.js b/cypress/support/step_definitions/Admin.PinPost/I_open_the_content_menu_of_post_{string}.js index 2b8d00dc9..dca7b9e7f 100644 --- a/cypress/support/step_definitions/Admin.PinPost/I_open_the_content_menu_of_post_{string}.js +++ b/cypress/support/step_definitions/Admin.PinPost/I_open_the_content_menu_of_post_{string}.js @@ -1,6 +1,6 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When("I open the content menu of post {string}", (title) => { +defineStep('I open the content menu of post {string}', (title) => { cy.contains('.post-teaser', title) .find('.content-menu .base-button') .click() diff --git a/cypress/support/step_definitions/Admin.PinPost/the_post_with_title_{string}_has_a_ribbon_for_pinned_posts.js b/cypress/support/step_definitions/Admin.PinPost/the_post_with_title_{string}_has_a_ribbon_for_pinned_posts.js index 3e9f43bc3..5690ac23e 100644 --- a/cypress/support/step_definitions/Admin.PinPost/the_post_with_title_{string}_has_a_ribbon_for_pinned_posts.js +++ b/cypress/support/step_definitions/Admin.PinPost/the_post_with_title_{string}_has_a_ribbon_for_pinned_posts.js @@ -1,9 +1,9 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("the post with title {string} has a ribbon for pinned posts", (title) => { - cy.get(".post-teaser").contains(title) +defineStep('the post with title {string} has a ribbon for pinned posts', (title) => { + cy.get('.post-teaser').contains(title) .parent() .parent() - .find(".ribbon.--pinned") - .should("contain", "Announcement") + .find('.ribbon.--pinned') + .should('contain', 'Announcement') }) diff --git a/cypress/support/step_definitions/Admin.PinPost/there_is_no_button_to_pin_a_post.js b/cypress/support/step_definitions/Admin.PinPost/there_is_no_button_to_pin_a_post.js index 70535b920..5d75e9a87 100644 --- a/cypress/support/step_definitions/Admin.PinPost/there_is_no_button_to_pin_a_post.js +++ b/cypress/support/step_definitions/Admin.PinPost/there_is_no_button_to_pin_a_post.js @@ -1,7 +1,7 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("there is no button to pin a post", () => { - cy.get("a.ds-menu-item-link") - .should('contain', "Report Post") // sanity check - .should('not.contain', "Pin post") +defineStep('there is no button to pin a post', () => { + cy.get('a.ds-menu-item-link') + .should('contain', 'Report Post') // sanity check + .should('not.contain', 'Pin post') }) diff --git a/cypress/support/step_definitions/Group.Create/I_choose_the_following_text_as_description.js b/cypress/support/step_definitions/Group.Create/I_choose_the_following_text_as_description.js new file mode 100644 index 000000000..278cab547 --- /dev/null +++ b/cypress/support/step_definitions/Group.Create/I_choose_the_following_text_as_description.js @@ -0,0 +1,9 @@ +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' + +defineStep('I choose the following text as description:', text => { + cy.task('getValue', 'lastGroup').then(lastGroup => { + lastGroup.description = text.replace('\n', ' ') + cy.task('pushValue', { name: 'lastGroup', value: lastGroup }) + cy.get('.editor .ProseMirror').type(lastGroup.description) + }) +}) diff --git a/cypress/support/step_definitions/Group.Create/I_choose_{string}_as_the_about.js b/cypress/support/step_definitions/Group.Create/I_choose_{string}_as_the_about.js new file mode 100644 index 000000000..7106ad44a --- /dev/null +++ b/cypress/support/step_definitions/Group.Create/I_choose_{string}_as_the_about.js @@ -0,0 +1,9 @@ +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' + +defineStep('I choose {string} as the about', about => { + cy.task('getValue', 'lastGroup').then(lastGroup => { + lastGroup.about = about.replace('\n', ' ') + cy.task('pushValue', { name: 'lastGroup', value: lastGroup }) + cy.get('input[name="about"]').type(lastGroup.about) + }) +}) diff --git a/cypress/support/step_definitions/Group.Create/I_choose_{string}_as_the_action_radius.js b/cypress/support/step_definitions/Group.Create/I_choose_{string}_as_the_action_radius.js new file mode 100644 index 000000000..6d1fd8615 --- /dev/null +++ b/cypress/support/step_definitions/Group.Create/I_choose_{string}_as_the_action_radius.js @@ -0,0 +1,9 @@ +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' + +defineStep('I choose {string} as the action radius', actionRadius => { + cy.task('getValue', 'lastGroup').then(lastGroup => { + lastGroup.actionRadius = actionRadius.replace('\n', ' ') + cy.task('pushValue', { name: 'lastGroup', value: lastGroup }) + cy.get('select[name="actionRadius"]').select(lastGroup.actionRadius) + }) +}) diff --git a/cypress/support/step_definitions/Group.Create/I_choose_{string}_as_the_name.js b/cypress/support/step_definitions/Group.Create/I_choose_{string}_as_the_name.js new file mode 100644 index 000000000..1c5c1b1e8 --- /dev/null +++ b/cypress/support/step_definitions/Group.Create/I_choose_{string}_as_the_name.js @@ -0,0 +1,8 @@ +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' + +defineStep('I choose {string} as the name', name => { + const lastGroup = {} + lastGroup.name = name.replace('\n', ' ') + cy.task('pushValue', { name: 'lastGroup', value: lastGroup }) + cy.get('input[name="name"]').type(lastGroup.name) +}) diff --git a/cypress/support/step_definitions/Group.Create/I_choose_{string}_as_the_visibility.js b/cypress/support/step_definitions/Group.Create/I_choose_{string}_as_the_visibility.js new file mode 100644 index 000000000..ed1a78ac2 --- /dev/null +++ b/cypress/support/step_definitions/Group.Create/I_choose_{string}_as_the_visibility.js @@ -0,0 +1,9 @@ +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' + +defineStep('I choose {string} as the visibility', groupType => { + cy.task('getValue', 'lastGroup').then(lastGroup => { + lastGroup.groupType = groupType.replace('\n', ' ') + cy.task('pushValue', { name: 'lastGroup', value: lastGroup }) + cy.get('select[name="groupType"]').select(lastGroup.groupType) + }) +}) diff --git a/cypress/support/step_definitions/Group.Create/the_group_was_saved_successfully.js b/cypress/support/step_definitions/Group.Create/the_group_was_saved_successfully.js new file mode 100644 index 000000000..591d5ba75 --- /dev/null +++ b/cypress/support/step_definitions/Group.Create/the_group_was_saved_successfully.js @@ -0,0 +1,7 @@ +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' + +defineStep('the group was saved successfully', () => { + cy.task('getValue', 'lastGroup').then(lastGroup => { + cy.get('h3.ds-heading').should('contain', lastGroup.name) + }) +}) diff --git a/cypress/support/step_definitions/Internationalization/I_see_a_button_with_the_label_{string}.js b/cypress/support/step_definitions/Internationalization/I_see_a_button_with_the_label_{string}.js index 73a4a5e50..921b0344f 100644 --- a/cypress/support/step_definitions/Internationalization/I_see_a_button_with_the_label_{string}.js +++ b/cypress/support/step_definitions/Internationalization/I_see_a_button_with_the_label_{string}.js @@ -1,5 +1,5 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("I see a button with the label {string}", label => { - cy.contains("button", label); -}); +defineStep('I see a button with the label {string}', label => { + cy.contains('button', label) +}) diff --git a/cypress/support/step_definitions/Internationalization/I_select_{string}_in_the_language_menu.js b/cypress/support/step_definitions/Internationalization/I_select_{string}_in_the_language_menu.js index ba89fd3f5..48e1d4b59 100644 --- a/cypress/support/step_definitions/Internationalization/I_select_{string}_in_the_language_menu.js +++ b/cypress/support/step_definitions/Internationalization/I_select_{string}_in_the_language_menu.js @@ -1,8 +1,8 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When("I select {string} in the language menu", language => { - cy.get(".locale-menu") - .click(); - cy.contains(".locale-menu-popover a", language) - .click(); -}); +defineStep('I select {string} in the language menu', language => { + cy.get('.locale-menu') + .click() + cy.contains('.locale-menu-popover a', language) + .click() +}) diff --git a/cypress/support/step_definitions/Internationalization/the_whole_user_interface_appears_in_{string}.js b/cypress/support/step_definitions/Internationalization/the_whole_user_interface_appears_in_{string}.js index d5a8ac95c..22d5e8c14 100644 --- a/cypress/support/step_definitions/Internationalization/the_whole_user_interface_appears_in_{string}.js +++ b/cypress/support/step_definitions/Internationalization/the_whole_user_interface_appears_in_{string}.js @@ -1,8 +1,8 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' import locales from '../../../../webapp/locales' -Then("the whole user interface appears in {string}", language => { - const { code } = locales.find((entry) => entry.name === language); - cy.get(`html[lang=${code}]`); - cy.getCookie("locale").should("have.property", "value", code); -}); +defineStep('the whole user interface appears in {string}', language => { + const { code } = locales.find((entry) => entry.name === language) + cy.get(`html[lang=${code}]`) + cy.getCookie('locale').should('have.property', 'value', code) +}) diff --git a/cypress/support/step_definitions/Moderation.HidePost/I_should_see_only_{int}_posts_on_the_newsfeed.js b/cypress/support/step_definitions/Moderation.HidePost/I_should_see_only_{int}_posts_on_the_newsfeed.js index 26221ae66..5a0b2cf17 100644 --- a/cypress/support/step_definitions/Moderation.HidePost/I_should_see_only_{int}_posts_on_the_newsfeed.js +++ b/cypress/support/step_definitions/Moderation.HidePost/I_should_see_only_{int}_posts_on_the_newsfeed.js @@ -1,7 +1,7 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("I should see only {int} posts on the newsfeed", posts => { - cy.get(".post-teaser") - .should("have.length", posts); -}); +defineStep('I should see only {int} posts on the newsfeed', posts => { + cy.get('.post-teaser') + .should('have.length', posts) +}) diff --git a/cypress/support/step_definitions/Moderation.HidePost/the_page_{string}_returns_a_404_error_with_a_message.js b/cypress/support/step_definitions/Moderation.HidePost/the_page_{string}_returns_a_404_error_with_a_message.js index 538e8a64d..af813cd6e 100644 --- a/cypress/support/step_definitions/Moderation.HidePost/the_page_{string}_returns_a_404_error_with_a_message.js +++ b/cypress/support/step_definitions/Moderation.HidePost/the_page_{string}_returns_a_404_error_with_a_message.js @@ -1,14 +1,14 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("the page {string} returns a 404 error with a message:", (route, message) => { +defineStep('the page {string} returns a 404 error with a message:', (route, message) => { cy.request({ url: route, failOnStatusCode: false }) - .its("status") - .should("eq", 404); + .its('status') + .should('eq', 404) cy.visit(route, { failOnStatusCode: false - }); - cy.get(".error-message").should("contain", message); -}); + }) + cy.get('.error-message').should('contain', message) +}) diff --git a/cypress/support/step_definitions/Moderation.ReportContent/I_can_t_see_the_moderation_menu_item.js b/cypress/support/step_definitions/Moderation.ReportContent/I_can_t_see_the_moderation_menu_item.js index fcb1cb686..dd929e2d4 100644 --- a/cypress/support/step_definitions/Moderation.ReportContent/I_can_t_see_the_moderation_menu_item.js +++ b/cypress/support/step_definitions/Moderation.ReportContent/I_can_t_see_the_moderation_menu_item.js @@ -1,6 +1,6 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then(`I can't see the moderation menu item`, () => { +defineStep(`I can't see the moderation menu item`, () => { cy.get('.avatar-menu-popover') .find('a[href="/settings"]', 'Settings') .should('exist') // OK, the dropdown is actually open diff --git a/cypress/support/step_definitions/Moderation.ReportContent/I_can_visit_the_post_page.js b/cypress/support/step_definitions/Moderation.ReportContent/I_can_visit_the_post_page.js index ce846c39a..2986a8fc8 100644 --- a/cypress/support/step_definitions/Moderation.ReportContent/I_can_visit_the_post_page.js +++ b/cypress/support/step_definitions/Moderation.ReportContent/I_can_visit_the_post_page.js @@ -1,6 +1,6 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then('I can visit the post page', () => { +defineStep('I can visit the post page', () => { cy.contains('Fake news').click() cy.location('pathname').should('contain', '/post') .get('.base-card .title').should('contain', 'Fake news') diff --git a/cypress/support/step_definitions/Moderation.ReportContent/I_click_on_Report_Post_from_the_content_menu_of_the_post.js b/cypress/support/step_definitions/Moderation.ReportContent/I_click_on_Report_Post_from_the_content_menu_of_the_post.js index 8588e156a..bcfc362cd 100644 --- a/cypress/support/step_definitions/Moderation.ReportContent/I_click_on_Report_Post_from_the_content_menu_of_the_post.js +++ b/cypress/support/step_definitions/Moderation.ReportContent/I_click_on_Report_Post_from_the_content_menu_of_the_post.js @@ -1,6 +1,6 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When('I click on "Report Post" from the content menu of the post', () => { +defineStep('I click on "Report Post" from the content menu of the post', () => { cy.contains('.base-card', 'The Truth about the Holocaust') .find('.content-menu .base-button') .click() diff --git a/cypress/support/step_definitions/Moderation.ReportContent/I_click_on_the_author.js b/cypress/support/step_definitions/Moderation.ReportContent/I_click_on_the_author.js index 049eb8e72..d0526bebd 100644 --- a/cypress/support/step_definitions/Moderation.ReportContent/I_click_on_the_author.js +++ b/cypress/support/step_definitions/Moderation.ReportContent/I_click_on_the_author.js @@ -1,6 +1,6 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When('I click on the author', () => { +defineStep('I click on the author', () => { cy.get('[data-test="avatarUserLink"]') .click() .url().should('include', '/profile/') diff --git a/cypress/support/step_definitions/Moderation.ReportContent/I_click_on_the_avatar_menu_in_the_top_right_corner.js b/cypress/support/step_definitions/Moderation.ReportContent/I_click_on_the_avatar_menu_in_the_top_right_corner.js index 0bb1e816f..5ce6ca6b6 100644 --- a/cypress/support/step_definitions/Moderation.ReportContent/I_click_on_the_avatar_menu_in_the_top_right_corner.js +++ b/cypress/support/step_definitions/Moderation.ReportContent/I_click_on_the_avatar_menu_in_the_top_right_corner.js @@ -1,7 +1,7 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; -import 'cypress-network-idle'; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' +import 'cypress-network-idle' -When("I click on the avatar menu in the top right corner", () => { - cy.get(".avatar-menu").click(); - cy.waitForNetworkIdle(2000); -}); +defineStep('I click on the avatar menu in the top right corner', () => { + cy.get('.avatar-menu').click() + cy.waitForNetworkIdle(2000) +}) diff --git a/cypress/support/step_definitions/Moderation.ReportContent/I_confirm_the_reporting_dialog.js b/cypress/support/step_definitions/Moderation.ReportContent/I_confirm_the_reporting_dialog.js index 970c61c00..085641b94 100644 --- a/cypress/support/step_definitions/Moderation.ReportContent/I_confirm_the_reporting_dialog.js +++ b/cypress/support/step_definitions/Moderation.ReportContent/I_confirm_the_reporting_dialog.js @@ -1,6 +1,6 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When(/^I confirm the reporting dialog .*:$/, message => { +defineStep(/^I confirm the reporting dialog .*:$/, message => { cy.contains(message) // wait for element to become visible cy.get('.ds-modal') .within(() => { diff --git a/cypress/support/step_definitions/Moderation.ReportContent/I_see_all_the_reported_posts_including_from_the_user_who_muted_me.js b/cypress/support/step_definitions/Moderation.ReportContent/I_see_all_the_reported_posts_including_from_the_user_who_muted_me.js index 8989ecf68..eaa06e625 100644 --- a/cypress/support/step_definitions/Moderation.ReportContent/I_see_all_the_reported_posts_including_from_the_user_who_muted_me.js +++ b/cypress/support/step_definitions/Moderation.ReportContent/I_see_all_the_reported_posts_including_from_the_user_who_muted_me.js @@ -1,6 +1,6 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then('I see all the reported posts including from the user who muted me', () => { +defineStep('I see all the reported posts including from the user who muted me', () => { cy.get('table tbody').within(() => { cy.contains('tr', 'Fake news') }) diff --git a/cypress/support/step_definitions/Moderation.ReportContent/I_see_all_the_reported_posts_including_the_one_from_above.js b/cypress/support/step_definitions/Moderation.ReportContent/I_see_all_the_reported_posts_including_the_one_from_above.js index f5cb71f91..40d018f6a 100644 --- a/cypress/support/step_definitions/Moderation.ReportContent/I_see_all_the_reported_posts_including_the_one_from_above.js +++ b/cypress/support/step_definitions/Moderation.ReportContent/I_see_all_the_reported_posts_including_the_one_from_above.js @@ -1,6 +1,6 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then('I see all the reported posts including the one from above', () => { +defineStep('I see all the reported posts including the one from above', () => { cy.intercept({ method: 'POST', url: '/api', diff --git a/cypress/support/step_definitions/Moderation.ReportContent/each_list_item_links_to_the_post_page.js b/cypress/support/step_definitions/Moderation.ReportContent/each_list_item_links_to_the_post_page.js index e174113fc..99283a117 100644 --- a/cypress/support/step_definitions/Moderation.ReportContent/each_list_item_links_to_the_post_page.js +++ b/cypress/support/step_definitions/Moderation.ReportContent/each_list_item_links_to_the_post_page.js @@ -1,6 +1,6 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then('each list item links to the post page', () => { - cy.contains('The Truth about the Holocaust').click(); +defineStep('each list item links to the post page', () => { + cy.contains('The Truth about the Holocaust').click() cy.location('pathname').should('contain', '/post') }) diff --git a/cypress/support/step_definitions/Moderation.ReportContent/somebody_reported_the_following_posts.js b/cypress/support/step_definitions/Moderation.ReportContent/somebody_reported_the_following_posts.js index 38cdbee09..191bcd25f 100644 --- a/cypress/support/step_definitions/Moderation.ReportContent/somebody_reported_the_following_posts.js +++ b/cypress/support/step_definitions/Moderation.ReportContent/somebody_reported_the_following_posts.js @@ -1,7 +1,9 @@ -import { Given } from "@badeball/cypress-cucumber-preprocessor"; -import 'cypress-network-idle'; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' +import './../../commands' +import './../../factories' +import 'cypress-network-idle' -Given('somebody reported the following posts:', table => { +defineStep('somebody reported the following posts:', table => { const reportIdRegex = /^[0-9a-zA-Z]{8}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{12}$/ cy.intercept({ method: 'POST', diff --git a/cypress/support/step_definitions/Moderation.ReportContent/there_is_an_annoying_user_who_has_muted_me.js b/cypress/support/step_definitions/Moderation.ReportContent/there_is_an_annoying_user_who_has_muted_me.js index 8d61afd61..1b0c72ea2 100644 --- a/cypress/support/step_definitions/Moderation.ReportContent/there_is_an_annoying_user_who_has_muted_me.js +++ b/cypress/support/step_definitions/Moderation.ReportContent/there_is_an_annoying_user_who_has_muted_me.js @@ -1,15 +1,15 @@ -import { Given } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Given("there is an annoying user who has muted me", () => { +defineStep('there is an annoying user who has muted me', () => { cy.neode() - .firstOf("User", { + .firstOf('User', { role: 'moderator' }) .then(mutedUser => { cy.neode() - .firstOf("User", { + .firstOf('User', { id: 'user' }) - .relateTo(mutedUser, "muted"); - }); -}); + .relateTo(mutedUser, 'muted') + }) +}) diff --git a/cypress/support/step_definitions/Notification.Mention/I_start_to_write_a_new_post_with_the_title_{string}_beginning_with.js b/cypress/support/step_definitions/Notification.Mention/I_start_to_write_a_new_post_with_the_title_{string}_beginning_with.js index b8e705c82..b6cd829e1 100644 --- a/cypress/support/step_definitions/Notification.Mention/I_start_to_write_a_new_post_with_the_title_{string}_beginning_with.js +++ b/cypress/support/step_definitions/Notification.Mention/I_start_to_write_a_new_post_with_the_title_{string}_beginning_with.js @@ -1,8 +1,8 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When("I start to write a new post with the title {string} beginning with:", (title, intro) => { +defineStep('I start to write a new post with the title {string} beginning with:', (title, intro) => { cy.get('input[name="title"]') - .type(title); - cy.get(".ProseMirror") - .type(intro); -}); + .type(title) + cy.get('.ProseMirror') + .type(intro) +}) diff --git a/cypress/support/step_definitions/Notification.Mention/mention_{string}_in_the_text.js b/cypress/support/step_definitions/Notification.Mention/mention_{string}_in_the_text.js index e1bd19da0..0997111d8 100644 --- a/cypress/support/step_definitions/Notification.Mention/mention_{string}_in_the_text.js +++ b/cypress/support/step_definitions/Notification.Mention/mention_{string}_in_the_text.js @@ -1,9 +1,9 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When("mention {string} in the text", mention => { - cy.get(".ProseMirror") - .type(" @"); - cy.get(".suggestion-list__item") +defineStep('mention {string} in the text', mention => { + cy.get('.ProseMirror') + .type(' @') + cy.get('.suggestion-list__item') .contains(mention) - .click(); -}); + .click() +}) diff --git a/cypress/support/step_definitions/Notification.Mention/open_the_notification_menu_and_click_on_the_first_item.js b/cypress/support/step_definitions/Notification.Mention/open_the_notification_menu_and_click_on_the_first_item.js index 0143d1ac5..534db2a56 100644 --- a/cypress/support/step_definitions/Notification.Mention/open_the_notification_menu_and_click_on_the_first_item.js +++ b/cypress/support/step_definitions/Notification.Mention/open_the_notification_menu_and_click_on_the_first_item.js @@ -1,10 +1,10 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When("open the notification menu and click on the first item", () => { - cy.get(".notifications-menu") +defineStep('open the notification menu and click on the first item', () => { + cy.get('.notifications-menu') .invoke('show') - .click(); // "invoke('show')" because of the delay for show the menu - cy.get(".notification .link") + .click() // 'invoke('show')' because of the delay for show the menu + cy.get('.notification-content a') .first() - .click({force: true}); -}); + .click({force: true}) +}) diff --git a/cypress/support/step_definitions/Notification.Mention/see_{int}_unread_notifications_in_the_top_menu.js b/cypress/support/step_definitions/Notification.Mention/see_{int}_unread_notifications_in_the_top_menu.js index ae1644cef..eebb5b877 100644 --- a/cypress/support/step_definitions/Notification.Mention/see_{int}_unread_notifications_in_the_top_menu.js +++ b/cypress/support/step_definitions/Notification.Mention/see_{int}_unread_notifications_in_the_top_menu.js @@ -1,6 +1,6 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("see {int} unread notifications in the top menu", count => { - cy.get(".notifications-menu") - .should("contain", count); -}); +defineStep('see {int} unread notifications in the top menu', count => { + cy.get('.notifications-menu') + .should('contain', count) +}) diff --git a/cypress/support/step_definitions/Notification.Mention/the_notification_menu_button_links_to_the_all_notifications_page.js b/cypress/support/step_definitions/Notification.Mention/the_notification_menu_button_links_to_the_all_notifications_page.js index a7204978e..3cdbeb305 100644 --- a/cypress/support/step_definitions/Notification.Mention/the_notification_menu_button_links_to_the_all_notifications_page.js +++ b/cypress/support/step_definitions/Notification.Mention/the_notification_menu_button_links_to_the_all_notifications_page.js @@ -1,8 +1,8 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("the notification menu button links to the all notifications page", () => { - cy.get(".notifications-menu") - .click(); - cy.location("pathname") - .should("contain", "/notifications"); -}); +defineStep('the notification menu button links to the all notifications page', () => { + cy.get('.notifications-menu') + .click() + cy.location('pathname') + .should('contain', '/notifications') +}) diff --git a/cypress/support/step_definitions/Notification.Mention/the_unread_counter_is_removed.js b/cypress/support/step_definitions/Notification.Mention/the_unread_counter_is_removed.js index 6c7ff96f0..771d66c3c 100644 --- a/cypress/support/step_definitions/Notification.Mention/the_unread_counter_is_removed.js +++ b/cypress/support/step_definitions/Notification.Mention/the_unread_counter_is_removed.js @@ -1,6 +1,6 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("the unread counter is removed", () => { +defineStep('the unread counter is removed', () => { cy.get('.notifications-menu .counter-icon') - .should('not.exist'); -}); + .should('not.exist') +}) diff --git a/cypress/support/step_definitions/Post.Comment/I_comment_the_following.js b/cypress/support/step_definitions/Post.Comment/I_comment_the_following.js index 495075b60..76d5b2c55 100644 --- a/cypress/support/step_definitions/Post.Comment/I_comment_the_following.js +++ b/cypress/support/step_definitions/Post.Comment/I_comment_the_following.js @@ -1,7 +1,7 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When("I comment the following:", text => { - const comment = text.replace("\n", " ") +defineStep('I comment the following:', text => { + const comment = text.replace('\n', ' ') cy.task('pushValue', { name: 'lastComment', value: comment }) - cy.get(".editor .ProseMirror").type(comment); -}); + cy.get('.editor .ProseMirror').type(comment) +}) diff --git a/cypress/support/step_definitions/Post.Comment/I_should_see_an_abbreviated_version_of_my_comment.js b/cypress/support/step_definitions/Post.Comment/I_should_see_an_abbreviated_version_of_my_comment.js index 67dc9bef8..f27cf795a 100644 --- a/cypress/support/step_definitions/Post.Comment/I_should_see_an_abbreviated_version_of_my_comment.js +++ b/cypress/support/step_definitions/Post.Comment/I_should_see_an_abbreviated_version_of_my_comment.js @@ -1,6 +1,6 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("I should see an abbreviated version of my comment", () => { - cy.get("article.comment-card") - .should("contain", "show more") -}); +defineStep('I should see an abbreviated version of my comment', () => { + cy.get('article.comment-card') + .should('contain', 'show more') +}) diff --git a/cypress/support/step_definitions/Post.Comment/I_should_see_my_comment.js b/cypress/support/step_definitions/Post.Comment/I_should_see_my_comment.js index 7b30ee82d..332379dcc 100644 --- a/cypress/support/step_definitions/Post.Comment/I_should_see_my_comment.js +++ b/cypress/support/step_definitions/Post.Comment/I_should_see_my_comment.js @@ -1,13 +1,13 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("I should see my comment", () => { - cy.get("article.comment-card p") - .should("contain", "Ocelot.social rocks") - .get(".user-teaser span.slug") - .should("contain", "@peter-pan") // specific enough - .get(".profile-avatar img") - .should("have.attr", "src") - .and("contain", 'https://') // some url - .get(".user-teaser > .info > .text") - .should("contain", "today at"); -}); +defineStep('I should see my comment', () => { + cy.get('article.comment-card p') + .should('contain', 'Ocelot.social rocks') + .get('.user-teaser span.slug') + .should('contain', '@peter-pan') // specific enough + .get('.profile-avatar img') + .should('have.attr', 'src') + .and('contain', 'https://') // some url + .get('.user-teaser .info > .text') + .should('contain', 'today at') +}) diff --git a/cypress/support/step_definitions/Post.Comment/I_should_see_the_entirety_of_my_comment.js b/cypress/support/step_definitions/Post.Comment/I_should_see_the_entirety_of_my_comment.js index 9ea48fa05..eb0d3d70f 100644 --- a/cypress/support/step_definitions/Post.Comment/I_should_see_the_entirety_of_my_comment.js +++ b/cypress/support/step_definitions/Post.Comment/I_should_see_the_entirety_of_my_comment.js @@ -1,6 +1,6 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("I should see the entirety of my comment", () => { - cy.get("article.comment-card") - .should("not.contain", "show more") -}); +defineStep('I should see the entirety of my comment', () => { + cy.get('article.comment-card') + .should('not.contain', 'show more') +}) diff --git a/cypress/support/step_definitions/Post.Comment/I_type_in_a_comment_with_{int}_characters.js b/cypress/support/step_definitions/Post.Comment/I_type_in_a_comment_with_{int}_characters.js index ef39bdbf4..e7fcdc70e 100644 --- a/cypress/support/step_definitions/Post.Comment/I_type_in_a_comment_with_{int}_characters.js +++ b/cypress/support/step_definitions/Post.Comment/I_type_in_a_comment_with_{int}_characters.js @@ -1,9 +1,9 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When("I type in a comment with {int} characters", size => { - var c=""; +defineStep('I type in a comment with {int} characters', size => { + var c = '' for (var i = 0; i < size; i++) { - c += "c" + c += 'c' } - cy.get(".editor .ProseMirror").type(c); -}); + cy.get('.editor .ProseMirror').type(c) +}) diff --git a/cypress/support/step_definitions/Post.Comment/it_should_create_a_mention_in_the_CommentForm.js b/cypress/support/step_definitions/Post.Comment/it_should_create_a_mention_in_the_CommentForm.js index 0e52e0f7a..f30c2ccba 100644 --- a/cypress/support/step_definitions/Post.Comment/it_should_create_a_mention_in_the_CommentForm.js +++ b/cypress/support/step_definitions/Post.Comment/it_should_create_a_mention_in_the_CommentForm.js @@ -1,7 +1,7 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("it should create a mention in the CommentForm", () => { - cy.get(".ProseMirror a") +defineStep('it should create a mention in the CommentForm', () => { + cy.get('.ProseMirror a') .should('have.class', 'mention') .should('contain', '@peter-pan') }) diff --git a/cypress/support/step_definitions/Post.Comment/my_comment_should_be_successfully_created.js b/cypress/support/step_definitions/Post.Comment/my_comment_should_be_successfully_created.js index acb94f216..6edb116b8 100644 --- a/cypress/support/step_definitions/Post.Comment/my_comment_should_be_successfully_created.js +++ b/cypress/support/step_definitions/Post.Comment/my_comment_should_be_successfully_created.js @@ -1,5 +1,5 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("my comment should be successfully created", () => { - cy.get(".iziToast-message").contains("Comment submitted!"); -}); +defineStep('my comment should be successfully created', () => { + cy.get('.iziToast-message').contains('Comment submitted!') +}) diff --git a/cypress/support/step_definitions/Post.Comment/the_editor_should_be_cleared.js b/cypress/support/step_definitions/Post.Comment/the_editor_should_be_cleared.js index f6e47313a..5b3ae68e5 100644 --- a/cypress/support/step_definitions/Post.Comment/the_editor_should_be_cleared.js +++ b/cypress/support/step_definitions/Post.Comment/the_editor_should_be_cleared.js @@ -1,5 +1,5 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("the editor should be cleared", () => { - cy.get(".ProseMirror p").should("have.class", "is-empty"); -}); +defineStep('the editor should be cleared', () => { + cy.get('.ProseMirror p').should('have.class', 'is-empty') +}) diff --git a/cypress/support/step_definitions/Post.Create/I_choose_{string}_as_the_title.js b/cypress/support/step_definitions/Post.Create/I_choose_{string}_as_the_title.js index fc57b23a5..18018d357 100644 --- a/cypress/support/step_definitions/Post.Create/I_choose_{string}_as_the_title.js +++ b/cypress/support/step_definitions/Post.Create/I_choose_{string}_as_the_title.js @@ -1,8 +1,8 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When("I choose {string} as the title", title => { +defineStep('I choose {string} as the title', title => { const lastPost = {} - lastPost.title = title.replace("\n", " "); + lastPost.title = title.replace('\n', ' ') cy.task('pushValue', { name: 'lastPost', value: lastPost }) - cy.get('input[name="title"]').type(lastPost.title); -}); + cy.get('input[name="title"]').type(lastPost.title) +}) diff --git a/cypress/support/step_definitions/Post.Create/the_post_was_saved_successfully.js b/cypress/support/step_definitions/Post.Create/the_post_was_saved_successfully.js index 50e414650..4850ab432 100644 --- a/cypress/support/step_definitions/Post.Create/the_post_was_saved_successfully.js +++ b/cypress/support/step_definitions/Post.Create/the_post_was_saved_successfully.js @@ -1,8 +1,8 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("the post was saved successfully", () => { +defineStep('the post was saved successfully', () => { cy.task('getValue', 'lastPost').then(lastPost => { - cy.get(".base-card > .title").should("contain", lastPost.title); - cy.get(".content").should("contain", lastPost.content); + cy.get('.base-card > .title').should('contain', lastPost.title) + cy.get('.content').should('contain', lastPost.content) }) -}); +}) diff --git a/cypress/support/step_definitions/Post.Images/I_add_all_required_fields.js b/cypress/support/step_definitions/Post.Images/I_add_all_required_fields.js index ce2e88a83..e7bc94795 100644 --- a/cypress/support/step_definitions/Post.Images/I_add_all_required_fields.js +++ b/cypress/support/step_definitions/Post.Images/I_add_all_required_fields.js @@ -1,8 +1,8 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("I add all required fields", () => { +defineStep('I add all required fields', () => { cy.get('input[name="title"]') .type('new post') - .get(".editor .ProseMirror") + .get('.editor .ProseMirror') .type('new post content') - }) +}) diff --git a/cypress/support/step_definitions/Post.Images/I_should_be_able_to_{string}_a_teaser_image.js b/cypress/support/step_definitions/Post.Images/I_should_be_able_to_{string}_a_teaser_image.js index 478851f92..04cfeff3d 100644 --- a/cypress/support/step_definitions/Post.Images/I_should_be_able_to_{string}_a_teaser_image.js +++ b/cypress/support/step_definitions/Post.Images/I_should_be_able_to_{string}_a_teaser_image.js @@ -1,29 +1,28 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("I should be able to {string} a teaser image", condition => { - let postTeaserImage = "" +defineStep('I should be able to {string} a teaser image', condition => { + let postTeaserImage = '' switch(condition){ - case "change": - postTeaserImage = "humanconnection.png" - cy.get(".delete-image-button") + case 'change': + postTeaserImage = 'humanconnection.png' + cy.get('.delete-image-button') .click() - cy.get("#postdropzone").selectFile( - { contents: `cypress/fixtures/${postTeaserImage}`, fileName: postTeaserImage, mimeType: "image/png" }, - { action: "drag-drop", force: true } - ).wait(750); - break; - case "add": - postTeaserImage = "onourjourney.png" - cy.get("#postdropzone").selectFile( - { contents: `cypress/fixtures/${postTeaserImage}`, fileName: postTeaserImage, mimeType: "image/png" }, - { action: "drag-drop", force: true } - ).wait(750); - break; - case "remove": - cy.get(".delete-image-button") + cy.get('#postdropzone').selectFile( + { contents: `cypress/fixtures/${postTeaserImage}`, fileName: postTeaserImage, mimeType: 'image/png' }, + { action: 'drag-drop', force: true } + ).wait(750) + break + case 'add': + postTeaserImage = 'onourjourney.png' + cy.get('#postdropzone').selectFile( + { contents: `cypress/fixtures/${postTeaserImage}`, fileName: postTeaserImage, mimeType: 'image/png' }, + { action: 'drag-drop', force: true } + ).wait(750) + break + case 'remove': + cy.get('.delete-image-button') .click() - break; + break } - }) diff --git a/cypress/support/step_definitions/Post.Images/my_post_has_a_teaser_image.js b/cypress/support/step_definitions/Post.Images/my_post_has_a_teaser_image.js index b9ce4b3c7..7858b958c 100644 --- a/cypress/support/step_definitions/Post.Images/my_post_has_a_teaser_image.js +++ b/cypress/support/step_definitions/Post.Images/my_post_has_a_teaser_image.js @@ -1,6 +1,6 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When('my post has a teaser image', () => { +defineStep('my post has a teaser image', () => { cy.get('.contribution-form .image') .should('exist') .and('have.attr', 'src') diff --git a/cypress/support/step_definitions/Post.Images/the_first_image_should_not_be_displayed_anymore.js b/cypress/support/step_definitions/Post.Images/the_first_image_should_not_be_displayed_anymore.js index 6388f4458..f2188a28a 100644 --- a/cypress/support/step_definitions/Post.Images/the_first_image_should_not_be_displayed_anymore.js +++ b/cypress/support/step_definitions/Post.Images/the_first_image_should_not_be_displayed_anymore.js @@ -1,7 +1,7 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("the first image should not be displayed anymore", () => { - cy.get(".hero-image") +defineStep('the first image should not be displayed anymore', () => { + cy.get('.hero-image') .children() .get('.hero-image > .image') .should('have.length', 1) diff --git a/cypress/support/step_definitions/Post.Images/the_post_was_saved_successfully_with_the_{string}_teaser_image.js b/cypress/support/step_definitions/Post.Images/the_post_was_saved_successfully_with_the_{string}_teaser_image.js index c0571068e..fdfb1c84a 100644 --- a/cypress/support/step_definitions/Post.Images/the_post_was_saved_successfully_with_the_{string}_teaser_image.js +++ b/cypress/support/step_definitions/Post.Images/the_post_was_saved_successfully_with_the_{string}_teaser_image.js @@ -1,11 +1,11 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("the post was saved successfully with the {string} teaser image", condition => { - cy.get(".base-card > .title") - .should("contain", condition === 'updated' ? 'to be updated' : 'new post') - .get(".content") - .should("contain", condition === 'updated' ? 'successfully updated' : 'new post content') +defineStep('the post was saved successfully with the {string} teaser image', condition => { + cy.get('.base-card > .title') + .should('contain', condition === 'updated' ? 'to be updated' : 'new post') + .get('.content') + .should('contain', condition === 'updated' ? 'successfully updated' : 'new post content') .get('.post-page img') - .should("have.attr", "src") - .and("contains", condition === 'updated' ? 'humanconnection' : 'onourjourney') + .should('have.attr', 'src') + .and('contains', condition === 'updated' ? 'humanconnection' : 'onourjourney') }) diff --git a/cypress/support/step_definitions/Post.Images/the_{string}_post_was_saved_successfully_without_a_teaser_image.js b/cypress/support/step_definitions/Post.Images/the_{string}_post_was_saved_successfully_without_a_teaser_image.js index 40245df2b..39947d029 100644 --- a/cypress/support/step_definitions/Post.Images/the_{string}_post_was_saved_successfully_without_a_teaser_image.js +++ b/cypress/support/step_definitions/Post.Images/the_{string}_post_was_saved_successfully_without_a_teaser_image.js @@ -1,6 +1,6 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then('the {string} post was saved successfully without a teaser image', condition => { +defineStep('the {string} post was saved successfully without a teaser image', condition => { cy.get(".base-card > .title") .should("contain", condition === 'updated' ? 'to be updated' : 'new post') .get(".content") diff --git a/cypress/support/step_definitions/Post/the_post_shows_up_on_the_newsfeed_at_position_{int}.js b/cypress/support/step_definitions/Post/the_post_shows_up_on_the_newsfeed_at_position_{int}.js index f10896a33..59484591f 100644 --- a/cypress/support/step_definitions/Post/the_post_shows_up_on_the_newsfeed_at_position_{int}.js +++ b/cypress/support/step_definitions/Post/the_post_shows_up_on_the_newsfeed_at_position_{int}.js @@ -1,8 +1,7 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("the post shows up on the newsfeed at position {int}", index => { - const selector = `.post-teaser:nth-child(${index}) > .base-card`; - cy.get(selector).should("contain", 'previously created post'); - cy.get(selector).should("contain", 'with some content'); - -}); +defineStep('the post shows up on the newsfeed at position {int}', index => { + const selector = `.post-teaser:nth-child(${index}) > .base-card` + cy.get(selector).should('contain', 'previously created post') + cy.get(selector).should('contain', 'with some content') +}) diff --git a/cypress/support/step_definitions/Search/I_select_a_post_entry.js b/cypress/support/step_definitions/Search/I_select_a_post_entry.js index 26e673499..ddc7d0162 100644 --- a/cypress/support/step_definitions/Search/I_select_a_post_entry.js +++ b/cypress/support/step_definitions/Search/I_select_a_post_entry.js @@ -1,7 +1,7 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When("I select a post entry", () => { - cy.get(".searchable-input .search-post") +defineStep('I select a post entry', () => { + cy.get('.searchable-input .search-post') .first() - .trigger("click"); -}); + .trigger('click') +}) diff --git a/cypress/support/step_definitions/Search/I_select_a_user_entry.js b/cypress/support/step_definitions/Search/I_select_a_user_entry.js index 3d186ffd8..b3df870a9 100644 --- a/cypress/support/step_definitions/Search/I_select_a_user_entry.js +++ b/cypress/support/step_definitions/Search/I_select_a_user_entry.js @@ -1,7 +1,7 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("I select a user entry", () => { - cy.get(".searchable-input .user-teaser") +defineStep('I select a user entry', () => { + cy.get('.searchable-input .user-teaser') .first() - .trigger("click"); + .trigger('click') }) diff --git a/cypress/support/step_definitions/Search/I_should_have_one_item_in_the_select_dropdown.js b/cypress/support/step_definitions/Search/I_should_have_one_item_in_the_select_dropdown.js index 148bb8195..cc52f5985 100644 --- a/cypress/support/step_definitions/Search/I_should_have_one_item_in_the_select_dropdown.js +++ b/cypress/support/step_definitions/Search/I_should_have_one_item_in_the_select_dropdown.js @@ -1,7 +1,7 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("I should have one item in the select dropdown", () => { - cy.get(".searchable-input .ds-select-dropdown").should($li => { - expect($li).to.have.length(1); - }); -}); +defineStep('I should have one item in the select dropdown', () => { + cy.get('.searchable-input .ds-select-dropdown').should($li => { + expect($li).to.have.length(1) + }) +}) diff --git a/cypress/support/step_definitions/Search/I_should_not_see_posts_without_the_searched-for_term_in_the_select_dropdown.js b/cypress/support/step_definitions/Search/I_should_not_see_posts_without_the_searched-for_term_in_the_select_dropdown.js index d2a2bc1df..1e8082d40 100644 --- a/cypress/support/step_definitions/Search/I_should_not_see_posts_without_the_searched-for_term_in_the_select_dropdown.js +++ b/cypress/support/step_definitions/Search/I_should_not_see_posts_without_the_searched-for_term_in_the_select_dropdown.js @@ -1,6 +1,6 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("I should not see posts without the searched-for term in the select dropdown", () => { - cy.get(".ds-select-dropdown") - .should("not.contain","No searched for content"); -}); +defineStep('I should not see posts without the searched-for term in the select dropdown', () => { + cy.get('.ds-select-dropdown') + .should('not.contain','No searched for content') +}) diff --git a/cypress/support/step_definitions/Search/I_should_see_posts_with_the_searched-for_term_in_the_select_dropdown.js b/cypress/support/step_definitions/Search/I_should_see_posts_with_the_searched-for_term_in_the_select_dropdown.js index 41c132dea..23d2c141f 100644 --- a/cypress/support/step_definitions/Search/I_should_see_posts_with_the_searched-for_term_in_the_select_dropdown.js +++ b/cypress/support/step_definitions/Search/I_should_see_posts_with_the_searched-for_term_in_the_select_dropdown.js @@ -1,6 +1,6 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("I should see posts with the searched-for term in the select dropdown", () => { - cy.get(".ds-select-dropdown") - .should("contain","101 Essays that will change the way you think"); -}); +defineStep('I should see posts with the searched-for term in the select dropdown', () => { + cy.get('.ds-select-dropdown') + .should('contain','101 Essays that will change the way you think') +}) diff --git a/cypress/support/step_definitions/Search/I_should_see_the_following_posts_on_the_search_results_page.js b/cypress/support/step_definitions/Search/I_should_see_the_following_posts_on_the_search_results_page.js index 3e5da72f7..57f41bccb 100644 --- a/cypress/support/step_definitions/Search/I_should_see_the_following_posts_on_the_search_results_page.js +++ b/cypress/support/step_definitions/Search/I_should_see_the_following_posts_on_the_search_results_page.js @@ -1,8 +1,8 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("I should see the following posts on the search results page:", table => { +defineStep('I should see the following posts on the search results page:', table => { table.hashes().forEach(({ title }) => { - cy.get(".post-teaser") - .should("contain",title) - }); -}); + cy.get('.post-teaser') + .should('contain',title) + }) +}) diff --git a/cypress/support/step_definitions/Search/I_should_see_the_following_users_in_the_select_dropdown.js b/cypress/support/step_definitions/Search/I_should_see_the_following_users_in_the_select_dropdown.js index 830e19a8d..f4b89de0c 100644 --- a/cypress/support/step_definitions/Search/I_should_see_the_following_users_in_the_select_dropdown.js +++ b/cypress/support/step_definitions/Search/I_should_see_the_following_users_in_the_select_dropdown.js @@ -1,8 +1,8 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("I should see the following users in the select dropdown:", table => { - cy.get(".search-heading").should("contain", "Users"); +defineStep('I should see the following users in the select dropdown:', table => { + cy.get('.search-heading').should('contain', 'Users') table.hashes().forEach(({ slug }) => { - cy.get(".ds-select-dropdown").should("contain", slug); - }); -}); + cy.get('.ds-select-dropdown').should('contain', slug) + }) +}) diff --git a/cypress/support/step_definitions/Search/I_type_{string}_and_press_Enter.js b/cypress/support/step_definitions/Search/I_type_{string}_and_press_Enter.js index 796820ba0..9a94cf756 100644 --- a/cypress/support/step_definitions/Search/I_type_{string}_and_press_Enter.js +++ b/cypress/support/step_definitions/Search/I_type_{string}_and_press_Enter.js @@ -1,8 +1,8 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When("I type {string} and press Enter", value => { - cy.get(".searchable-input .ds-select input") +defineStep('I type {string} and press Enter', value => { + cy.get('.searchable-input .ds-select input') .focus() .type(value) - .type("{enter}", { force: true }); -}); + .type('{enter}', { force: true }) +}) diff --git a/cypress/support/step_definitions/Search/I_type_{string}_and_press_escape.js b/cypress/support/step_definitions/Search/I_type_{string}_and_press_escape.js index 3e2e67be8..e393ba227 100644 --- a/cypress/support/step_definitions/Search/I_type_{string}_and_press_escape.js +++ b/cypress/support/step_definitions/Search/I_type_{string}_and_press_escape.js @@ -1,8 +1,8 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When("I type {string} and press escape", value => { - cy.get(".searchable-input .ds-select input") +defineStep('I type {string} and press escape', value => { + cy.get('.searchable-input .ds-select input') .focus() .type(value) - .type("{esc}"); -}); + .type('{esc}') +}) diff --git a/cypress/support/step_definitions/Search/the_search_field_should_clear.js b/cypress/support/step_definitions/Search/the_search_field_should_clear.js index 10f73959f..1c31bbf99 100644 --- a/cypress/support/step_definitions/Search/the_search_field_should_clear.js +++ b/cypress/support/step_definitions/Search/the_search_field_should_clear.js @@ -1,6 +1,6 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("the search field should clear", () => { - cy.get(".searchable-input .ds-select input") - .should("have.text", ""); -}); +defineStep('the search field should clear', () => { + cy.get('.searchable-input .ds-select input') + .should('have.text', '') +}) diff --git a/cypress/support/step_definitions/Search/the_search_parameter_equals_{string}.js b/cypress/support/step_definitions/Search/the_search_parameter_equals_{string}.js index 0f433cf1f..552dd5738 100644 --- a/cypress/support/step_definitions/Search/the_search_parameter_equals_{string}.js +++ b/cypress/support/step_definitions/Search/the_search_parameter_equals_{string}.js @@ -1,6 +1,6 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("the search parameter equals {string}", search => { - cy.location("search") - .should("eq", search); -}); +defineStep('the search parameter equals {string}', search => { + cy.location('search') + .should('eq', search) +}) diff --git a/cypress/support/step_definitions/User.Authentication/I_am_logged_in_with_username_{string}.js b/cypress/support/step_definitions/User.Authentication/I_am_logged_in_with_username_{string}.js index d4af04ff6..04ca3e59b 100644 --- a/cypress/support/step_definitions/User.Authentication/I_am_logged_in_with_username_{string}.js +++ b/cypress/support/step_definitions/User.Authentication/I_am_logged_in_with_username_{string}.js @@ -1,7 +1,7 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("I am logged in with username {string}", name => { - cy.get(".avatar-menu").click(); - cy.get(".avatar-menu-popover").contains(name); - cy.get(".avatar-menu").click(); // Close menu again -}); +defineStep('I am logged in with username {string}', name => { + cy.get('.avatar-menu').click() + cy.get('.avatar-menu-popover').contains(name) + cy.get('.avatar-menu').click() // Close menu again +}) diff --git a/cypress/support/step_definitions/User.Block/I_block_the_user_{string}.js b/cypress/support/step_definitions/User.Block/I_block_the_user_{string}.js index be82f00d9..8fe506919 100644 --- a/cypress/support/step_definitions/User.Block/I_block_the_user_{string}.js +++ b/cypress/support/step_definitions/User.Block/I_block_the_user_{string}.js @@ -1,11 +1,11 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When("I block the user {string}", name => { +defineStep('I block the user {string}', name => { cy.neode() - .firstOf("User", { name }) + .firstOf('User', { name }) .then(blockedUser => { cy.neode() - .firstOf("User", {id: "id-of-peter-pan"}) - .relateTo(blockedUser, "blocked"); - }); -}); + .firstOf('User', {id: 'id-of-peter-pan'}) + .relateTo(blockedUser, 'blocked') + }) +}) diff --git a/cypress/support/step_definitions/User.Block/I_should_not_see_{string}_button.js b/cypress/support/step_definitions/User.Block/I_should_not_see_{string}_button.js index 791a5aaaf..ae47405f3 100644 --- a/cypress/support/step_definitions/User.Block/I_should_not_see_{string}_button.js +++ b/cypress/support/step_definitions/User.Block/I_should_not_see_{string}_button.js @@ -1,6 +1,6 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then('I should not see {string} button', button => { +defineStep('I should not see {string} button', button => { cy.get('.base-card .action-buttons') .should('have.length', 1) }) diff --git a/cypress/support/step_definitions/User.Block/I_should_see_no_users_in_my_blocked_users_list.js b/cypress/support/step_definitions/User.Block/I_should_see_no_users_in_my_blocked_users_list.js index 3e4813fbc..702e07df4 100644 --- a/cypress/support/step_definitions/User.Block/I_should_see_no_users_in_my_blocked_users_list.js +++ b/cypress/support/step_definitions/User.Block/I_should_see_no_users_in_my_blocked_users_list.js @@ -1,6 +1,6 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("I should see no users in my blocked users list", () => { +defineStep('I should see no users in my blocked users list', () => { cy.get('.ds-placeholder') - .should('contain', "So far, you have not blocked anybody.") + .should('contain', 'So far, you have not blocked anybody.') }) diff --git a/cypress/support/step_definitions/User.Block/I_should_see_the_{string}_button.js b/cypress/support/step_definitions/User.Block/I_should_see_the_{string}_button.js index 7e6b7eacb..a6e014130 100644 --- a/cypress/support/step_definitions/User.Block/I_should_see_the_{string}_button.js +++ b/cypress/support/step_definitions/User.Block/I_should_see_the_{string}_button.js @@ -1,6 +1,6 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then('I should see the {string} button', button => { +defineStep('I should see the {string} button', button => { cy.get('.base-card .action-buttons .base-button') .should('contain', button) }) diff --git a/cypress/support/step_definitions/User.Block/I_{string}_see_{string}_from_the_content_menu_in_the_user_info_box.js b/cypress/support/step_definitions/User.Block/I_{string}_see_{string}_from_the_content_menu_in_the_user_info_box.js index bcfd9bd7a..fa568efeb 100644 --- a/cypress/support/step_definitions/User.Block/I_{string}_see_{string}_from_the_content_menu_in_the_user_info_box.js +++ b/cypress/support/step_definitions/User.Block/I_{string}_see_{string}_from_the_content_menu_in_the_user_info_box.js @@ -1,7 +1,7 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("I {string} see {string} from the content menu in the user info box", (condition, link) => { - cy.get(".user-content-menu .base-button").click() - cy.get(".popover .ds-menu-item-link") +defineStep('I {string} see {string} from the content menu in the user info box', (condition, link) => { + cy.get('.user-content-menu .base-button').click() + cy.get('.popover .ds-menu-item-link') .should(condition === 'should' ? 'contain' : 'not.contain', link) }) diff --git a/cypress/support/step_definitions/User.Block/a_user_has_blocked_me.js b/cypress/support/step_definitions/User.Block/a_user_has_blocked_me.js index 13b247ccf..a53d6b3c0 100644 --- a/cypress/support/step_definitions/User.Block/a_user_has_blocked_me.js +++ b/cypress/support/step_definitions/User.Block/a_user_has_blocked_me.js @@ -1,15 +1,15 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When("a user has blocked me", () => { +defineStep('a user has blocked me', () => { cy.neode() - .firstOf("User", { - name: "Peter Pan" + .firstOf('User', { + name: 'Peter Pan' }) .then(blockedUser => { cy.neode() - .firstOf("User", { + .firstOf('User', { name: 'Harassing User' }) - .relateTo(blockedUser, "blocked"); - }); -}); + .relateTo(blockedUser, 'blocked') + }) +}) diff --git a/cypress/support/step_definitions/User.Block/they_should_not_see_the_comment_form.js b/cypress/support/step_definitions/User.Block/they_should_not_see_the_comment_form.js index 34aa86aaf..b9dff833d 100644 --- a/cypress/support/step_definitions/User.Block/they_should_not_see_the_comment_form.js +++ b/cypress/support/step_definitions/User.Block/they_should_not_see_the_comment_form.js @@ -1,5 +1,5 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("they should not see the comment form", () => { - cy.get(".base-card").children().should('not.have.class', 'comment-form') +defineStep('they should not see the comment form', () => { + cy.get('.base-card').children().should('not.have.class', 'comment-form') }) diff --git a/cypress/support/step_definitions/User.Block/they_should_see_a_text_explaining_why_commenting_is_not_possible.js b/cypress/support/step_definitions/User.Block/they_should_see_a_text_explaining_why_commenting_is_not_possible.js index 64f0f0fd1..0f282e8fd 100644 --- a/cypress/support/step_definitions/User.Block/they_should_see_a_text_explaining_why_commenting_is_not_possible.js +++ b/cypress/support/step_definitions/User.Block/they_should_see_a_text_explaining_why_commenting_is_not_possible.js @@ -1,5 +1,5 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("they should see a text explaining why commenting is not possible", () => { - cy.get('.ds-placeholder').should('contain', "Commenting is not possible at this time on this post.") +defineStep('they should see a text explaining why commenting is not possible', () => { + cy.get('.ds-placeholder').should('contain', 'Commenting is not possible at this time on this post.') }) diff --git a/cypress/support/step_definitions/User.Mute/I_mute_the_user_{string}.js b/cypress/support/step_definitions/User.Mute/I_mute_the_user_{string}.js index 7b52ca373..b05401d7a 100644 --- a/cypress/support/step_definitions/User.Mute/I_mute_the_user_{string}.js +++ b/cypress/support/step_definitions/User.Mute/I_mute_the_user_{string}.js @@ -1,13 +1,13 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When("I mute the user {string}", name => { +defineStep('I mute the user {string}', name => { cy.neode() - .firstOf("User", { name }) + .firstOf('User', { name }) .then(mutedUser => { cy.neode() - .firstOf("User", { - name: "Peter Pan" + .firstOf('User', { + name: 'Peter Pan' }) - .relateTo(mutedUser, "muted"); - }); -}); + .relateTo(mutedUser, 'muted') + }) +}) diff --git a/cypress/support/step_definitions/User.Mute/the_list_of_posts_of_this_user_is_empty.js b/cypress/support/step_definitions/User.Mute/the_list_of_posts_of_this_user_is_empty.js index 66ac3bdb8..7a2f3d7df 100644 --- a/cypress/support/step_definitions/User.Mute/the_list_of_posts_of_this_user_is_empty.js +++ b/cypress/support/step_definitions/User.Mute/the_list_of_posts_of_this_user_is_empty.js @@ -1,6 +1,6 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("the list of posts of this user is empty", () => { - cy.get(".base-card").not(".post-link"); - cy.get(".main-container").find(".ds-space.hc-empty"); -}); +defineStep('the list of posts of this user is empty', () => { + cy.get('.base-card').not('.post-link') + cy.get('.main-container').find('.ds-space.hc-empty') +}) diff --git a/cypress/support/step_definitions/User.Mute/the_search_should_contain_the_annoying_user.js b/cypress/support/step_definitions/User.Mute/the_search_should_contain_the_annoying_user.js index 7d47c48aa..e47f6f5ac 100644 --- a/cypress/support/step_definitions/User.Mute/the_search_should_contain_the_annoying_user.js +++ b/cypress/support/step_definitions/User.Mute/the_search_should_contain_the_annoying_user.js @@ -1,13 +1,13 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("the search should contain the annoying user", () => { - cy.get(".searchable-input .ds-select-dropdown") +defineStep('the search should contain the annoying user', () => { + cy.get('.searchable-input .ds-select-dropdown') .should($li => { - expect($li).to.have.length(1); + expect($li).to.have.length(1) }) - cy.get(".ds-select-dropdown .user-teaser .slug") - .should("contain", '@annoying-user'); - cy.get(".searchable-input .ds-select input") + cy.get('.ds-select-dropdown .user-teaser .slug') + .should('contain', '@annoying-user') + cy.get('.searchable-input .ds-select input') .focus() - .type("{esc}"); + .type('{esc}') }) diff --git a/cypress/support/step_definitions/User.Mute/the_search_should_not_contain_posts_by_the_annoying_user.js b/cypress/support/step_definitions/User.Mute/the_search_should_not_contain_posts_by_the_annoying_user.js index 1dad99678..cdf29fc7d 100644 --- a/cypress/support/step_definitions/User.Mute/the_search_should_not_contain_posts_by_the_annoying_user.js +++ b/cypress/support/step_definitions/User.Mute/the_search_should_not_contain_posts_by_the_annoying_user.js @@ -1,10 +1,10 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("the search should not contain posts by the annoying user", () => { - cy.get(".searchable-input .ds-select-dropdown").should($li => { - expect($li).to.have.length(1); +defineStep('the search should not contain posts by the annoying user', () => { + cy.get('.searchable-input .ds-select-dropdown').should($li => { + expect($li).to.have.length(1) }) - cy.get(".ds-select-dropdown") - .should("not.have.class", '.search-post') - .should("not.contain", 'Spam') -}); + cy.get('.ds-select-dropdown') + .should('not.have.class', '.search-post') + .should('not.contain', 'Spam') +}) diff --git a/cypress/support/step_definitions/User.SettingNotifications/I_click_on_element_with_ID_{string}.js b/cypress/support/step_definitions/User.SettingNotifications/I_click_on_element_with_ID_{string}.js index 7bdb20e5d..90bc73a01 100644 --- a/cypress/support/step_definitions/User.SettingNotifications/I_click_on_element_with_ID_{string}.js +++ b/cypress/support/step_definitions/User.SettingNotifications/I_click_on_element_with_ID_{string}.js @@ -1,5 +1,5 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When("I click on element with ID {string}", (id) => { +defineStep('I click on element with ID {string}', (id) => { cy.get('#' + id).click() }) diff --git a/cypress/support/step_definitions/User.SettingNotifications/I_click_save.js b/cypress/support/step_definitions/User.SettingNotifications/I_click_save.js index 9412d7912..40a37dc9e 100644 --- a/cypress/support/step_definitions/User.SettingNotifications/I_click_save.js +++ b/cypress/support/step_definitions/User.SettingNotifications/I_click_save.js @@ -1,5 +1,5 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("I click save", () => { - cy.get(".save-button").click() +defineStep('I click save', () => { + cy.get('.save-button').click() }) diff --git a/cypress/support/step_definitions/UserProfile.Avatar/I_cannot_upload_a_picture.js b/cypress/support/step_definitions/UserProfile.Avatar/I_cannot_upload_a_picture.js index 9e44b55ba..792c6462c 100644 --- a/cypress/support/step_definitions/UserProfile.Avatar/I_cannot_upload_a_picture.js +++ b/cypress/support/step_definitions/UserProfile.Avatar/I_cannot_upload_a_picture.js @@ -1,8 +1,8 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("I cannot upload a picture", () => { - cy.get(".base-card") +defineStep('I cannot upload a picture', () => { + cy.get('.base-card') .children() - .should("not.have.id", "customdropzone") - .should("have.class", "profile-avatar"); -}); + .should('not.have.id', 'customdropzone') + .should('have.class', 'profile-avatar') +}) diff --git a/cypress/support/step_definitions/UserProfile.Avatar/I_should_be_able_to_change_my_profile_picture.js b/cypress/support/step_definitions/UserProfile.Avatar/I_should_be_able_to_change_my_profile_picture.js index b1b2401e2..3a175c3fe 100644 --- a/cypress/support/step_definitions/UserProfile.Avatar/I_should_be_able_to_change_my_profile_picture.js +++ b/cypress/support/step_definitions/UserProfile.Avatar/I_should_be_able_to_change_my_profile_picture.js @@ -1,15 +1,15 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("I should be able to change my profile picture", () => { - const avatarUpload = "onourjourney.png"; +defineStep('I should be able to change my profile picture', () => { + const avatarUpload = 'onourjourney.png' - cy.get("#customdropzone").selectFile( - { contents: `cypress/fixtures/${avatarUpload}`, fileName: avatarUpload, mimeType: "image/png" }, - { action: "drag-drop" } - ); - cy.get(".profile-page-avatar img") - .should("have.attr", "src") - .and("contains", "onourjourney"); - cy.contains(".iziToast-message", "Upload successful") - .should("have.length",1); -}); + cy.get('#customdropzone').selectFile( + { contents: `cypress/fixtures/${avatarUpload}`, fileName: avatarUpload, mimeType: 'image/png' }, + { action: 'drag-drop' } + ) + cy.get('.profile-page-avatar img') + .should('have.attr', 'src') + .and('contains', 'onourjourney') + cy.contains('.iziToast-message', 'Upload successful') + .should('have.length',1) +}) diff --git a/cypress/support/step_definitions/UserProfile.ChangePassword/I_can_login_successfully.js b/cypress/support/step_definitions/UserProfile.ChangePassword/I_can_login_successfully.js index 1349b5eb9..ad4fc6076 100644 --- a/cypress/support/step_definitions/UserProfile.ChangePassword/I_can_login_successfully.js +++ b/cypress/support/step_definitions/UserProfile.ChangePassword/I_can_login_successfully.js @@ -1,7 +1,6 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("I can login successfully", () => { - // cy.reload(); - cy.get(".iziToast-wrapper") - .should("contain", "You are logged in!"); -}); +defineStep('I can login successfully', () => { + cy.get('.iziToast-wrapper') + .should('contain', 'You are logged in!') +}) diff --git a/cypress/support/step_definitions/UserProfile.ChangePassword/I_cannot_login_anymore.js b/cypress/support/step_definitions/UserProfile.ChangePassword/I_cannot_login_anymore.js index f6159c79b..544752b1a 100644 --- a/cypress/support/step_definitions/UserProfile.ChangePassword/I_cannot_login_anymore.js +++ b/cypress/support/step_definitions/UserProfile.ChangePassword/I_cannot_login_anymore.js @@ -1,7 +1,6 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("I cannot login anymore", password => { - //cy.reload(); - cy.get(".iziToast-wrapper") - .should("contain", "Incorrect email address or password."); -}); +defineStep('I cannot login anymore', password => { + cy.get('.iziToast-wrapper') + .should('contain', 'Incorrect email address or password.') +}) diff --git a/cypress/support/step_definitions/UserProfile.ChangePassword/I_cannot_submit_the_form.js b/cypress/support/step_definitions/UserProfile.ChangePassword/I_cannot_submit_the_form.js index 02a2c7d83..643b44e20 100644 --- a/cypress/support/step_definitions/UserProfile.ChangePassword/I_cannot_submit_the_form.js +++ b/cypress/support/step_definitions/UserProfile.ChangePassword/I_cannot_submit_the_form.js @@ -1,6 +1,6 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When("I cannot submit the form", () => { - cy.get("button[type=submit]") - .should('be.disabled'); -}); +defineStep('I cannot submit the form', () => { + cy.get('button[type=submit]') + .should('be.disabled') +}) diff --git a/cypress/support/step_definitions/UserProfile.ChangePassword/I_fill_the_password_form_with.js b/cypress/support/step_definitions/UserProfile.ChangePassword/I_fill_the_password_form_with.js index af0c6639b..e430f6af6 100644 --- a/cypress/support/step_definitions/UserProfile.ChangePassword/I_fill_the_password_form_with.js +++ b/cypress/support/step_definitions/UserProfile.ChangePassword/I_fill_the_password_form_with.js @@ -1,11 +1,11 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When("I fill the password form with:", table => { - table = table.rowsHash(); - cy.get("input[id=oldPassword]") - .type(table["Your old password"]) - .get("input[id=password]") - .type(table["Your new password"]) - .get("input[id=passwordConfirmation]") - .type(table["Confirm new password"]); -}); +defineStep('I fill the password form with:', table => { + table = table.rowsHash() + cy.get('input[id=oldPassword]') + .type(table['Your old password']) + .get('input[id=password]') + .type(table['Your new password']) + .get('input[id=passwordConfirmation]') + .type(table['Confirm new password']) +}) diff --git a/cypress/support/step_definitions/UserProfile.ChangePassword/I_submit_the_form.js b/cypress/support/step_definitions/UserProfile.ChangePassword/I_submit_the_form.js index 8b17f6de1..268615560 100644 --- a/cypress/support/step_definitions/UserProfile.ChangePassword/I_submit_the_form.js +++ b/cypress/support/step_definitions/UserProfile.ChangePassword/I_submit_the_form.js @@ -1,5 +1,5 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When("I submit the form", () => { - cy.get("form").submit(); -}); +defineStep('I submit the form', () => { + cy.get('form').submit() +}) diff --git a/cypress/support/step_definitions/UserProfile.NameDescriptionLocation/I_can_see_my_new_name_{string}_when_I_click_on_my_profile_picture_in_the_top_right.js b/cypress/support/step_definitions/UserProfile.NameDescriptionLocation/I_can_see_my_new_name_{string}_when_I_click_on_my_profile_picture_in_the_top_right.js index c5dd84bf0..f63508ce8 100644 --- a/cypress/support/step_definitions/UserProfile.NameDescriptionLocation/I_can_see_my_new_name_{string}_when_I_click_on_my_profile_picture_in_the_top_right.js +++ b/cypress/support/step_definitions/UserProfile.NameDescriptionLocation/I_can_see_my_new_name_{string}_when_I_click_on_my_profile_picture_in_the_top_right.js @@ -1,10 +1,10 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then('I can see my new name {string} when I click on my profile picture in the top right', name => { +defineStep('I can see my new name {string} when I click on my profile picture in the top right', name => { cy.get(".avatar-menu").then(($menu) => { if (!$menu.is(':visible')){ - cy.scrollTo("top"); - cy.wait(500); + cy.scrollTo("top") + cy.wait(500) } }) cy.get('.avatar-menu').click() // open diff --git a/cypress/support/step_definitions/UserProfile.NameDescriptionLocation/I_have_the_following_self-description.js b/cypress/support/step_definitions/UserProfile.NameDescriptionLocation/I_have_the_following_self-description.js index f0f6ba4da..950f320ef 100644 --- a/cypress/support/step_definitions/UserProfile.NameDescriptionLocation/I_have_the_following_self-description.js +++ b/cypress/support/step_definitions/UserProfile.NameDescriptionLocation/I_have_the_following_self-description.js @@ -1,6 +1,6 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When('I have the following self-description:', text => { +defineStep('I have the following self-description:', text => { cy.get('textarea[id=about]') .clear() .type(text) diff --git a/cypress/support/step_definitions/UserProfile.NameDescriptionLocation/I_save_{string}_as_my_location.js b/cypress/support/step_definitions/UserProfile.NameDescriptionLocation/I_save_{string}_as_my_location.js index 00d5141f8..2e708564c 100644 --- a/cypress/support/step_definitions/UserProfile.NameDescriptionLocation/I_save_{string}_as_my_location.js +++ b/cypress/support/step_definitions/UserProfile.NameDescriptionLocation/I_save_{string}_as_my_location.js @@ -1,6 +1,6 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When('I save {string} as my location', location => { +defineStep('I save {string} as my location', location => { cy.get('input[id=city]').type(location) cy.get('.ds-select-option') .contains(location) diff --git a/cypress/support/step_definitions/UserProfile.NameDescriptionLocation/I_save_{string}_as_my_new_name.js b/cypress/support/step_definitions/UserProfile.NameDescriptionLocation/I_save_{string}_as_my_new_name.js index b94683a5b..487458b26 100644 --- a/cypress/support/step_definitions/UserProfile.NameDescriptionLocation/I_save_{string}_as_my_new_name.js +++ b/cypress/support/step_definitions/UserProfile.NameDescriptionLocation/I_save_{string}_as_my_new_name.js @@ -1,6 +1,6 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When('I save {string} as my new name', name => { +defineStep('I save {string} as my new name', name => { cy.get('input[id=name]') .clear() .type(name) diff --git a/cypress/support/step_definitions/UserProfile.NameDescriptionLocation/they_can_see_the_following_text_in_the_info_box_below_my_avatar.js b/cypress/support/step_definitions/UserProfile.NameDescriptionLocation/they_can_see_the_following_text_in_the_info_box_below_my_avatar.js index d416c8d10..823aec202 100644 --- a/cypress/support/step_definitions/UserProfile.NameDescriptionLocation/they_can_see_the_following_text_in_the_info_box_below_my_avatar.js +++ b/cypress/support/step_definitions/UserProfile.NameDescriptionLocation/they_can_see_the_following_text_in_the_info_box_below_my_avatar.js @@ -1,5 +1,5 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When('they can see the following text in the info box below my avatar:', text => { +defineStep('they can see the following text in the info box below my avatar:', text => { cy.contains(text) }) diff --git a/cypress/support/step_definitions/UserProfile.NameDescriptionLocation/they_can_see_{string}_in_the_info_box_below_my_avatar.js b/cypress/support/step_definitions/UserProfile.NameDescriptionLocation/they_can_see_{string}_in_the_info_box_below_my_avatar.js index ea8cf2158..58aa2b3c0 100644 --- a/cypress/support/step_definitions/UserProfile.NameDescriptionLocation/they_can_see_{string}_in_the_info_box_below_my_avatar.js +++ b/cypress/support/step_definitions/UserProfile.NameDescriptionLocation/they_can_see_{string}_in_the_info_box_below_my_avatar.js @@ -1,5 +1,5 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then('they can see {string} in the info box below my avatar', location => { +defineStep('they can see {string} in the info box below my avatar', location => { cy.contains(location) }) diff --git a/cypress/support/step_definitions/UserProfile.SocialMedia/I_add_a_social_media_link.js b/cypress/support/step_definitions/UserProfile.SocialMedia/I_add_a_social_media_link.js index eab8ba0d6..d1100e035 100644 --- a/cypress/support/step_definitions/UserProfile.SocialMedia/I_add_a_social_media_link.js +++ b/cypress/support/step_definitions/UserProfile.SocialMedia/I_add_a_social_media_link.js @@ -1,6 +1,6 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When('I add a social media link', () => { +defineStep('I add a social media link', () => { cy.get('[data-test="add-save-button"]') .click() .get('#editSocialMedia') diff --git a/cypress/support/step_definitions/UserProfile.SocialMedia/I_can_cancel_editing.js b/cypress/support/step_definitions/UserProfile.SocialMedia/I_can_cancel_editing.js index 72d83c9a3..9a17cee7f 100644 --- a/cypress/support/step_definitions/UserProfile.SocialMedia/I_can_cancel_editing.js +++ b/cypress/support/step_definitions/UserProfile.SocialMedia/I_can_cancel_editing.js @@ -1,6 +1,6 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then('I can cancel editing', () => { +defineStep('I can cancel editing', () => { cy.get('button#cancel') .click() .get('input#editSocialMedia') diff --git a/cypress/support/step_definitions/UserProfile.SocialMedia/I_delete_a_social_media_link.js b/cypress/support/step_definitions/UserProfile.SocialMedia/I_delete_a_social_media_link.js index 1f0f3e22e..4adf6d0d6 100644 --- a/cypress/support/step_definitions/UserProfile.SocialMedia/I_delete_a_social_media_link.js +++ b/cypress/support/step_definitions/UserProfile.SocialMedia/I_delete_a_social_media_link.js @@ -1,6 +1,6 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When('I delete a social media link', () => { +defineStep('I delete a social media link', () => { cy.get(".base-button[title='Delete']") .click() }) diff --git a/cypress/support/step_definitions/UserProfile.SocialMedia/I_delete_the_social_media_link_{string}.js b/cypress/support/step_definitions/UserProfile.SocialMedia/I_delete_the_social_media_link_{string}.js index 3acba4756..3c3e4286e 100644 --- a/cypress/support/step_definitions/UserProfile.SocialMedia/I_delete_the_social_media_link_{string}.js +++ b/cypress/support/step_definitions/UserProfile.SocialMedia/I_delete_the_social_media_link_{string}.js @@ -1,6 +1,6 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When('I delete the social media link {string}', (link) => { +defineStep('I delete the social media link {string}', (link) => { cy.get('[data-test="delete-button"]') .click() cy.get('[data-test="confirm-modal"]') diff --git a/cypress/support/step_definitions/UserProfile.SocialMedia/I_edit_and_save_the_link.js b/cypress/support/step_definitions/UserProfile.SocialMedia/I_edit_and_save_the_link.js index 3cec61688..d1b11e322 100644 --- a/cypress/support/step_definitions/UserProfile.SocialMedia/I_edit_and_save_the_link.js +++ b/cypress/support/step_definitions/UserProfile.SocialMedia/I_edit_and_save_the_link.js @@ -1,6 +1,6 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When('I edit and save the link', () => { +defineStep('I edit and save the link', () => { cy.get('input#editSocialMedia') .clear() .type('https://freeradical.zone/tinkerbell') diff --git a/cypress/support/step_definitions/UserProfile.SocialMedia/I_have_added_a_social_media_link.js b/cypress/support/step_definitions/UserProfile.SocialMedia/I_have_added_a_social_media_link.js index b1eb698de..9567ab8a6 100644 --- a/cypress/support/step_definitions/UserProfile.SocialMedia/I_have_added_a_social_media_link.js +++ b/cypress/support/step_definitions/UserProfile.SocialMedia/I_have_added_a_social_media_link.js @@ -1,6 +1,6 @@ -import { Given } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Given('I have added a social media link', () => { +defineStep('I have added a social media link', () => { cy.visit('/settings/my-social-media') .get('button') .contains('Add link') diff --git a/cypress/support/step_definitions/UserProfile.SocialMedia/I_have_added_the_social_media_link_{string}.js b/cypress/support/step_definitions/UserProfile.SocialMedia/I_have_added_the_social_media_link_{string}.js index d0daab843..709f45831 100644 --- a/cypress/support/step_definitions/UserProfile.SocialMedia/I_have_added_the_social_media_link_{string}.js +++ b/cypress/support/step_definitions/UserProfile.SocialMedia/I_have_added_the_social_media_link_{string}.js @@ -1,6 +1,6 @@ -import { Given } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Given('I have added the social media link {string}', (link) => { +defineStep('I have added the social media link {string}', (link) => { cy.visit('/settings/my-social-media') .get('[data-test="add-save-button"]') .click() diff --git a/cypress/support/step_definitions/UserProfile.SocialMedia/I_start_editing_a_social_media_link.js b/cypress/support/step_definitions/UserProfile.SocialMedia/I_start_editing_a_social_media_link.js index 11ee84084..0d533e152 100644 --- a/cypress/support/step_definitions/UserProfile.SocialMedia/I_start_editing_a_social_media_link.js +++ b/cypress/support/step_definitions/UserProfile.SocialMedia/I_start_editing_a_social_media_link.js @@ -1,6 +1,6 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When('I start editing a social media link', () => { +defineStep('I start editing a social media link', () => { cy.get('[data-test="edit-button"]') .click() }) diff --git a/cypress/support/step_definitions/UserProfile.SocialMedia/the_new_social_media_link_shows_up_on_the_page.js b/cypress/support/step_definitions/UserProfile.SocialMedia/the_new_social_media_link_shows_up_on_the_page.js index fd1bf9350..76a921ca2 100644 --- a/cypress/support/step_definitions/UserProfile.SocialMedia/the_new_social_media_link_shows_up_on_the_page.js +++ b/cypress/support/step_definitions/UserProfile.SocialMedia/the_new_social_media_link_shows_up_on_the_page.js @@ -1,6 +1,6 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then('the new social media link shows up on the page', () => { +defineStep('the new social media link shows up on the page', () => { cy.get('a[href="https://freeradical.zone/peter-pan"]') .should('have.length', 1) }) diff --git a/cypress/support/step_definitions/UserProfile.SocialMedia/the_new_url_is_displayed.js b/cypress/support/step_definitions/UserProfile.SocialMedia/the_new_url_is_displayed.js index c9c7dd906..576d6d3a9 100644 --- a/cypress/support/step_definitions/UserProfile.SocialMedia/the_new_url_is_displayed.js +++ b/cypress/support/step_definitions/UserProfile.SocialMedia/the_new_url_is_displayed.js @@ -1,6 +1,6 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then('the new url is displayed', () => { +defineStep('the new url is displayed', () => { cy.get("a[href='https://freeradical.zone/tinkerbell']") .should('have.length', 1) }) diff --git a/cypress/support/step_definitions/UserProfile.SocialMedia/the_old_url_is_not_displayed.js b/cypress/support/step_definitions/UserProfile.SocialMedia/the_old_url_is_not_displayed.js index c0941f600..6d71c8eb6 100644 --- a/cypress/support/step_definitions/UserProfile.SocialMedia/the_old_url_is_not_displayed.js +++ b/cypress/support/step_definitions/UserProfile.SocialMedia/the_old_url_is_not_displayed.js @@ -1,6 +1,6 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then('the old url is not displayed', () => { +defineStep('the old url is not displayed', () => { cy.get("a[href='https://freeradical.zone/peter-pan']") .should('have.length', 0) }) diff --git a/cypress/support/step_definitions/UserProfile.SocialMedia/they_should_be_able_to_see_my_social_media_links.js b/cypress/support/step_definitions/UserProfile.SocialMedia/they_should_be_able_to_see_my_social_media_links.js index 3b1692b59..d800c9a05 100644 --- a/cypress/support/step_definitions/UserProfile.SocialMedia/they_should_be_able_to_see_my_social_media_links.js +++ b/cypress/support/step_definitions/UserProfile.SocialMedia/they_should_be_able_to_see_my_social_media_links.js @@ -1,6 +1,6 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then('they should be able to see my social media links', () => { +defineStep('they should be able to see my social media links', () => { cy.get('[data-test="social-media-list-headline"]') .contains('Peter Pan') .get('a[href="https://freeradical.zone/peter-pan"]') diff --git a/cypress/support/step_definitions/common/I_am_logged_in_as_{string}.js b/cypress/support/step_definitions/common/I_am_logged_in_as_{string}.js index 1dbaa3d94..b8153190c 100644 --- a/cypress/support/step_definitions/common/I_am_logged_in_as_{string}.js +++ b/cypress/support/step_definitions/common/I_am_logged_in_as_{string}.js @@ -1,9 +1,9 @@ -import { Given } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' import encode from '../../../../backend/build/src/jwt/encode' -Given("I am logged in as {string}", slug => { +defineStep('I am logged in as {string}', slug => { cy.neode() - .firstOf("User", { slug }) + .firstOf('User', { slug }) .then(user => { return new Cypress.Promise((resolve, reject) => { if(!user) { @@ -15,4 +15,4 @@ Given("I am logged in as {string}", slug => { .then(user => { cy.setCookie('ocelot-social-token', encode(user)) }) -}); +}) diff --git a/cypress/support/step_definitions/common/I_am_on_page_{string}.js b/cypress/support/step_definitions/common/I_am_on_page_{string}.js index 44b10c4c4..b12b24ae5 100644 --- a/cypress/support/step_definitions/common/I_am_on_page_{string}.js +++ b/cypress/support/step_definitions/common/I_am_on_page_{string}.js @@ -1,6 +1,6 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("I am on page {string}", page => { - cy.location("pathname") - .should("match", new RegExp(page)); -}); +defineStep('I am on page {string}', page => { + cy.location('pathname') + .should('match', new RegExp(page)) +}) diff --git a/cypress/support/step_definitions/common/I_can_see_the_following_table.js b/cypress/support/step_definitions/common/I_can_see_the_following_table.js index f62e1a99a..7bbc5201e 100644 --- a/cypress/support/step_definitions/common/I_can_see_the_following_table.js +++ b/cypress/support/step_definitions/common/I_can_see_the_following_table.js @@ -1,6 +1,6 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then('I can see the following table:', table => { +defineStep('I can see the following table:', table => { const headers = table.raw()[0] headers.forEach((expected, i) => { cy.get('thead th') diff --git a/cypress/support/step_definitions/common/I_choose_the_following_text_as_content.js b/cypress/support/step_definitions/common/I_choose_the_following_text_as_content.js index 51d77d8e1..97e2865d2 100644 --- a/cypress/support/step_definitions/common/I_choose_the_following_text_as_content.js +++ b/cypress/support/step_definitions/common/I_choose_the_following_text_as_content.js @@ -1,9 +1,9 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When("I choose the following text as content:", text => { +defineStep('I choose the following text as content:', text => { cy.task('getValue', 'lastPost').then(lastPost => { - lastPost.content = text.replace("\n", " "); + lastPost.content = text.replace('\n', ' ') cy.task('pushValue', { name: 'lastPost', value: lastPost }) - cy.get(".editor .ProseMirror").type(lastPost.content); + cy.get('.editor .ProseMirror').type(lastPost.content) }) -}); +}) diff --git a/cypress/support/step_definitions/common/I_click_on_{string}.js b/cypress/support/step_definitions/common/I_click_on_{string}.js index 9d51f27f7..3df19246d 100644 --- a/cypress/support/step_definitions/common/I_click_on_{string}.js +++ b/cypress/support/step_definitions/common/I_click_on_{string}.js @@ -1,9 +1,10 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When("I click on {string}", element => { +defineStep('I click on {string}', element => { const elementSelectors = { 'submit button': 'button[name=submit]', 'create post button': '.post-add-button', + 'create group button': '.group-add-button', 'save button': 'button[type=submit]', 'the first post': '.post-teaser:first-child', 'comment button': 'button[type=submit]', @@ -15,5 +16,5 @@ When("I click on {string}", element => { cy.get(elementSelectors[element]) .click() - .wait(750); -}); + .wait(750) +}) diff --git a/cypress/support/step_definitions/common/I_click_on_{string}_from_the_content_menu_in_the_user_info_box.js b/cypress/support/step_definitions/common/I_click_on_{string}_from_the_content_menu_in_the_user_info_box.js index 66373037e..8912b5974 100644 --- a/cypress/support/step_definitions/common/I_click_on_{string}_from_the_content_menu_in_the_user_info_box.js +++ b/cypress/support/step_definitions/common/I_click_on_{string}_from_the_content_menu_in_the_user_info_box.js @@ -1,12 +1,12 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When("I click on {string} from the content menu in the user info box", +defineStep('I click on {string} from the content menu in the user info box', button => { - cy.get(".user-content-menu .base-button").click(); - cy.get(".popover .ds-menu-item-link") + cy.get('.user-content-menu .base-button').click() + cy.get('.popover .ds-menu-item-link') .contains(button) .click({ force: true - }); + }) } -); +) diff --git a/cypress/support/step_definitions/common/I_click_the_checkbox_show_donations_progress_bar_and_save.js b/cypress/support/step_definitions/common/I_click_the_checkbox_show_donations_progress_bar_and_save.js index 257b2b556..2c588e635 100644 --- a/cypress/support/step_definitions/common/I_click_the_checkbox_show_donations_progress_bar_and_save.js +++ b/cypress/support/step_definitions/common/I_click_the_checkbox_show_donations_progress_bar_and_save.js @@ -1,8 +1,8 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; -import 'cypress-network-idle'; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' +import 'cypress-network-idle' -Then("I click the checkbox show donations progress bar and save", () => { - cy.get("#showDonations").click() - cy.get(".donations-info-button").click() +defineStep('I click the checkbox show donations progress bar and save', () => { + cy.get('#showDonations').click() + cy.get('.donations-info-button').click() cy.waitForNetworkIdle(2000) }) diff --git a/cypress/support/step_definitions/common/I_fill_in_my_credentials_{string}_{string}.js b/cypress/support/step_definitions/common/I_fill_in_my_credentials_{string}_{string}.js index 3c0b0d02e..00db6344c 100644 --- a/cypress/support/step_definitions/common/I_fill_in_my_credentials_{string}_{string}.js +++ b/cypress/support/step_definitions/common/I_fill_in_my_credentials_{string}_{string}.js @@ -1,12 +1,12 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When("I fill in my credentials {string} {string}", (email,password) => { - cy.get("input[name=email]") - .trigger("focus") +defineStep('I fill in my credentials {string} {string}', (email,password) => { + cy.get('input[name=email]') + .trigger('focus') .type('{selectall}{backspace}') .type(email) - .get("input[name=password]") - .trigger("focus") + .get('input[name=password]') + .trigger('focus') .type('{selectall}{backspace}') - .type(password); -}); + .type(password) +}) diff --git a/cypress/support/step_definitions/common/I_follow_the_user_{string}.js b/cypress/support/step_definitions/common/I_follow_the_user_{string}.js index 3698daee8..bca16ad8a 100644 --- a/cypress/support/step_definitions/common/I_follow_the_user_{string}.js +++ b/cypress/support/step_definitions/common/I_follow_the_user_{string}.js @@ -1,13 +1,13 @@ -import { Given } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Given("I follow the user {string}", name => { +defineStep('I follow the user {string}', name => { cy.neode() - .firstOf("User", {name}) + .firstOf('User', {name}) .then(followed => { cy.neode() - .firstOf("User", { - name: "Peter Pan" + .firstOf('User', { + name: 'Peter Pan' }) - .relateTo(followed, "following"); - }); -}); + .relateTo(followed, 'following') + }) +}) diff --git a/cypress/support/step_definitions/common/I_get_removed_from_his_follower_collection.js b/cypress/support/step_definitions/common/I_get_removed_from_his_follower_collection.js index 7721efda0..36ef62a54 100644 --- a/cypress/support/step_definitions/common/I_get_removed_from_his_follower_collection.js +++ b/cypress/support/step_definitions/common/I_get_removed_from_his_follower_collection.js @@ -1,8 +1,8 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("I get removed from his follower collection", () => { - cy.get(".base-card") - .not(".post-link"); - cy.get(".main-container") - .contains(".base-card","is not followed by anyone"); - }); +defineStep('I get removed from his follower collection', () => { + cy.get('.base-card') + .not('.post-link') + cy.get('.main-container') + .contains('.base-card','is not followed by anyone') + }) diff --git a/cypress/support/step_definitions/common/I_log_out.js b/cypress/support/step_definitions/common/I_log_out.js index c97506535..efe14a9c9 100644 --- a/cypress/support/step_definitions/common/I_log_out.js +++ b/cypress/support/step_definitions/common/I_log_out.js @@ -1,15 +1,15 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When("I log out", () => { - cy.get(".avatar-menu").then(($menu) => { +defineStep('I log out', () => { + cy.get('.avatar-menu').then(($menu) => { if (!$menu.is(':visible')){ - cy.scrollTo("top"); - cy.wait(500); + cy.scrollTo('top') + cy.wait(500) } }) - cy.get(".avatar-menu") - .click(); - cy.get(".avatar-menu-popover") + cy.get('.avatar-menu') + .click() + cy.get('.avatar-menu-popover') .find('a[href="/logout"]') - .click(); -}); + .click() +}) diff --git a/cypress/support/step_definitions/common/I_navigate_to_my_{string}_settings_page.js b/cypress/support/step_definitions/common/I_navigate_to_my_{string}_settings_page.js index 91a3b58d9..73b93d3d3 100644 --- a/cypress/support/step_definitions/common/I_navigate_to_my_{string}_settings_page.js +++ b/cypress/support/step_definitions/common/I_navigate_to_my_{string}_settings_page.js @@ -1,10 +1,10 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When("I navigate to my {string} settings page", settingsPage => { - cy.get(".avatar-menu-trigger").click(); - cy.get(".avatar-menu-popover") - .find("a[href]") - .contains("Settings") - .click(); - cy.contains(".ds-menu-item-link", settingsPage).click(); -}); +defineStep('I navigate to my {string} settings page', settingsPage => { + cy.get('.avatar-menu-trigger').click() + cy.get('.avatar-menu-popover') + .find('a[href]') + .contains('Settings') + .click() + cy.contains('.ds-menu-item-link', settingsPage).click() +}) diff --git a/cypress/support/step_definitions/common/I_navigate_to_page_{string}.js b/cypress/support/step_definitions/common/I_navigate_to_page_{string}.js index d90cc906c..509a49f43 100644 --- a/cypress/support/step_definitions/common/I_navigate_to_page_{string}.js +++ b/cypress/support/step_definitions/common/I_navigate_to_page_{string}.js @@ -1,7 +1,7 @@ -import { Given } from "@badeball/cypress-cucumber-preprocessor"; -import 'cypress-network-idle'; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' +import 'cypress-network-idle' -Given("I navigate to page {string}", page => { - cy.visit(page); +defineStep('I navigate to page {string}', page => { + cy.visit(page) cy.waitForNetworkIdle(2000) -}); +}) diff --git a/cypress/support/step_definitions/common/I_refresh_the_page.js b/cypress/support/step_definitions/common/I_refresh_the_page.js index 47e493fe4..5c48ad671 100644 --- a/cypress/support/step_definitions/common/I_refresh_the_page.js +++ b/cypress/support/step_definitions/common/I_refresh_the_page.js @@ -1,6 +1,6 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When('I refresh the page', () => { +defineStep('I refresh the page', () => { cy.visit('/') - .reload(); -}); + .reload() +}) diff --git a/cypress/support/step_definitions/common/I_search_for_{string}.js b/cypress/support/step_definitions/common/I_search_for_{string}.js index f91959182..d568d1332 100644 --- a/cypress/support/step_definitions/common/I_search_for_{string}.js +++ b/cypress/support/step_definitions/common/I_search_for_{string}.js @@ -1,12 +1,12 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When("I search for {string}", value => { +defineStep('I search for {string}', value => { cy.intercept({ - method: "POST", - url: "http://localhost:3000/api", - }).as("graphqlRequest"); - cy.get(".searchable-input .ds-select input") + method: 'POST', + url: 'http://localhost:3000/api', + }).as('graphqlRequest') + cy.get('.searchable-input .ds-select input') .focus() - .type(value); - cy.wait("@graphqlRequest"); -}); + .type(value) + cy.wait('@graphqlRequest') +}) diff --git a/cypress/support/step_definitions/common/I_see_a_toaster_with_status_{string}.js b/cypress/support/step_definitions/common/I_see_a_toaster_with_status_{string}.js index c7bd91e29..059f60184 100644 --- a/cypress/support/step_definitions/common/I_see_a_toaster_with_status_{string}.js +++ b/cypress/support/step_definitions/common/I_see_a_toaster_with_status_{string}.js @@ -1,9 +1,9 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("I see a toaster with status {string}", (status) => { +defineStep('I see a toaster with status {string}', (status) => { switch (status) { - case "success": - cy.get(".iziToast.iziToast-color-green").should("be.visible"); - break; + case 'success': + cy.get('.iziToast.iziToast-color-green').should('be.visible') + break } }) diff --git a/cypress/support/step_definitions/common/I_see_a_toaster_with_{string}.js b/cypress/support/step_definitions/common/I_see_a_toaster_with_{string}.js index e1496b8de..332e525a4 100644 --- a/cypress/support/step_definitions/common/I_see_a_toaster_with_{string}.js +++ b/cypress/support/step_definitions/common/I_see_a_toaster_with_{string}.js @@ -1,5 +1,5 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("I see a toaster with {string}", (title) => { - cy.get(".iziToast-message").should("contain", title); +defineStep('I see a toaster with {string}', (title) => { + cy.get('.iziToast-message').should('contain', title) }) diff --git a/cypress/support/step_definitions/common/I_see_a_{string}_message.js b/cypress/support/step_definitions/common/I_see_a_{string}_message.js index cc8deca5f..8add062ec 100644 --- a/cypress/support/step_definitions/common/I_see_a_{string}_message.js +++ b/cypress/support/step_definitions/common/I_see_a_{string}_message.js @@ -1,5 +1,5 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("I see a {string} message:", (_type, message) => { - cy.contains(message); -}); +defineStep('I see a {string} message:', (_type, message) => { + cy.contains(message) +}) diff --git a/cypress/support/step_definitions/common/I_should_see_the_following_posts_in_the_select_dropdown.js b/cypress/support/step_definitions/common/I_should_see_the_following_posts_in_the_select_dropdown.js index 88e18a280..cef95198b 100644 --- a/cypress/support/step_definitions/common/I_should_see_the_following_posts_in_the_select_dropdown.js +++ b/cypress/support/step_definitions/common/I_should_see_the_following_posts_in_the_select_dropdown.js @@ -1,8 +1,8 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("I should see the following posts in the select dropdown:", table => { +defineStep('I should see the following posts in the select dropdown:', table => { table.hashes().forEach(({ title }) => { - cy.get(".ds-select-dropdown") - .should("contain", title); - }); -}); + cy.get('.ds-select-dropdown') + .should('contain', title) + }) +}) diff --git a/cypress/support/step_definitions/common/I_wait_for_{int}_milliseconds.js b/cypress/support/step_definitions/common/I_wait_for_{int}_milliseconds.js index 2ed462340..aa884d08c 100644 --- a/cypress/support/step_definitions/common/I_wait_for_{int}_milliseconds.js +++ b/cypress/support/step_definitions/common/I_wait_for_{int}_milliseconds.js @@ -1,5 +1,5 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When("I wait for {int} milliseconds", time => { +defineStep('I wait for {int} milliseconds', time => { cy.wait(time) -}); +}) diff --git a/cypress/support/step_definitions/common/the_checkbox_with_ID_{string}_should_{string}.js b/cypress/support/step_definitions/common/the_checkbox_with_ID_{string}_should_{string}.js index 603524804..1e223909c 100644 --- a/cypress/support/step_definitions/common/the_checkbox_with_ID_{string}_should_{string}.js +++ b/cypress/support/step_definitions/common/the_checkbox_with_ID_{string}_should_{string}.js @@ -1,5 +1,5 @@ -import { When } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -When("the checkbox with ID {string} should {string}", (id, value) => { +defineStep('the checkbox with ID {string} should {string}', (id, value) => { cy.get('#' + id).should(value) }) diff --git a/cypress/support/step_definitions/common/the_first_post_on_the_newsfeed_has_the_title.js b/cypress/support/step_definitions/common/the_first_post_on_the_newsfeed_has_the_title.js index ba98a7a8e..0e3a804cc 100644 --- a/cypress/support/step_definitions/common/the_first_post_on_the_newsfeed_has_the_title.js +++ b/cypress/support/step_definitions/common/the_first_post_on_the_newsfeed_has_the_title.js @@ -1,6 +1,6 @@ -import { Then } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' -Then("the first post on the newsfeed has the title:", title => { - cy.get(".post-teaser:first") - .should("contain", title); -}); +defineStep('the first post on the newsfeed has the title:', title => { + cy.get('.post-teaser:first') + .should('contain', title) +}) diff --git a/cypress/support/step_definitions/common/the_following_{string}_are_in_the_database.js b/cypress/support/step_definitions/common/the_following_{string}_are_in_the_database.js index 8e41afa2a..94c647745 100644 --- a/cypress/support/step_definitions/common/the_following_{string}_are_in_the_database.js +++ b/cypress/support/step_definitions/common/the_following_{string}_are_in_the_database.js @@ -1,10 +1,11 @@ -import { Given } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' +import './../../factories' -Given("the following {string} are in the database:", (table,data) => { +defineStep('the following {string} are in the database:', (table,data) => { switch(table){ - case "posts": + case 'posts': data.hashes().forEach( entry => { - cy.factory().build("post", { + cy.factory().build('post', { ...entry, deleted: Boolean(entry.deleted), disabled: Boolean(entry.disabled), @@ -12,29 +13,29 @@ Given("the following {string} are in the database:", (table,data) => { },{ ...entry, tagIds: entry.tagIds ? entry.tagIds.split(',').map(item => item.trim()) : [], - }); + }) }) break - case "comments": + case 'comments': data.hashes().forEach( entry => { cy.factory() - .build("comment", entry, entry); + .build('comment', entry, entry) }) break - case "users": + case 'users': data.hashes().forEach( entry => { - cy.factory().build("user", entry, entry); - }); + cy.factory().build('user', entry, entry) + }) break - case "tags": + case 'tags': data.hashes().forEach( entry => { - cy.factory().build("tag", entry, entry) - }); + cy.factory().build('tag', entry, entry) + }) break - case "donations": + case 'donations': data.hashes().forEach( entry => { - cy.factory().build("donations", entry, entry) - }); + cy.factory().build('donations', entry, entry) + }) break } }) diff --git a/cypress/support/step_definitions/common/{string}_wrote_a_post_{string}.js b/cypress/support/step_definitions/common/{string}_wrote_a_post_{string}.js index 0da055951..af509d8b3 100644 --- a/cypress/support/step_definitions/common/{string}_wrote_a_post_{string}.js +++ b/cypress/support/step_definitions/common/{string}_wrote_a_post_{string}.js @@ -1,10 +1,11 @@ -import { Given } from "@badeball/cypress-cucumber-preprocessor"; +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' +import './../../factories' -Given('{string} wrote a post {string}', (author, title) => { +defineStep('{string} wrote a post {string}', (author, title) => { cy.factory() - .build("post", { + .build('post', { title, }, { authorId: author, - }); -}); + }) +}) diff --git a/deployment/helm/charts/ocelot-neo4j/Chart.yaml b/deployment/helm/charts/ocelot-neo4j/Chart.yaml index f8a6c88f1..3bb36ca4e 100644 --- a/deployment/helm/charts/ocelot-neo4j/Chart.yaml +++ b/deployment/helm/charts/ocelot-neo4j/Chart.yaml @@ -21,4 +21,4 @@ version: 0.1.0 # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "3.2.0" +appVersion: "3.8.2" diff --git a/deployment/helm/charts/ocelot-social/Chart.yaml b/deployment/helm/charts/ocelot-social/Chart.yaml index c363a90cb..b37293fcb 100644 --- a/deployment/helm/charts/ocelot-social/Chart.yaml +++ b/deployment/helm/charts/ocelot-social/Chart.yaml @@ -21,4 +21,4 @@ version: 0.1.0 # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "3.2.0" +appVersion: "3.8.2" diff --git a/deployment/helm/charts/ocelot-social/templates/backend/stateful-set.yaml b/deployment/helm/charts/ocelot-social/templates/backend/stateful-set.yaml index 98eb3fcad..604b79826 100644 --- a/deployment/helm/charts/ocelot-social/templates/backend/stateful-set.yaml +++ b/deployment/helm/charts/ocelot-social/templates/backend/stateful-set.yaml @@ -18,13 +18,16 @@ spec: - name: {{ .Release.Name }}-backend-migrations image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag | default (include "defaultTag" .) }}" imagePullPolicy: {{ quote .Values.global.image.pullPolicy }} - command: ["/bin/sh", "-c", "yarn prod:migrate up"] + command: ["/bin/sh", "-c", "yarn prod:migrate init && yarn prod:migrate up"] {{- include "resources" .Values.backend.resources | indent 10 }} envFrom: - configMapRef: name: {{ .Release.Name }}-backend-env - secretRef: name: {{ .Release.Name }}-backend-secret-env + volumeMounts: + - mountPath: /app/public/uploads + name: uploads containers: - name: {{ .Release.Name }}-backend image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag | default (include "defaultTag" .) }}" diff --git a/deployment/helm/helmfile/secrets/ocelot.yaml b/deployment/helm/helmfile/secrets/ocelot.yaml index 3965bc09e..41eff134c 100644 --- a/deployment/helm/helmfile/secrets/ocelot.yaml +++ b/deployment/helm/helmfile/secrets/ocelot.yaml @@ -23,15 +23,16 @@ secrets: NEO4J_USERNAME: null NEO4J_PASSWORD: null REDIS_PASSWORD: null + AWS_ACCESS_KEY_ID: ENC[AES256_GCM,data:iiN5ueqyo60VHb9e2bnhc19iGTg=,iv:zawYpKrFafgsu1+YRet1hzZf1G3a6BIlZgsh7xNADaE=,tag:rTsmm8cqei34b6cT6vn08w==,type:str] + AWS_SECRET_ACCESS_KEY: ENC[AES256_GCM,data:Zl4LRXdDh/6Q8F9RVp+3L7NXGZ0F2cgFMKPhl/TVeuD5Bhy68W5ekg==,iv:AmPoinGISrSOZdoBKdeFFXfr2hwOK4nWMnniz8K5qgU=,tag:K8Q7M7e+6G9T0Oh3Sp4OzA==,type:str] + AWS_ENDPOINT: ENC[AES256_GCM,data:/waEqUgcOmldZ+peFTNVsDQf2KrpWY8ZZMt1nT5117SkbY4=,iv:n+Kvidjb/TM4bQYKqTaFxt8GkHo02PuxEGpzgOcywr4=,tag:lrGPgCWWy3GMIcTv75IYTg==,type:str] + AWS_REGION: ENC[AES256_GCM,data:kBPpHZ8zw4PMpg==,iv:R+QZe303do37Hd/97NpS1pt9VaBE/gqZDY2/qlIvvps=,tag:0WduW8wfJXtBqlh4qfRGNA==,type:str] + AWS_BUCKET: ENC[AES256_GCM,data:0fAspN/PoRVPlSbz+qDBRUOieeC4,iv:JGJ/LyLpMymN0tpZmW6DjPT3xqXzK/KhYQsy9sgPd60=,tag:Y6PBs0916JkHRHSe7hqSMA==,type:str] neo4j: env: NEO4J_USERNAME: "" NEO4J_PASSWORD: "" sops: - kms: [] - gcp_kms: [] - azure_kv: [] - hc_vault: [] age: - recipient: age1llp6k66265q3rzqemxpnq0x3562u20989vcjf65fl9s3hjhgcscq6mhnjw enc: | @@ -69,8 +70,7 @@ sops: aGNFeXZZRmlJM041OHdTM0pmM3BBdGMKGvFgYY1jhKwciAOZKyw0hlFVNbOk7CM7 041g17JXNV1Wk6WgMZ4w8p54RKQVaWCT4wxChy6wNNdQ3IeKgqEU2w== -----END AGE ENCRYPTED FILE----- - lastmodified: "2024-10-29T14:26:49Z" - mac: ENC[AES256_GCM,data:YXX7MEAK0wmuxLTmdr7q5uVd6DG6FhGUeE+EzbhWe/OovH6n+CjKZGklnEX+5ztDO0IgZh/T9Hx1CgFYuVbcOkvDoFBDwNpRA/QOQrM0p/+tRlMNCypC/Wh2xL0DhA4A/Qum2oyE/BDkt1Yy8N5wZDZn575+ZAjXEgAzlhpT5qk=,iv:ire3gkHTY6+0lgbV1Es6Lf8bcKTg4WKnq46M+b/VRcU=,tag:MkZULKcwROvIw/C0YtcUbA==,type:str] - pgp: [] + lastmodified: "2025-05-29T06:57:01Z" + mac: ENC[AES256_GCM,data:0eWUqVsJrolrVYFsG4aigAYSjBW35Z+64y/ZE8Az15GmI0E4p/1kZPIY6hv9SUiCD1R2Ro0nm1QsV9A/hvNeWla0EdARNnARxIphxMJWKRGwcVkFVk28Jh4g4jE/rTggWx/wLbR6bza1RLHA1wRMb1PYuLfZybsb/whN4+gTMfg=,iv:irQkLpj1+3egSsiV9HqZP4tgG1fotCOizubL42gRjSQ=,tag:DC0xzG/VqcL4ib6ijxQZnA==,type:str] unencrypted_suffix: _unencrypted - version: 3.9.0 + version: 3.10.2 diff --git a/docker-compose.maintenance.yml b/docker-compose.maintenance.yml deleted file mode 100644 index e2cd1e515..000000000 --- a/docker-compose.maintenance.yml +++ /dev/null @@ -1,35 +0,0 @@ -# Todo: !!! This file seems related to our old maintenance worker for MongoDB and has to be refactored in case of using it !!! - -services: - maintenance-worker: - image: ghcr.io/ocelot-social-community/ocelot-social/develop-maintenance-worker:latest - build: - context: deployment/legacy-migration/maintenance-worker - volumes: - - uploads:/uploads - - neo4j-data:/data - - ./deployment/legacy-migration/maintenance-worker/migration/:/migration - - ./deployment/legacy-migration/maintenance-worker/ssh/:/root/.ssh - environment: - - NEO4J_dbms_security_auth__enabled=false - - NEO4J_dbms_memory_heap_max__size=2G - - GRAPHQL_URI=http://localhost:4000 - - CLIENT_URI=http://localhost:3000 - - JWT_SECRET=b/&&7b78BF&fv/Vd - - PRIVATE_KEY_PASSPHRASE=a7dsf78sadg87ad87sfagsadg78 - - NEO4J_apoc_import_file_enabled=true - - "SSH_USERNAME=${SSH_USERNAME}" - - "SSH_HOST=${SSH_HOST}" - - "MONGODB_USERNAME=${MONGODB_USERNAME}" - - "MONGODB_PASSWORD=${MONGODB_PASSWORD}" - - "MONGODB_AUTH_DB=${MONGODB_AUTH_DB}" - - "MONGODB_DATABASE=${MONGODB_DATABASE}" - - "UPLOADS_DIRECTORY=${UPLOADS_DIRECTORY}" - - "MONGO_EXPORT_SPLIT_SIZE=${MONGO_EXPORT_SPLIT_SIZE}" - ports: - - 7687:7687 - - 7474:7474 - -volumes: - neo4j-data: - uploads: diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 53f7e7b4e..a9e957dca 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -25,11 +25,22 @@ services: backend: image: ghcr.io/ocelot-social-community/ocelot-social/backend:local-development + depends_on: + - minio + - minio-mc build: target: development environment: - NODE_ENV="development" - DEBUG=true + - SMTP_PORT=1025 + - SMTP_HOST=mailserver + - AWS_ACCESS_KEY_ID=minio + - AWS_SECRET_ACCESS_KEY=12341234 + - AWS_ENDPOINT=http:/minio:9000 + - AWS_REGION=local + - AWS_BUCKET=ocelot + - S3_PUBLIC_GATEWAY=http:/localhost:9000 volumes: - ./backend:/app @@ -39,8 +50,38 @@ services: - 7474:7474 mailserver: - image: djfarrelly/maildev + image: maildev/maildev container_name: mailserver ports: - - 1080:80 - - 25:25 + - 1080:1080 + - 1025:1025 + + minio: + image: quay.io/minio/minio + ports: + - 9000:9000 + - 9001:9001 + volumes: + - minio_data:/data + environment: + - MINIO_ROOT_USER=minio + - MINIO_ROOT_PASSWORD=12341234 + command: server /data --console-address ":9001" + + minio-mc: + image: quay.io/minio/mc + depends_on: + - minio + restart: on-failure + volumes: + - ./minio/readonly-policy.json:/tmp/readonly-policy.json + entrypoint: > + /bin/sh -c " + sleep 5; + /usr/bin/mc alias set dockerminio http://minio:9000 minio 12341234; + /usr/bin/mc mb --ignore-existing dockerminio/ocelot; + /usr/bin/mc anonymous set-json /tmp/readonly-policy.json dockerminio/ocelot; + " + +volumes: + minio_data: diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 119cd20eb..67645ad87 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -6,17 +6,26 @@ services: build: target: test environment: - - NODE_ENV="test" + - NODE_ENV=test volumes: - ./coverage:/app/coverage backend: # name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there image: ghcr.io/ocelot-social-community/ocelot-social/backend:test + depends_on: + - minio + - minio-mc + - mailserver build: target: test environment: - NODE_ENV="test" + - AWS_ACCESS_KEY_ID=minio + - AWS_SECRET_ACCESS_KEY=12341234 + - AWS_ENDPOINT=http:/minio:9000 + - AWS_REGION=local + - AWS_BUCKET=ocelot volumes: - ./coverage:/app/coverage @@ -37,7 +46,37 @@ services: # - NEO4J_dbms_connector_https_listen__address=0.0.0.0:7473 mailserver: - image: djfarrelly/maildev + image: maildev/maildev ports: - - 1080:80 - - 25:25 + - 1080:1080 + - 1025:1025 + + minio: + image: quay.io/minio/minio + ports: + - 9000:9000 + - 9001:9001 + volumes: + - minio_data:/data + environment: + - MINIO_ROOT_USER=minio + - MINIO_ROOT_PASSWORD=12341234 + command: server /data --console-address ":9001" + + minio-mc: + image: quay.io/minio/mc + depends_on: + - minio + restart: on-failure + volumes: + - ./minio/readonly-policy.json:/tmp/readonly-policy.json + entrypoint: > + /bin/sh -c " + sleep 5; + /usr/bin/mc alias set dockerminio http://minio:9000 minio 12341234; + /usr/bin/mc mb --ignore-existing dockerminio/ocelot; + /usr/bin/mc anonymous set-json /tmp/readonly-policy.json dockerminio/ocelot; + " + +volumes: + minio_data: diff --git a/docker-compose.yml b/docker-compose.yml index d46b5cd29..4a1e9e951 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,8 +43,6 @@ services: # Application only envs #- HOST=0.0.0.0 # This is nuxt specific, alternative value is HOST=webapp #- GRAPHQL_URI=http://backend:4000 - env_file: - - ./frontend/.env backend: image: ghcr.io/ocelot-social-community/ocelot-social/backend:${OCELOT_VERSION:-latest} @@ -55,8 +53,6 @@ services: - neo4j ports: - 4000:4000 - volumes: - - backend_uploads:/app/public/uploads environment: # Envs used in Dockerfile # - DOCKER_WORKDIR="/app" @@ -106,5 +102,4 @@ services: # command: ["tail", "-f", "/dev/null"] volumes: - backend_uploads: neo4j_data: diff --git a/frontend/.nvmrc b/frontend/.nvmrc new file mode 100644 index 000000000..5871e601c --- /dev/null +++ b/frontend/.nvmrc @@ -0,0 +1 @@ +v20.12.1 \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 63b54a127..8ade59470 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "ocelot-social-frontend", - "version": "3.2.1", + "version": "3.8.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ocelot-social-frontend", - "version": "3.2.1", + "version": "3.8.2", "license": "Apache-2.0", "dependencies": { "@intlify/unplugin-vue-i18n": "^2.0.0", diff --git a/frontend/package.json b/frontend/package.json index f032f0d0c..1cc48f58f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "ocelot-social-frontend", - "version": "3.2.1", + "version": "3.8.2", "description": "ocelot.social new Frontend (in development and not fully implemented) by IT4C Boilerplate for frontends", "main": "build/index.js", "type": "module", diff --git a/minio/readonly-policy.json b/minio/readonly-policy.json new file mode 100644 index 000000000..6cdbdd667 --- /dev/null +++ b/minio/readonly-policy.json @@ -0,0 +1,19 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "AWS": [ + "*" + ] + }, + "Action": [ + "s3:GetObject" + ], + "Resource": [ + "arn:aws:s3:::ocelot/*" + ] + } + ] +} diff --git a/package-lock.json b/package-lock.json index 0b708bace..1b29ceab5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,28 +1,28 @@ { "name": "ocelot-social", - "version": "3.2.1", + "version": "3.8.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ocelot-social", - "version": "3.2.1", + "version": "3.8.2", "license": "MIT", "devDependencies": { - "@babel/core": "^7.26.10", - "@babel/preset-env": "^7.26.9", - "@babel/register": "^7.25.9", + "@babel/core": "^7.27.1", + "@babel/preset-env": "^7.27.1", + "@babel/register": "^7.27.1", "@badeball/cypress-cucumber-preprocessor": "^22.0.1", - "@cucumber/cucumber": "11.2.0", + "@cucumber/cucumber": "11.3.0", "@cypress/browserify-preprocessor": "^3.0.2", - "@faker-js/faker": "9.6.0", + "@faker-js/faker": "9.8.0", "auto-changelog": "^2.5.0", - "bcryptjs": "^2.4.3", + "bcryptjs": "^3.0.2", "cross-env": "^7.0.3", - "cypress": "^14.2.1", + "cypress": "^14.4.1", "cypress-network-idle": "^1.15.0", "date-fns": "^3.6.0", - "dotenv": "^16.4.7", + "dotenv": "^16.5.0", "expect": "^29.6.4", "graphql-request": "^2.0.0", "import": "^0.0.6", @@ -102,24 +102,24 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", - "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.1.tgz", + "integrity": "sha512-Q+E+rd/yBzNQhXkG+zQnF58e4zoZfBedaxwzPmicKsiK3nt8iJYrSrDbjwFFDGC4f+rPafqRaPH6TsDoSvMf7A==", "dev": true, "license": "MIT", "engines": { @@ -127,22 +127,22 @@ } }, "node_modules/@babel/core": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", - "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", + "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.10", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.10", - "@babel/parser": "^7.26.10", - "@babel/template": "^7.26.9", - "@babel/traverse": "^7.26.10", - "@babel/types": "^7.26.10", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helpers": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -158,14 +158,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", - "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", + "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.0", - "@babel/types": "^7.27.0", + "@babel/parser": "^7.27.1", + "@babel/types": "^7.27.1", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -175,26 +175,27 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz", + "integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", - "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.1.tgz", + "integrity": "sha512-2YaDd/Rd9E598B5+WIc8wJPmWETiiJXFYVE60oX8FDohv7rAUU3CQj+A1MgeEmcsk2+dQuEjIe/GDvig0SqL4g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.26.5", - "@babel/helper-validator-option": "^7.25.9", + "@babel/compat-data": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -204,17 +205,18 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", - "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", + "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/traverse": "^7.25.9", + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.27.1", "semver": "^6.3.1" }, "engines": { @@ -225,13 +227,14 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.9.tgz", - "integrity": "sha512-ORPNZ3h6ZRkOyAa/SaHU+XsLZr0UQzRwuDQ0cczIA17nAzZ+85G5cVkOJIj7QavLZGSe8QXUmNFxSZzjcZF9bw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", + "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "regexpu-core": "^6.1.1", + "@babel/helper-annotate-as-pure": "^7.27.1", + "regexpu-core": "^6.2.0", "semver": "^6.3.1" }, "engines": { @@ -258,40 +261,43 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", - "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", + "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -301,21 +307,22 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", - "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", - "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", "dev": true, "license": "MIT", "engines": { @@ -323,14 +330,15 @@ } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", - "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-wrap-function": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -340,14 +348,15 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", - "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -357,81 +366,86 @@ } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", - "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "devOptional": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "devOptional": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", - "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz", + "integrity": "sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", - "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", + "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.0", - "@babel/types": "^7.27.0" + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", - "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.1.tgz", + "integrity": "sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==", "devOptional": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.0" + "@babel/types": "^7.27.1" }, "bin": { "parser": "bin/babel-parser.js" @@ -441,13 +455,14 @@ } }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", - "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", + "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -457,12 +472,13 @@ } }, "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", - "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -472,12 +488,13 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", - "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -487,14 +504,15 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", - "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/plugin-transform-optional-chaining": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -504,13 +522,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", - "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.27.1.tgz", + "integrity": "sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -569,12 +588,13 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", - "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", + "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -584,12 +604,13 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", - "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -642,12 +663,13 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", - "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -657,15 +679,15 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.26.8.tgz", - "integrity": "sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.27.1.tgz", + "integrity": "sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5", - "@babel/helper-remap-async-to-generator": "^7.25.9", - "@babel/traverse": "^7.26.8" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -675,14 +697,15 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", - "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", + "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-remap-async-to-generator": "^7.25.9" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -692,13 +715,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz", - "integrity": "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -708,12 +731,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", - "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.1.tgz", + "integrity": "sha512-QEcFlMl9nGTgh1rn2nIeU5bkfb9BAjaQcWbiP4LvKxUot52ABcTkpcyJ7f2Q2U2RuQ84BNLgts3jRme2dTx6Fw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -723,13 +747,14 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz", - "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -739,13 +764,14 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", - "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.27.1.tgz", + "integrity": "sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -755,16 +781,17 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", - "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.27.1.tgz", + "integrity": "sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9", - "@babel/traverse": "^7.25.9", + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.27.1", "globals": "^11.1.0" }, "engines": { @@ -775,13 +802,14 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", - "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", + "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/template": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/template": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -791,12 +819,13 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", - "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.1.tgz", + "integrity": "sha512-ttDCqhfvpE9emVkXbPD8vyxxh4TWYACVybGkDj+oReOGwnp066ITEivDlLwe0b1R0+evJ13IXQuLNB5w1fhC5Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -806,13 +835,14 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", - "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", + "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -822,12 +852,13 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", - "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -837,13 +868,14 @@ } }, "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", - "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -853,12 +885,13 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", - "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -868,13 +901,13 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz", - "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", + "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -884,12 +917,13 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz", - "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -899,14 +933,14 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.26.9.tgz", - "integrity": "sha512-Hry8AusVm8LW5BVFgiyUReuoGzPUpdHQQqJY5bZnbbf+ngOHWuCuYFKw/BqaaWlvEUrF91HMhDtEaI1hZzNbLg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -916,14 +950,15 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", - "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -933,12 +968,13 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", - "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", + "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -948,12 +984,13 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", - "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -963,12 +1000,13 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz", - "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", + "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -978,12 +1016,13 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", - "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -993,13 +1032,14 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", - "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1009,14 +1049,14 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", - "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1026,15 +1066,16 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", - "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", + "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1044,13 +1085,14 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", - "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1060,13 +1102,14 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", - "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1076,12 +1119,13 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", - "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1091,13 +1135,13 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.26.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz", - "integrity": "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1107,12 +1151,13 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz", - "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", + "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1122,14 +1167,15 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz", - "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.1.tgz", + "integrity": "sha512-/sSliVc9gHE20/7D5qsdGlq7RG5NCDTWsAhyqzGuq174EtWJoGzIu1BQ7G56eDsTcy1jseBZwv50olSdXOlGuA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/plugin-transform-parameters": "^7.25.9" + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1139,13 +1185,14 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", - "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1155,12 +1202,13 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz", - "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", + "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1170,13 +1218,14 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", - "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", + "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1186,12 +1235,13 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", - "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.1.tgz", + "integrity": "sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1201,13 +1251,14 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz", - "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", + "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1217,14 +1268,15 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", - "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", + "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1234,12 +1286,13 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", - "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1314,13 +1367,13 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", - "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.1.tgz", + "integrity": "sha512-B19lbbL7PMrKr52BNPjCqg1IyNUIjTcxKj8uX9zHO+PmWN93s19NDr/f69mIkEp2x9nmDJ08a7lgHaTTzvW7mw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "regenerator-transform": "^0.15.2" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1330,13 +1383,14 @@ } }, "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz", - "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", + "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1346,12 +1400,13 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", - "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1381,12 +1436,13 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", - "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1396,13 +1452,14 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", - "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", + "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1412,12 +1469,13 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", - "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1427,13 +1485,13 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.26.8.tgz", - "integrity": "sha512-OmGDL5/J0CJPJZTHZbi2XpO0tyT2Ia7fzpW5GURwdtp2X3fMmN8au/ej6peC/T33/+CRiIpA8Krse8hFGVmT5Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1443,13 +1501,13 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.26.7.tgz", - "integrity": "sha512-jfoTXXZTgGg36BmhqT3cAYK5qkmqvJpvNrPhaK/52Vgjhw4Rq29s9UqpWWV0D6yuRmgiFH/BUVlkl96zJWqnaw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1459,12 +1517,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", - "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1474,13 +1533,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz", - "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", + "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1490,13 +1550,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", - "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1506,13 +1567,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz", - "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", + "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1522,75 +1584,75 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.9.tgz", - "integrity": "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.27.1.tgz", + "integrity": "sha512-TZ5USxFpLgKDpdEt8YWBR7p6g+bZo6sHaXLqP2BY/U0acaoI8FTVflcYCr/v94twM1C5IWFdZ/hscq9WjUeLXA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.26.8", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-plugin-utils": "^7.26.5", - "@babel/helper-validator-option": "^7.25.9", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", + "@babel/compat-data": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.27.1", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.26.0", - "@babel/plugin-syntax-import-attributes": "^7.26.0", + "@babel/plugin-syntax-import-assertions": "^7.27.1", + "@babel/plugin-syntax-import-attributes": "^7.27.1", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.25.9", - "@babel/plugin-transform-async-generator-functions": "^7.26.8", - "@babel/plugin-transform-async-to-generator": "^7.25.9", - "@babel/plugin-transform-block-scoped-functions": "^7.26.5", - "@babel/plugin-transform-block-scoping": "^7.25.9", - "@babel/plugin-transform-class-properties": "^7.25.9", - "@babel/plugin-transform-class-static-block": "^7.26.0", - "@babel/plugin-transform-classes": "^7.25.9", - "@babel/plugin-transform-computed-properties": "^7.25.9", - "@babel/plugin-transform-destructuring": "^7.25.9", - "@babel/plugin-transform-dotall-regex": "^7.25.9", - "@babel/plugin-transform-duplicate-keys": "^7.25.9", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", - "@babel/plugin-transform-dynamic-import": "^7.25.9", - "@babel/plugin-transform-exponentiation-operator": "^7.26.3", - "@babel/plugin-transform-export-namespace-from": "^7.25.9", - "@babel/plugin-transform-for-of": "^7.26.9", - "@babel/plugin-transform-function-name": "^7.25.9", - "@babel/plugin-transform-json-strings": "^7.25.9", - "@babel/plugin-transform-literals": "^7.25.9", - "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", - "@babel/plugin-transform-member-expression-literals": "^7.25.9", - "@babel/plugin-transform-modules-amd": "^7.25.9", - "@babel/plugin-transform-modules-commonjs": "^7.26.3", - "@babel/plugin-transform-modules-systemjs": "^7.25.9", - "@babel/plugin-transform-modules-umd": "^7.25.9", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", - "@babel/plugin-transform-new-target": "^7.25.9", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.26.6", - "@babel/plugin-transform-numeric-separator": "^7.25.9", - "@babel/plugin-transform-object-rest-spread": "^7.25.9", - "@babel/plugin-transform-object-super": "^7.25.9", - "@babel/plugin-transform-optional-catch-binding": "^7.25.9", - "@babel/plugin-transform-optional-chaining": "^7.25.9", - "@babel/plugin-transform-parameters": "^7.25.9", - "@babel/plugin-transform-private-methods": "^7.25.9", - "@babel/plugin-transform-private-property-in-object": "^7.25.9", - "@babel/plugin-transform-property-literals": "^7.25.9", - "@babel/plugin-transform-regenerator": "^7.25.9", - "@babel/plugin-transform-regexp-modifiers": "^7.26.0", - "@babel/plugin-transform-reserved-words": "^7.25.9", - "@babel/plugin-transform-shorthand-properties": "^7.25.9", - "@babel/plugin-transform-spread": "^7.25.9", - "@babel/plugin-transform-sticky-regex": "^7.25.9", - "@babel/plugin-transform-template-literals": "^7.26.8", - "@babel/plugin-transform-typeof-symbol": "^7.26.7", - "@babel/plugin-transform-unicode-escapes": "^7.25.9", - "@babel/plugin-transform-unicode-property-regex": "^7.25.9", - "@babel/plugin-transform-unicode-regex": "^7.25.9", - "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.27.1", + "@babel/plugin-transform-async-to-generator": "^7.27.1", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.27.1", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@babel/plugin-transform-class-static-block": "^7.27.1", + "@babel/plugin-transform-classes": "^7.27.1", + "@babel/plugin-transform-computed-properties": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.27.1", + "@babel/plugin-transform-dotall-regex": "^7.27.1", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-exponentiation-operator": "^7.27.1", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.27.1", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-modules-systemjs": "^7.27.1", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", + "@babel/plugin-transform-numeric-separator": "^7.27.1", + "@babel/plugin-transform-object-rest-spread": "^7.27.1", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.1", + "@babel/plugin-transform-private-methods": "^7.27.1", + "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.27.1", + "@babel/plugin-transform-regexp-modifiers": "^7.27.1", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.27.1", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.27.1", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.10", "babel-plugin-polyfill-corejs3": "^0.11.0", @@ -1683,10 +1745,11 @@ } }, "node_modules/@babel/register": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.25.9.tgz", - "integrity": "sha512-8D43jXtGsYmEeDvm4MWHYUpWf8iiXgWYx3fW7E7Wb7Oe6FWqJPl5K6TuFW0dOwNZzEE5rjlaSJYH9JjrUKJszA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.27.1.tgz", + "integrity": "sha512-K13lQpoV54LATKkzBpBAEu1GGSIRzxR9f4IN4V8DCDgiUMo2UDGagEZr3lPeVNJPLkWUi5JE4hCHKneVTwQlYQ==", "dev": true, + "license": "MIT", "dependencies": { "clone-deep": "^4.0.1", "find-cache-dir": "^2.0.0", @@ -1714,32 +1777,32 @@ } }, "node_modules/@babel/template": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", - "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.1.tgz", + "integrity": "sha512-Fyo3ghWMqkHHpHQCoBs2VnYjR4iWFFjguTDEqA5WgZDOrFesVjMhMM2FSqTKSoUSDO1VQtavj8NFpdRBEvJTtg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.27.0", - "@babel/types": "^7.27.0" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", - "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", + "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.27.0", - "@babel/parser": "^7.27.0", - "@babel/template": "^7.27.0", - "@babel/types": "^7.27.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -1748,14 +1811,14 @@ } }, "node_modules/@babel/types": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", - "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", "devOptional": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1908,9 +1971,9 @@ "dev": true }, "node_modules/@cucumber/cucumber": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/@cucumber/cucumber/-/cucumber-11.2.0.tgz", - "integrity": "sha512-F69uIPTc7dfgU7/TGAaQaWUz7r/DzoPW39AfJoKQOC7IvBiPQwpvSIo6QEd+63pdpdKNRbtQoVl5vP9IclhhuA==", + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@cucumber/cucumber/-/cucumber-11.3.0.tgz", + "integrity": "sha512-1YGsoAzRfDyVOnRMTSZP/EcFsOBElOKa2r+5nin0DJAeK+Mp0mzjcmSllMgApGtck7Ji87wwy3kFONfHUHMn4g==", "dev": true, "license": "MIT", "dependencies": { @@ -1918,16 +1981,16 @@ "@cucumber/cucumber-expressions": "18.0.1", "@cucumber/gherkin": "30.0.4", "@cucumber/gherkin-streams": "5.0.1", - "@cucumber/gherkin-utils": "9.0.0", - "@cucumber/html-formatter": "21.7.0", + "@cucumber/gherkin-utils": "9.2.0", + "@cucumber/html-formatter": "21.10.1", "@cucumber/junit-xml-formatter": "0.7.1", "@cucumber/message-streams": "4.0.1", - "@cucumber/messages": "27.0.2", - "@cucumber/tag-expressions": "6.1.1", + "@cucumber/messages": "27.2.0", + "@cucumber/tag-expressions": "6.1.2", "assertion-error-formatter": "^3.0.0", "capital-case": "^1.0.4", "chalk": "^4.1.2", - "cli-table3": "0.6.3", + "cli-table3": "0.6.5", "commander": "^10.0.0", "debug": "^4.3.4", "error-stack-parser": "^2.1.4", @@ -1940,21 +2003,19 @@ "knuth-shuffle-seeded": "^1.0.6", "lodash.merge": "^4.6.2", "lodash.mergewith": "^4.6.2", - "luxon": "3.2.1", + "luxon": "3.6.1", "mime": "^3.0.0", "mkdirp": "^2.1.5", "mz": "^2.7.0", "progress": "^2.0.3", "read-package-up": "^11.0.0", - "resolve-pkg": "^2.0.0", - "semver": "7.5.3", + "semver": "7.7.1", "string-argv": "0.3.1", "supports-color": "^8.1.1", - "tmp": "0.2.3", - "type-fest": "^4.8.3", + "type-fest": "^4.41.0", "util-arity": "^1.1.0", "yaml": "^2.2.2", - "yup": "1.2.0" + "yup": "1.6.1" }, "bin": { "cucumber-js": "bin/cucumber.js" @@ -1977,16 +2038,16 @@ } }, "node_modules/@cucumber/cucumber/node_modules/@cucumber/messages": { - "version": "27.0.2", - "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-27.0.2.tgz", - "integrity": "sha512-jo2B+vYXmpuLOKh6Gc8loHl2E8svCkLvEXLVgFwVHqKWZJWBTa9yTRCPmZIxrz4fnO7Pr3N3vKQCPu73/gjlVQ==", + "version": "27.2.0", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-27.2.0.tgz", + "integrity": "sha512-f2o/HqKHgsqzFLdq6fAhfG1FNOQPdBdyMGpKwhb7hZqg0yZtx9BVqkTyuoNk83Fcvk3wjMVfouFXXHNEk4nddA==", "dev": true, "license": "MIT", "dependencies": { "@types/uuid": "10.0.0", "class-transformer": "0.5.1", "reflect-metadata": "0.2.2", - "uuid": "10.0.0" + "uuid": "11.0.5" } }, "node_modules/@cucumber/cucumber/node_modules/@types/uuid": { @@ -2039,18 +2100,6 @@ "node": ">=8" } }, - "node_modules/@cucumber/cucumber/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@cucumber/cucumber/node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", @@ -2059,13 +2108,11 @@ "license": "Apache-2.0" }, "node_modules/@cucumber/cucumber/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -2074,10 +2121,11 @@ } }, "node_modules/@cucumber/cucumber/node_modules/type-fest": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.8.3.tgz", - "integrity": "sha512-//BaTm14Q/gHBn09xlnKNqfI8t6bmdzx2DXYfPBNofN0WUybCEUDcbCWcTa0oF09lzLjZgPphXAsvRiMK0V6Bw==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" }, @@ -2085,26 +2133,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@cucumber/cucumber/node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@cucumber/cucumber/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@cucumber/gherkin": { "version": "30.0.4", "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-30.0.4.tgz", @@ -2142,15 +2170,16 @@ } }, "node_modules/@cucumber/gherkin-utils": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@cucumber/gherkin-utils/-/gherkin-utils-9.0.0.tgz", - "integrity": "sha512-clk4q39uj7pztZuZtyI54V8lRsCUz0Y/p8XRjIeHh7ExeEztpWkp4ca9q1FjUOPfQQ8E7OgqFbqoQQXZ1Bx7fw==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin-utils/-/gherkin-utils-9.2.0.tgz", + "integrity": "sha512-3nmRbG1bUAZP3fAaUBNmqWO0z0OSkykZZotfLjyhc8KWwDSOrOmMJlBTd474lpA8EWh4JFLAX3iXgynBqBvKzw==", "dev": true, + "license": "MIT", "dependencies": { - "@cucumber/gherkin": "^28.0.0", - "@cucumber/messages": "^24.0.0", + "@cucumber/gherkin": "^31.0.0", + "@cucumber/messages": "^27.0.0", "@teppeis/multimaps": "3.0.0", - "commander": "12.0.0", + "commander": "13.1.0", "source-map-support": "^0.5.21" }, "bin": { @@ -2158,53 +2187,85 @@ } }, "node_modules/@cucumber/gherkin-utils/node_modules/@cucumber/gherkin": { - "version": "28.0.0", - "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-28.0.0.tgz", - "integrity": "sha512-Ee6zJQq0OmIUPdW0mSnsCsrWA2PZAELNDPICD2pLfs0Oz7RAPgj80UsD2UCtqyAhw2qAR62aqlktKUlai5zl/A==", + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-31.0.0.tgz", + "integrity": "sha512-wlZfdPif7JpBWJdqvHk1Mkr21L5vl4EfxVUOS4JinWGf3FLRV6IKUekBv5bb5VX79fkDcfDvESzcQ8WQc07Wgw==", "dev": true, + "license": "MIT", "dependencies": { - "@cucumber/messages": ">=19.1.4 <=24" + "@cucumber/messages": ">=19.1.4 <=26" + } + }, + "node_modules/@cucumber/gherkin-utils/node_modules/@cucumber/gherkin/node_modules/@cucumber/messages": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-26.0.1.tgz", + "integrity": "sha512-DIxSg+ZGariumO+Lq6bn4kOUIUET83A4umrnWmidjGFl8XxkBieUZtsmNbLYgH/gnsmP07EfxxdTr0hOchV1Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/uuid": "10.0.0", + "class-transformer": "0.5.1", + "reflect-metadata": "0.2.2", + "uuid": "10.0.0" + } + }, + "node_modules/@cucumber/gherkin-utils/node_modules/@cucumber/gherkin/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" } }, "node_modules/@cucumber/gherkin-utils/node_modules/@cucumber/messages": { - "version": "24.1.0", - "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-24.1.0.tgz", - "integrity": "sha512-hxVHiBurORcobhVk80I9+JkaKaNXkW6YwGOEFIh/2aO+apAN+5XJgUUWjng9NwqaQrW1sCFuawLB1AuzmBaNdQ==", + "version": "27.2.0", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-27.2.0.tgz", + "integrity": "sha512-f2o/HqKHgsqzFLdq6fAhfG1FNOQPdBdyMGpKwhb7hZqg0yZtx9BVqkTyuoNk83Fcvk3wjMVfouFXXHNEk4nddA==", "dev": true, + "license": "MIT", "dependencies": { - "@types/uuid": "9.0.8", + "@types/uuid": "10.0.0", "class-transformer": "0.5.1", - "reflect-metadata": "0.2.1", - "uuid": "9.0.1" + "reflect-metadata": "0.2.2", + "uuid": "11.0.5" } }, "node_modules/@cucumber/gherkin-utils/node_modules/@types/uuid": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", - "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", - "dev": true + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" }, "node_modules/@cucumber/gherkin-utils/node_modules/commander": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz", - "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@cucumber/gherkin-utils/node_modules/reflect-metadata": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.1.tgz", - "integrity": "sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==", - "deprecated": "This version has a critical bug in fallback handling. Please upgrade to reflect-metadata@0.2.2 or newer.", - "dev": true + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/@cucumber/html-formatter": { - "version": "21.7.0", - "resolved": "https://registry.npmjs.org/@cucumber/html-formatter/-/html-formatter-21.7.0.tgz", - "integrity": "sha512-bv211aY8mErp6CdmhN426E+7KIsVIES4fGx5ASMlUzYWiMus6NhSdI9UL3Vswx8JXJMgySeIcJJKfznREUFLNA==", + "version": "21.10.1", + "resolved": "https://registry.npmjs.org/@cucumber/html-formatter/-/html-formatter-21.10.1.tgz", + "integrity": "sha512-isaaNMNnBYThsvaHy7i+9kkk9V3+rhgdkt0pd6TCY6zY1CSRZQ7tG6ST9pYyRaECyfbCeF7UGH0KpNEnh6UNvQ==", "dev": true, + "license": "MIT", "peerDependencies": { "@cucumber/messages": ">=18" } @@ -2225,16 +2286,6 @@ "@cucumber/messages": "*" } }, - "node_modules/@cucumber/junit-xml-formatter/node_modules/luxon": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", - "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, "node_modules/@cucumber/message-streams": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@cucumber/message-streams/-/message-streams-4.0.1.tgz", @@ -2295,10 +2346,11 @@ } }, "node_modules/@cucumber/tag-expressions": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@cucumber/tag-expressions/-/tag-expressions-6.1.1.tgz", - "integrity": "sha512-0oj5KTzf2DsR3DhL3hYeI9fP3nyKzs7TQdpl54uJelJ3W3Hlyyet2Hib+8LK7kNnqJsXENnJg9zahRYyrtvNEg==", - "dev": true + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@cucumber/tag-expressions/-/tag-expressions-6.1.2.tgz", + "integrity": "sha512-xa3pER+ntZhGCxRXSguDTKEHTZpUUsp+RzTRNnit+vi5cqnk6abLdSLg5i3HZXU3c74nQ8afQC6IT507EN74oQ==", + "dev": true, + "license": "MIT" }, "node_modules/@cypress/browserify-preprocessor": { "version": "3.0.2", @@ -2786,9 +2838,9 @@ } }, "node_modules/@faker-js/faker": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.6.0.tgz", - "integrity": "sha512-3vm4by+B5lvsFPSyep3ELWmZfE3kicDtmemVpuwl1yH7tqtnHdsA6hG8fbXedMVdkzgtvzWoRgjSB4Q+FHnZiw==", + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.8.0.tgz", + "integrity": "sha512-U9wpuSrJC93jZBxx/Qq2wPjCuYISBueyVUGK7qqdmj7r/nxaxwW8AQDCLeRO7wZnjj94sh3p246cAYjUKuqgfg==", "dev": true, "funding": [ { @@ -6597,10 +6649,14 @@ } }, "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==", - "dev": true + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } }, "node_modules/binary-extensions": { "version": "2.2.0", @@ -7248,10 +7304,11 @@ } }, "node_modules/cli-table3": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", - "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==", + "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" }, @@ -7758,9 +7815,9 @@ "optional": true }, "node_modules/cypress": { - "version": "14.2.1", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.2.1.tgz", - "integrity": "sha512-5xd0E7fUp0pjjib1D7ljkmCwFDgMkWuW06jWiz8dKrI7MNRrDo0C65i4Sh+oZ9YHjMHZRJBR0XZk1DfekOhOUw==", + "version": "14.4.1", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.4.1.tgz", + "integrity": "sha512-YSGvVXtTqSGRTyHbaxHI5dHU/9xc5ymaTIM4BU85GKhj980y6XgA3fShSpj5DatS8knXMsAvYItQxVQFHGpUtw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -7778,7 +7835,7 @@ "check-more-types": "^2.24.0", "ci-info": "^4.1.0", "cli-cursor": "^3.1.0", - "cli-table3": "~0.6.5", + "cli-table3": "0.6.1", "commander": "^6.2.1", "common-tags": "^1.8.0", "dayjs": "^1.10.4", @@ -7907,9 +7964,9 @@ } }, "node_modules/cypress/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==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.1.tgz", + "integrity": "sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==", "dev": true, "license": "MIT", "dependencies": { @@ -7919,7 +7976,18 @@ "node": "10.* || >= 12.*" }, "optionalDependencies": { - "@colors/colors": "1.5.0" + "colors": "1.4.0" + } + }, + "node_modules/cypress/node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" } }, "node_modules/cypress/node_modules/commander": { @@ -8477,10 +8545,11 @@ } }, "node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -11668,10 +11737,11 @@ } }, "node_modules/luxon": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.2.1.tgz", - "integrity": "sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", + "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" } @@ -12279,30 +12349,6 @@ "node": ">=14.14" } }, - "node_modules/multiple-cucumber-html-reporter/node_modules/luxon": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", - "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/multiple-cucumber-html-reporter/node_modules/uuid": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", - "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==", - "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -13549,7 +13595,8 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/property-information": { "version": "6.5.0", @@ -14011,13 +14058,15 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/regenerate-unicode-properties": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", "dev": true, + "license": "MIT", "dependencies": { "regenerate": "^1.4.2" }, @@ -14031,15 +14080,6 @@ "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", "dev": true }, - "node_modules/regenerator-transform": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", - "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.8.4" - } - }, "node_modules/regex": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/regex/-/regex-4.3.3.tgz", @@ -14083,15 +14123,16 @@ } }, "node_modules/regexpu-core": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.1.1.tgz", - "integrity": "sha512-k67Nb9jvwJcJmVpw0jPttR1/zVfnKf8Km0IPatrU/zJ5XeG3+Slx0xLXs9HByJSzXzrlz5EDvN6yLNMDc2qdnw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", + "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", "dev": true, + "license": "MIT", "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.0", "regjsgen": "^0.8.0", - "regjsparser": "^0.11.0", + "regjsparser": "^0.12.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.1.0" }, @@ -14103,13 +14144,15 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/regjsparser": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.11.1.tgz", - "integrity": "sha512-1DHODs4B8p/mQHU9kr+jv8+wIC9mtG4eBHxWxIq5mhjE3D5oORhCc6deRKzTjs9DcfRFmj9BHSDguZklqCGFWQ==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "jsesc": "~3.0.2" }, @@ -14246,18 +14289,6 @@ "node": ">=4" } }, - "node_modules/resolve-pkg": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg/-/resolve-pkg-2.0.0.tgz", - "integrity": "sha512-+1lzwXehGCXSeryaISr6WujZzowloigEofRB+dj75y9RRa/obVcYgbHJd53tdYw8pvZj8GojXaaENws8Ktw/hQ==", - "dev": true, - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -14267,15 +14298,6 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/resolve-pkg/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -15463,7 +15485,8 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/tinyglobby": { "version": "0.2.10", @@ -15549,7 +15572,8 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/tough-cookie": { "version": "5.1.2", @@ -15785,6 +15809,7 @@ "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -15794,6 +15819,7 @@ "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", "dev": true, + "license": "MIT", "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", "unicode-property-aliases-ecmascript": "^2.0.0" @@ -15807,6 +15833,7 @@ "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -15816,6 +15843,7 @@ "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -16005,16 +16033,17 @@ "dev": true }, "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", + "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==", "dev": true, "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], + "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/validate-npm-package-license": { @@ -17584,10 +17613,11 @@ } }, "node_modules/yup": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/yup/-/yup-1.2.0.tgz", - "integrity": "sha512-PPqYKSAXjpRCgLgLKVGPA33v5c/WgEx3wi6NFjIiegz90zSwyMpvTFp/uGcVnnbx6to28pgnzp/q8ih3QRjLMQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.6.1.tgz", + "integrity": "sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==", "dev": true, + "license": "MIT", "dependencies": { "property-expr": "^2.0.5", "tiny-case": "^1.0.3", @@ -17600,6 +17630,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=12.20" }, diff --git a/package.json b/package.json index dd27c53ca..e0fa8c292 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ocelot-social", - "version": "3.2.1", + "version": "3.8.2", "description": "Free and open source software program code available to run social networks.", "author": "ocelot.social Community", "license": "MIT", @@ -33,20 +33,20 @@ "release": "./scripts/release.sh" }, "devDependencies": { - "@babel/core": "^7.26.10", - "@babel/preset-env": "^7.26.9", - "@babel/register": "^7.25.9", + "@babel/core": "^7.27.1", + "@babel/preset-env": "^7.27.1", + "@babel/register": "^7.27.1", "@badeball/cypress-cucumber-preprocessor": "^22.0.1", - "@cucumber/cucumber": "11.2.0", + "@cucumber/cucumber": "11.3.0", "@cypress/browserify-preprocessor": "^3.0.2", - "@faker-js/faker": "9.6.0", + "@faker-js/faker": "9.8.0", "auto-changelog": "^2.5.0", - "bcryptjs": "^2.4.3", + "bcryptjs": "^3.0.2", "cross-env": "^7.0.3", - "cypress": "^14.2.1", + "cypress": "^14.4.1", "cypress-network-idle": "^1.15.0", "date-fns": "^3.6.0", - "dotenv": "^16.4.7", + "dotenv": "^16.5.0", "expect": "^29.6.4", "graphql-request": "^2.0.0", "import": "^0.0.6", diff --git a/scripts/release.sh b/scripts/release.sh index 453375203..c078d6b04 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -26,4 +26,4 @@ sed -i -e 's/appVersion: ".*"/appVersion: "'"$VERSION_NEW"'"/g' $ROOT_DIR/deploy # generate changelog cd $ROOT_DIR -yarn run auto-changelog --latest-version $VERSION_NEW \ No newline at end of file +yarn run auto-changelog --commit-limit 0 --latest-version $VERSION_NEW \ No newline at end of file diff --git a/webapp/.env.template b/webapp/.env.template index 76bc502f9..344bf8b43 100644 --- a/webapp/.env.template +++ b/webapp/.env.template @@ -5,4 +5,10 @@ GRAPHQL_URI=http://localhost:4000/ MAPBOX_TOKEN="pk.eyJ1IjoiYnVzZmFrdG9yIiwiYSI6ImNraDNiM3JxcDBhaWQydG1uczhpZWtpOW4ifQ.7TNRTO-o9aK1Y6MyW_Nd4g" PUBLIC_REGISTRATION=false INVITE_REGISTRATION=true -CATEGORIES_ACTIVE=false +BADGES_ENABLED=true +INVITE_LINK_LIMIT=7 +NETWORK_NAME="Ocelot.social" + +ASK_FOR_REAL_NAME=false + +REQUIRE_LOCATION=false \ No newline at end of file diff --git a/webapp/Dockerfile b/webapp/Dockerfile index 7ec65cbf9..70b44d388 100644 --- a/webapp/Dockerfile +++ b/webapp/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20.12.1-alpine3.19 AS base +FROM node:20.12.1-alpine AS base LABEL org.label-schema.name="ocelot.social:webapp" LABEL org.label-schema.description="Web Frontend of the Social Network Software ocelot.social" LABEL org.label-schema.usage="https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/master/README.md" @@ -37,7 +37,23 @@ ONBUILD RUN cp -r ./locales /build ONBUILD RUN cp -r ./package.json ./yarn.lock /build ONBUILD RUN cd /build && yarn install --production=true --frozen-lockfile --non-interactive -FROM build AS test +FROM base AS test_build +ENV NODE_ENV=test +COPY . . +RUN tools/merge-locales.sh +RUN yarn install --production=false --frozen-lockfile --non-interactive +RUN yarn run build +RUN mkdir /build +RUN cp -r ./.nuxt /build +RUN cp -r ./nuxt.config.js /build +RUN cp -r ./config/ /build +RUN cp -r ./constants /build +RUN cp -r ./static /build +RUN cp -r ./locales /build +RUN cp -r ./package.json ./yarn.lock /build +RUN cd /build && yarn install --frozen-lockfile --non-interactive + +FROM test_build AS test CMD ["/bin/bash", "-c", "yarn run dev"] FROM build AS production_build diff --git a/webapp/Dockerfile.maintenance b/webapp/Dockerfile.maintenance index 93d104fd5..bc930f7d0 100644 --- a/webapp/Dockerfile.maintenance +++ b/webapp/Dockerfile.maintenance @@ -8,7 +8,7 @@ LABEL org.label-schema.vendor="ocelot.social Community" LABEL org.label-schema.schema-version="1.0" LABEL maintainer="devops@ocelot.social" -FROM node:20.12.1-alpine3.19 AS build +FROM node:20.12.1-alpine AS build ENV NODE_ENV="production" RUN apk --no-cache add git python3 make g++ bash jq RUN mkdir -p /app @@ -36,6 +36,6 @@ ONBUILD RUN yarn run generate FROM build AS production_build -FROM base as production +FROM base AS production COPY --from=production_build ./app/dist/ /usr/share/nginx/html/ COPY --from=production_build ./app/maintenance/nginx/custom.conf /etc/nginx/conf.d/default.conf diff --git a/webapp/__mocks__/vue/index.js b/webapp/__mocks__/vue/index.js new file mode 100644 index 000000000..8d3322f81 --- /dev/null +++ b/webapp/__mocks__/vue/index.js @@ -0,0 +1,6 @@ +import Vue from 'vue' + +Vue.config.productionTip = false +Vue.config.devtools = false + +export default Vue diff --git a/webapp/assets/_new/styles/export.scss b/webapp/assets/_new/styles/export.scss index e29c014e2..c8f16b750 100644 --- a/webapp/assets/_new/styles/export.scss +++ b/webapp/assets/_new/styles/export.scss @@ -20,6 +20,10 @@ backgroundColorPrimary: $background-color-primary; colorNeutral30: $color-neutral-30; + + chatSidemenuBg: $chat-sidemenu-bg; + chatSidemenuBackgroundOver: $chat-sidemenu-background-over; + chatSidemenuBackgroundActive: $chat-sidemenu-background-active; chatMessageColor: $chat-message-color; @@ -34,4 +38,8 @@ chatRoomBackgroundCounterBadge: $chat-room-background-counter-badge; chatRoomColorCounterBadge: $chat-room-color-counter-badge; + + chatIconAdd: $chat-icon-add; + chatIconSend: $chat-icon-send; + chatIconEmoji: $chat-icon-emoji; } \ No newline at end of file diff --git a/webapp/assets/_new/styles/tokens.scss b/webapp/assets/_new/styles/tokens.scss index dd3a042d1..42c01d3a8 100644 --- a/webapp/assets/_new/styles/tokens.scss +++ b/webapp/assets/_new/styles/tokens.scss @@ -150,7 +150,7 @@ $font-size-xx-large: 2rem; $font-size-x-large: 1.5rem; $font-size-large: 1.25rem; $font-size-base: 1rem; -$font-size-body: 15px; +$font-size-body: 0.938rem; $font-size-small: 0.8rem; $font-size-x-small: 0.7rem; $font-size-xx-small: 0.6rem; @@ -359,37 +359,37 @@ $media-query-medium: (min-width: 768px); $media-query-large: (min-width: 1024px); $media-query-x-large: (min-width: 1200px); -/** +/** * @tokens Background Images */ -/** +/** * @tokens Header Color */ $color-header-background: $color-neutral-100; -/** +/** * @tokens Footer Color */ $color-footer-background: $color-neutral-100; $color-footer-link: $color-primary; -/** +/** * @tokens Locale Menu Color */ $color-locale-menu: $text-color-soft; -/** +/** * @tokens Donation Bar Color */ $color-donation-bar: $color-primary; $color-donation-bar-light: $color-primary-light; -/** +/** * @tokens Toast Color */ @@ -403,10 +403,12 @@ $color-toast-green: $color-success; * @tokens Ribbon Color */ -$color-ribbon-event: $background-color-third; -$color-ribbon-event-active: $background-color-third-active; -$color-ribbon-article: $background-color-secondary; -$color-ribbon-article-active: $background-color-secondary-active; +$color-ribbon-announcement: $background-color-warning; +$color-ribbon-announcement-shadow: color.adjust($color-ribbon-announcement, $lightness: -20%,); +$color-ribbon-article: $background-color-secondary-active; +$color-ribbon-article-shadow: color.adjust($color-ribbon-article, $lightness: -20%); +$color-ribbon-event: $background-color-third-active; +$color-ribbon-event-shadow: color.adjust($color-ribbon-event, $lightness: -20%); /** * @tokens Chat Color @@ -415,10 +417,15 @@ $color-ribbon-article-active: $background-color-secondary-active; $chat-message-bg-me: $color-primary-light; $chat-message-color: $text-color-base; $chat-message-bg-others: $color-neutral-80; -$chat-sidemenu-bg: $color-secondary-active; +$chat-sidemenu-bg: white; +$chat-sidemenu-background-over: '#f6f6f6'; +$chat-sidemenu-background-active: $color-primary-light; $chat-new-message-color: $color-secondary-active; $chat-message-timestamp: $text-color-soft; $chat-message-checkmark-seen: $text-color-secondary; $chat-message-checkmark: $text-color-soft; $chat-room-color-counter-badge: $text-color-inverse; $chat-room-background-counter-badge: $color-secondary; +$chat-icon-add: $color-primary; +$chat-icon-send: $color-primary; +$chat-icon-emoji: $color-primary; diff --git a/webapp/assets/_new/styles/uses.scss b/webapp/assets/_new/styles/uses.scss new file mode 100644 index 000000000..37ad2e69c --- /dev/null +++ b/webapp/assets/_new/styles/uses.scss @@ -0,0 +1,4 @@ +// we can not use the command '@use' in 'tokens.scss' because of the compiler error 'SassError: @use rules must be written before any other rules.' +// therefore we integrate this file at first in 'nuxt.config.js' + +@use 'sass:color'; diff --git a/webapp/assets/styles/main.scss b/webapp/assets/styles/main.scss index b726758c7..fdf5c4240 100644 --- a/webapp/assets/styles/main.scss +++ b/webapp/assets/styles/main.scss @@ -1,6 +1,10 @@ @import './imports/_tooltip.scss'; @import './imports/_toast.scss'; +html { + scrollbar-gutter: stable; +} + // Transition Easing $easeOut: cubic-bezier(0.19, 1, 0.22, 1); @@ -52,11 +56,6 @@ $easeOut: cubic-bezier(0.19, 1, 0.22, 1); background: #fff; } -body.dropdown-open { - height: 100vh; - overflow: hidden; -} - blockquote { display: block; padding: 15px 20px 15px 45px; @@ -140,9 +139,18 @@ hr { opacity: 1; transition-delay: 0; transition: opacity 80ms ease-out; + + @media(hover: none) { + pointer-events: all; + } } } +body.dropdown-open { + max-height: 100vh; + overflow: hidden; +} + .base-card > .ds-section { padding: 0; margin: -$space-base; @@ -155,7 +163,6 @@ hr { [class$='menu-popover'] { min-width: 130px; - a, button { display: flex; align-content: center; @@ -179,4 +186,12 @@ hr { .dropdown-arrow { font-size: $font-size-xx-small; -} \ No newline at end of file +} + +/* Prevent ds-select overflow */ +.ds-select-value { + max-height: 38px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/webapp/components/ActionButton.spec.js b/webapp/components/ActionButton.spec.js new file mode 100644 index 000000000..3889d26db --- /dev/null +++ b/webapp/components/ActionButton.spec.js @@ -0,0 +1,64 @@ +import { render, screen, fireEvent } from '@testing-library/vue' +import '@testing-library/jest-dom' +import ActionButton from './ActionButton.vue' + +const localVue = global.localVue + +describe('ActionButton.vue', () => { + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn((t) => t), + } + }) + + let wrapper + const Wrapper = ({ isDisabled = false } = {}) => { + return render(ActionButton, { + mocks, + localVue, + propsData: { + icon: 'heart', + text: 'Click me', + count: 7, + disabled: isDisabled, + }, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + describe('when not disabled', () => { + it('renders', () => { + const wrapper = Wrapper() + expect(wrapper.container).toMatchSnapshot() + }) + + it('shows count', () => { + const count = screen.getByText('7') + expect(count).toBeInTheDocument() + }) + + it('button emits click event', async () => { + const button = screen.getByRole('button') + await fireEvent.click(button) + expect(wrapper.emitted().click).toEqual([[]]) + }) + }) + + describe('when disabled', () => { + it('renders', () => { + const wrapper = Wrapper({ isDisabled: true }) + expect(wrapper.container).toMatchSnapshot() + }) + + it('button does not emit click event', async () => { + const button = screen.getByRole('button') + await fireEvent.click(button) + expect(wrapper.emitted().click).toEqual([[]]) + }) + }) +}) diff --git a/webapp/components/ActionButton.vue b/webapp/components/ActionButton.vue new file mode 100644 index 000000000..f440deff8 --- /dev/null +++ b/webapp/components/ActionButton.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/webapp/components/AvatarMenu/AvatarMenu.vue b/webapp/components/AvatarMenu/AvatarMenu.vue index ac583ed4b..d19f0bf95 100644 --- a/webapp/components/AvatarMenu/AvatarMenu.vue +++ b/webapp/components/AvatarMenu/AvatarMenu.vue @@ -168,10 +168,10 @@ export default { background-color: $color-neutral-90; } .logout-link { - color: $text-color-base; + color: $text-color-danger; padding-top: $space-xx-small; &:hover { - color: $text-color-link-active; + color: color.adjust($text-color-danger, $lightness: -10%); } } } diff --git a/webapp/components/BadgeSelection.spec.js b/webapp/components/BadgeSelection.spec.js new file mode 100644 index 000000000..78f00b87a --- /dev/null +++ b/webapp/components/BadgeSelection.spec.js @@ -0,0 +1,76 @@ +import { render, screen, fireEvent } from '@testing-library/vue' +import BadgeSelection from './BadgeSelection.vue' + +const localVue = global.localVue + +describe('Badges.vue', () => { + const Wrapper = (propsData) => { + return render(BadgeSelection, { + propsData, + localVue, + }) + } + + describe('without badges', () => { + it('renders', () => { + const wrapper = Wrapper({ badges: [] }) + expect(wrapper.container).toMatchSnapshot() + }) + }) + + describe('with badges', () => { + const badges = [ + { + id: '1', + icon: '/path/to/some/icon', + isDefault: false, + description: 'Some description', + }, + { + id: '2', + icon: '/path/to/another/icon', + isDefault: true, + description: 'Another description', + }, + { + id: '3', + icon: '/path/to/third/icon', + isDefault: false, + description: 'Third description', + }, + ] + + let wrapper + + beforeEach(() => { + wrapper = Wrapper({ badges }) + }) + + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + describe('clicking on a badge', () => { + beforeEach(async () => { + const badge = screen.getByText(badges[1].description) + await fireEvent.click(badge) + }) + + it('emits badge-selected with badge', async () => { + expect(wrapper.emitted()['badge-selected']).toEqual([[badges[1]]]) + }) + }) + + describe('clicking twice on a badge', () => { + beforeEach(async () => { + const badge = screen.getByText(badges[1].description) + await fireEvent.click(badge) + await fireEvent.click(badge) + }) + + it('emits badge-selected with null', async () => { + expect(wrapper.emitted()['badge-selected']).toEqual([[badges[1]], [null]]) + }) + }) + }) +}) diff --git a/webapp/components/BadgeSelection.vue b/webapp/components/BadgeSelection.vue new file mode 100644 index 000000000..a6554d779 --- /dev/null +++ b/webapp/components/BadgeSelection.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/webapp/components/Badges.spec.js b/webapp/components/Badges.spec.js index d19c2beb2..ae15e0b0a 100644 --- a/webapp/components/Badges.spec.js +++ b/webapp/components/Badges.spec.js @@ -1,29 +1,114 @@ -import { shallowMount } from '@vue/test-utils' +import { render, screen, fireEvent } from '@testing-library/vue' import Badges from './Badges.vue' +const localVue = global.localVue + describe('Badges.vue', () => { - let propsData + const Wrapper = (propsData) => { + return render(Badges, { + propsData, + localVue, + }) + } - beforeEach(() => { - propsData = {} - }) - - describe('shallowMount', () => { - const Wrapper = () => { - return shallowMount(Badges, { propsData }) - } - - it('has class "hc-badges"', () => { - expect(Wrapper().find('.hc-badges').exists()).toBe(true) + describe('without badges', () => { + it('renders in presentation mode', () => { + const wrapper = Wrapper({ badges: [], selectionMode: false }) + expect(wrapper.container).toMatchSnapshot() }) - describe('given a badge', () => { + it('renders in selection mode', () => { + const wrapper = Wrapper({ badges: [], selectionMode: true }) + expect(wrapper.container).toMatchSnapshot() + }) + }) + + describe('with badges', () => { + const badges = [ + { + id: '1', + icon: '/path/to/some/icon', + isDefault: false, + description: 'Some description', + }, + { + id: '2', + icon: '/path/to/another/icon', + isDefault: true, + description: 'Another description', + }, + { + id: '3', + icon: '/path/to/third/icon', + isDefault: false, + description: 'Third description', + }, + ] + + describe('in presentation mode', () => { + let wrapper + beforeEach(() => { - propsData.badges = [{ id: '1', icon: '/path/to/some/icon' }] + wrapper = Wrapper({ badges, scale: 1.2, selectionMode: false }) }) - it('proxies badge icon, which is just a URL without metadata', () => { - expect(Wrapper().find('img[src="/api/path/to/some/icon"]').exists()).toBe(true) + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + it('clicking on second badge does nothing', async () => { + const badge = screen.getByTitle(badges[1].description) + await fireEvent.click(badge) + expect(wrapper.emitted()).toEqual({}) + }) + }) + + describe('in selection mode', () => { + let wrapper + + beforeEach(() => { + wrapper = Wrapper({ badges, scale: 1.2, selectionMode: true }) + }) + + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + it('clicking on first badge does nothing', async () => { + const badge = screen.getByTitle(badges[0].description) + await fireEvent.click(badge) + expect(wrapper.emitted()).toEqual({}) + }) + + describe('clicking on second badge', () => { + beforeEach(async () => { + const badge = screen.getByTitle(badges[1].description) + await fireEvent.click(badge) + }) + + it('selects badge', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + it('emits badge-selected with index', async () => { + expect(wrapper.emitted()['badge-selected']).toEqual([[1]]) + }) + }) + + describe('clicking twice on second badge', () => { + beforeEach(async () => { + const badge = screen.getByTitle(badges[1].description) + await fireEvent.click(badge) + await fireEvent.click(badge) + }) + + it('deselects badge', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + it('emits badge-selected with null', async () => { + expect(wrapper.emitted()['badge-selected']).toEqual([[1], [null]]) + }) }) }) }) diff --git a/webapp/components/Badges.vue b/webapp/components/Badges.vue index d569452c7..0fdf065c3 100644 --- a/webapp/components/Badges.vue +++ b/webapp/components/Badges.vue @@ -1,69 +1,173 @@ diff --git a/webapp/components/CategoriesSelect/CategoriesSelect.spec.js b/webapp/components/CategoriesSelect/CategoriesSelect.spec.js index 82f5e61eb..6d379fbb8 100644 --- a/webapp/components/CategoriesSelect/CategoriesSelect.spec.js +++ b/webapp/components/CategoriesSelect/CategoriesSelect.spec.js @@ -1,6 +1,6 @@ import { mount } from '@vue/test-utils' import CategoriesSelect from './CategoriesSelect' -import Vue from 'vue' +import Vuex from 'vuex' const localVue = global.localVue @@ -12,7 +12,6 @@ describe('CategoriesSelect.vue', () => { let environmentAndNature let consumptionAndSustainablity - const propsData = { model: 'categoryIds' } const categories = [ { id: 'cat9', @@ -35,6 +34,20 @@ describe('CategoriesSelect.vue', () => { id: 'cat8', }, ] + + const propsData = { model: 'categoryIds' } + const categoriesMock = jest.fn().mockReturnValue(categories) + + const storeMocks = { + getters: { + 'categories/categories': categoriesMock, + 'categories/isInitialized': jest.fn(() => true), + }, + actions: { + 'categories/init': jest.fn(), + }, + } + beforeEach(() => { provide = { $parentForm: { @@ -48,7 +61,8 @@ describe('CategoriesSelect.vue', () => { describe('shallowMount', () => { const Wrapper = () => { - return mount(CategoriesSelect, { propsData, mocks, localVue, provide }) + const store = new Vuex.Store(storeMocks) + return mount(CategoriesSelect, { propsData, mocks, localVue, provide, store }) } beforeEach(() => { @@ -56,9 +70,7 @@ describe('CategoriesSelect.vue', () => { }) describe('toggleCategory', () => { - beforeEach(async () => { - wrapper.vm.categories = categories - await Vue.nextTick() + beforeEach(() => { democracyAndPolitics = wrapper.findAll('button').at(0) democracyAndPolitics.trigger('click') }) diff --git a/webapp/components/CategoriesSelect/CategoriesSelect.vue b/webapp/components/CategoriesSelect/CategoriesSelect.vue index 91fb7704c..47149bcfd 100644 --- a/webapp/components/CategoriesSelect/CategoriesSelect.vue +++ b/webapp/components/CategoriesSelect/CategoriesSelect.vue @@ -1,7 +1,7 @@ - +
+ + +
@@ -59,6 +69,7 @@ import ContentMenu from '~/components/ContentMenu/ContentMenu' import ContentViewer from '~/components/Editor/ContentViewer' import CommentForm from '~/components/CommentForm/CommentForm' import CommentMutations from '~/graphql/CommentMutations' +import ShoutButton from '~/components/ShoutButton.vue' import scrollToAnchor from '~/mixins/scrollToAnchor.js' export default { @@ -67,6 +78,7 @@ export default { ContentMenu, ContentViewer, CommentForm, + ShoutButton, }, mixins: [scrollToAnchor], data() { @@ -98,6 +110,11 @@ export default { hasLongContent() { return this.$filters.removeHtml(this.comment.content).length > COMMENT_MAX_UNTRUNCATED_LENGTH }, + isAuthor() { + const { author } = this.comment + if (!author) return false + return this.$store.getters['auth/user'].id === author.id + }, isUnavailable() { return (this.comment.deleted || this.comment.disabled) && !this.isModerator }, @@ -192,19 +209,14 @@ export default { margin-bottom: $space-small; } - > .base-button { - align-self: flex-end; + .actions { + display: flex; + width: 100%; + justify-content: space-between; + align-items: center; } } -.reply-button { - float: right; - top: 0px; -} -.reply-button:after { - clear: both; -} - @keyframes highlight { 0% { border: $border-size-base solid $color-primary; @@ -214,3 +226,17 @@ export default { } } + + diff --git a/webapp/components/ContentMenu/ContentMenu.spec.js b/webapp/components/ContentMenu/ContentMenu.spec.js index ce7a45a42..ab45240f4 100644 --- a/webapp/components/ContentMenu/ContentMenu.spec.js +++ b/webapp/components/ContentMenu/ContentMenu.spec.js @@ -16,7 +16,10 @@ const stubs = { }, } -let getters, mutations, mocks, menuToggle, openModalSpy +let getters, mutations, actions, mocks, menuToggle, openModalSpy + +const maxPinnedPostsMock = jest.fn() +const currentlyPinnedPostsMock = jest.fn() describe('ContentMenu.vue', () => { beforeEach(() => { @@ -38,10 +41,15 @@ describe('ContentMenu.vue', () => { getters = { 'auth/isModerator': () => false, 'auth/isAdmin': () => false, + 'pinnedPosts/maxPinnedPosts': maxPinnedPostsMock, + 'pinnedPosts/currentlyPinnedPosts': currentlyPinnedPostsMock, + } + actions = { + 'pinnedPosts/fetch': jest.fn(), } const openContentMenu = async (values = {}) => { - const store = new Vuex.Store({ mutations, getters }) + const store = new Vuex.Store({ mutations, getters, actions }) const wrapper = mount(ContentMenu, { propsData: { ...values, @@ -91,55 +99,359 @@ describe('ContentMenu.vue', () => { }) describe('admin can', () => { - it('pin unpinned post', async () => { + it('push post', async () => { getters['auth/isAdmin'] = () => true const wrapper = await openContentMenu({ isOwner: false, resourceType: 'contribution', resource: { id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8', - pinnedBy: null, + sortDate: 'some-date', + createdAt: 'some-date', }, }) + expect( + wrapper.findAll('.ds-menu-item').filter((item) => item.text() === 'post.menu.push'), + ).toHaveLength(1) wrapper .findAll('.ds-menu-item') - .filter((item) => item.text() === 'post.menu.pin') + .filter((item) => item.text() === 'post.menu.push') .at(0) .trigger('click') - expect(wrapper.emitted('pinPost')).toEqual([ + expect(wrapper.emitted('pushPost')).toEqual([ [ { id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8', - pinnedBy: null, + sortDate: 'some-date', + createdAt: 'some-date', }, ], ]) }) - it('unpin pinned post', async () => { + it('not unpush post which was not pushed', async () => { + getters['auth/isAdmin'] = () => true const wrapper = await openContentMenu({ isOwner: false, resourceType: 'contribution', resource: { id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8', - pinnedBy: 'someone', + sortDate: 'some-date', + createdAt: 'some-date', }, }) + expect( + wrapper.findAll('.ds-menu-item').filter((item) => item.text() === 'post.menu.unpush'), + ).toHaveLength(0) + }) + + it('unpush post which was pushed', async () => { + getters['auth/isAdmin'] = () => true + const wrapper = await openContentMenu({ + isOwner: false, + resourceType: 'contribution', + resource: { + id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8', + sortDate: 'some-date', + createdAt: 'some-other-date', + }, + }) + expect( + wrapper.findAll('.ds-menu-item').filter((item) => item.text() === 'post.menu.unpush'), + ).toHaveLength(1) wrapper .findAll('.ds-menu-item') - .filter((item) => item.text() === 'post.menu.unpin') + .filter((item) => item.text() === 'post.menu.unpush') .at(0) .trigger('click') - expect(wrapper.emitted('unpinPost')).toEqual([ + expect(wrapper.emitted('unpushPost')).toEqual([ [ { id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8', - pinnedBy: 'someone', + sortDate: 'some-date', + createdAt: 'some-other-date', }, ], ]) }) + describe('when maxPinnedPosts = 0', () => { + beforeEach(() => { + maxPinnedPostsMock.mockReturnValue(0) + }) + + it('not pin unpinned post', async () => { + getters['auth/isAdmin'] = () => true + const wrapper = await openContentMenu({ + isOwner: false, + resourceType: 'contribution', + resource: { + id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8', + pinnedBy: null, + }, + }) + expect( + wrapper.findAll('.ds-menu-item').filter((item) => item.text() === 'post.menu.pin'), + ).toHaveLength(0) + }) + + it('unpin pinned post', async () => { + const wrapper = await openContentMenu({ + isOwner: false, + resourceType: 'contribution', + resource: { + id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8', + pinnedBy: 'someone', + }, + }) + wrapper + .findAll('.ds-menu-item') + .filter((item) => item.text() === 'post.menu.unpin') + .at(0) + .trigger('click') + expect(wrapper.emitted('unpinPost')).toEqual([ + [ + { + id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8', + pinnedBy: 'someone', + }, + ], + ]) + }) + }) + + describe('when maxPinnedPosts = 1', () => { + beforeEach(() => { + maxPinnedPostsMock.mockReturnValue(1) + }) + + it('pin unpinned post', async () => { + getters['auth/isAdmin'] = () => true + const wrapper = await openContentMenu({ + isOwner: false, + resourceType: 'contribution', + resource: { + id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8', + pinnedBy: null, + }, + }) + wrapper + .findAll('.ds-menu-item') + .filter((item) => item.text() === 'post.menu.pin') + .at(0) + .trigger('click') + expect(wrapper.emitted('pinPost')).toEqual([ + [ + { + id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8', + pinnedBy: null, + }, + ], + ]) + }) + + it('unpin pinned post', async () => { + const wrapper = await openContentMenu({ + isOwner: false, + resourceType: 'contribution', + resource: { + id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8', + pinnedBy: 'someone', + }, + }) + wrapper + .findAll('.ds-menu-item') + .filter((item) => item.text() === 'post.menu.unpin') + .at(0) + .trigger('click') + expect(wrapper.emitted('unpinPost')).toEqual([ + [ + { + id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8', + pinnedBy: 'someone', + }, + ], + ]) + }) + + describe('post in public group', () => { + it('can pin unpinned post', async () => { + getters['auth/isAdmin'] = () => true + const wrapper = await openContentMenu({ + isOwner: false, + resourceType: 'contribution', + resource: { + id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8', + pinnedBy: null, + group: { + groupType: 'public', + }, + }, + }) + wrapper + .findAll('.ds-menu-item') + .filter((item) => item.text() === 'post.menu.pin') + .at(0) + .trigger('click') + expect(wrapper.emitted('pinPost')).toEqual([ + [ + { + id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8', + pinnedBy: null, + group: { + groupType: 'public', + }, + }, + ], + ]) + }) + }) + + describe('post in closed group', () => { + it('can not be pinned', async () => { + getters['auth/isAdmin'] = () => true + const wrapper = await openContentMenu({ + isOwner: false, + resourceType: 'contribution', + resource: { + id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8', + pinnedBy: null, + group: { + groupType: 'closed', + }, + }, + }) + expect( + wrapper.findAll('.ds-menu-item').filter((item) => item.text() === 'post.menu.pin'), + ).toHaveLength(0) + }) + }) + + describe('post in hidden group', () => { + it('can not be pinned', async () => { + getters['auth/isAdmin'] = () => true + const wrapper = await openContentMenu({ + isOwner: false, + resourceType: 'contribution', + resource: { + id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8', + pinnedBy: null, + group: { + groupType: 'hidden', + }, + }, + }) + expect( + wrapper.findAll('.ds-menu-item').filter((item) => item.text() === 'post.menu.pin'), + ).toHaveLength(0) + }) + }) + }) + + describe('when maxPinnedPosts = 3', () => { + describe('and max is not reached', () => { + beforeEach(() => { + maxPinnedPostsMock.mockReturnValue(3) + currentlyPinnedPostsMock.mockReturnValue(2) + }) + + it('pin unpinned post', async () => { + getters['auth/isAdmin'] = () => true + const wrapper = await openContentMenu({ + isOwner: false, + resourceType: 'contribution', + resource: { + id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8', + pinnedBy: null, + }, + }) + wrapper + .findAll('.ds-menu-item') + .filter((item) => item.text() === 'post.menu.pin') + .at(0) + .trigger('click') + expect(wrapper.emitted('pinPost')).toEqual([ + [ + { + id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8', + pinnedBy: null, + }, + ], + ]) + }) + + it('unpin pinned post', async () => { + const wrapper = await openContentMenu({ + isOwner: false, + resourceType: 'contribution', + resource: { + id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8', + pinnedBy: 'someone', + }, + }) + wrapper + .findAll('.ds-menu-item') + .filter((item) => item.text() === 'post.menu.unpin') + .at(0) + .trigger('click') + expect(wrapper.emitted('unpinPost')).toEqual([ + [ + { + id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8', + pinnedBy: 'someone', + }, + ], + ]) + }) + }) + + describe('and max is reached', () => { + beforeEach(() => { + maxPinnedPostsMock.mockReturnValue(3) + currentlyPinnedPostsMock.mockReturnValue(3) + }) + + it('not pin unpinned post', async () => { + getters['auth/isAdmin'] = () => true + const wrapper = await openContentMenu({ + isOwner: false, + resourceType: 'contribution', + resource: { + id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8', + pinnedBy: null, + }, + }) + expect( + wrapper.findAll('.ds-menu-item').filter((item) => item.text() === 'post.menu.pin'), + ).toHaveLength(0) + }) + + it('unpin pinned post', async () => { + const wrapper = await openContentMenu({ + isOwner: false, + resourceType: 'contribution', + resource: { + id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8', + pinnedBy: 'someone', + }, + }) + wrapper + .findAll('.ds-menu-item') + .filter((item) => item.text() === 'post.menu.unpin') + .at(0) + .trigger('click') + expect(wrapper.emitted('unpinPost')).toEqual([ + [ + { + id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8', + pinnedBy: 'someone', + }, + ], + ]) + }) + }) + }) + it('can delete another user', async () => { getters['auth/user'] = () => { return { id: 'some-user', slug: 'some-user' } diff --git a/webapp/components/ContentMenu/ContentMenu.vue b/webapp/components/ContentMenu/ContentMenu.vue index 627e5d982..dc765c4a3 100644 --- a/webapp/components/ContentMenu/ContentMenu.vue +++ b/webapp/components/ContentMenu/ContentMenu.vue @@ -32,12 +32,14 @@ diff --git a/webapp/components/InviteButton/InviteButton.spec.js b/webapp/components/InviteButton/InviteButton.spec.js deleted file mode 100644 index 1282c2bad..000000000 --- a/webapp/components/InviteButton/InviteButton.spec.js +++ /dev/null @@ -1,59 +0,0 @@ -import { mount } from '@vue/test-utils' -import InviteButton from './InviteButton.vue' - -const localVue = global.localVue - -const stubs = { - 'v-popover': { - template: '', - }, -} - -describe('InviteButton.vue', () => { - let wrapper - let mocks - let propsData - - beforeEach(() => { - mocks = { - $t: jest.fn(), - navigator: { - clipboard: { - writeText: jest.fn(), - }, - }, - } - propsData = {} - }) - - describe('mount', () => { - const Wrapper = () => { - return mount(InviteButton, { mocks, localVue, propsData, stubs }) - } - - beforeEach(() => { - wrapper = Wrapper() - }) - - it('renders', () => { - expect(wrapper.find('.invite-button').exists()).toBe(true) - }) - - it('open popup', () => { - wrapper.find('.base-button').trigger('click') - expect(wrapper.find('.invite-button').exists()).toBe(true) - }) - - it('invite codes not available', async () => { - wrapper.find('.base-button').trigger('click') // open popup - wrapper.find('.invite-button').trigger('click') // click copy button - expect(mocks.$t).toHaveBeenCalledWith('invite-codes.not-available') - }) - - it.skip('invite codes copied to clipboard', async () => { - wrapper.find('.base-button').trigger('click') // open popup - wrapper.find('.invite-button').trigger('click') // click copy button - expect(mocks.$t).toHaveBeenCalledWith('invite-codes.not-available') - }) - }) -}) diff --git a/webapp/components/InviteButton/InviteButton.vue b/webapp/components/InviteButton/InviteButton.vue index 3042a706a..51d8c3ff6 100644 --- a/webapp/components/InviteButton/InviteButton.vue +++ b/webapp/components/InviteButton/InviteButton.vue @@ -1,5 +1,5 @@ @@ -66,14 +87,14 @@ import { } from '~/graphql/User' import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon' import Dropdown from '~/components/Dropdown' -import NotificationList from '../NotificationList/NotificationList' +import NotificationsTable from '../NotificationsTable/NotificationsTable.vue' export default { name: 'NotificationMenu', components: { + NotificationsTable, CounterIcon, Dropdown, - NotificationList, }, data() { return { @@ -82,15 +103,27 @@ export default { }, props: { placement: { type: String }, + noMenu: { type: Boolean, default: false }, + }, + mounted() { + window.addEventListener('resize', this.handleResize) + }, + beforeDestroy() { + window.removeEventListener('resize', this.handleResize) }, methods: { - async markAsRead(notificationSourceId) { - const variables = { id: notificationSourceId } + handleResize() { + // When the viewport get resized close menu + this.$refs?.dropdown?.closeMenu?.() + }, + async markAsReadAndCloseMenu(notificationSourceId, closeMenu) { try { await this.$apollo.mutate({ mutation: markAsReadMutation(this.$i18n), - variables, + variables: { id: notificationSourceId }, }) + + closeMenu?.() } catch (error) { this.$toast.error(error.message) } @@ -100,7 +133,7 @@ export default { return } - closeMenu() + closeMenu?.() try { await this.$apollo.mutate({ mutation: markAllAsReadMutation(this.$i18n), @@ -127,7 +160,7 @@ export default { apollo: { notifications: { query() { - return notificationQuery(this.$i18n) + return notificationQuery() }, variables() { return { @@ -165,12 +198,8 @@ export default { } .notifications-link-container { background-color: $background-color-softer-active; - justify-content: center; - padding: $space-x-small; + text-align: right; + padding: $space-x-small 0; flex-direction: row; } -.notifications-link-container-item { - justify-content: center; - display: flex; -} diff --git a/webapp/components/NotificationsTable/NotificationsTable.spec.js b/webapp/components/NotificationsTable/NotificationsTable.spec.js index 0d3560787..35ddaf9b7 100644 --- a/webapp/components/NotificationsTable/NotificationsTable.spec.js +++ b/webapp/components/NotificationsTable/NotificationsTable.spec.js @@ -63,11 +63,7 @@ describe('NotificationsTable.vue', () => { expect(wrapper.find('.notification-grid').exists()).toBe(true) }) - describe('renders 4 columns', () => { - it('for icon', () => { - expect(wrapper.vm.fields.icon).toBeTruthy() - }) - + describe('renders 2 columns', () => { it('for user', () => { expect(wrapper.vm.fields.user).toBeTruthy() }) @@ -75,10 +71,6 @@ describe('NotificationsTable.vue', () => { it('for post', () => { expect(wrapper.vm.fields.post).toBeTruthy() }) - - it('for content', () => { - expect(wrapper.vm.fields.content).toBeTruthy() - }) }) describe('Post', () => { @@ -88,14 +80,14 @@ describe('NotificationsTable.vue', () => { }) it('renders the author', () => { - const userinfo = firstRowNotification.find('.user-teaser > .info') + const userinfo = firstRowNotification.find('.user-teaser .info') expect(userinfo.text()).toContain(postNotification.from.author.name) }) it('renders the reason for the notification', () => { - const dsTexts = firstRowNotification.findAll('.ds-text') - const reason = dsTexts.filter( - (element) => element.text() === 'notifications.reason.mentioned_in_post', + const dsTexts = firstRowNotification.findAll('.info span') + const reason = dsTexts.filter((element) => + element.text().startsWith('notifications.reason.mentioned_in_post'), ) expect(reason.exists()).toBe(true) }) @@ -106,7 +98,7 @@ describe('NotificationsTable.vue', () => { }) it("renders the Post's content", () => { - const boldTags = firstRowNotification.findAll('b') + const boldTags = firstRowNotification.findAll('p') const content = boldTags.filter( (element) => element.text() === postNotification.from.contentExcerpt, ) @@ -121,14 +113,14 @@ describe('NotificationsTable.vue', () => { }) it('renders the author', () => { - const userinfo = secondRowNotification.find('.user-teaser > .info') + const userinfo = secondRowNotification.find('.user-teaser .info') expect(userinfo.text()).toContain(commentNotification.from.author.name) }) it('renders the reason for the notification', () => { - const dsTexts = secondRowNotification.findAll('.ds-text') - const reason = dsTexts.filter( - (element) => element.text() === 'notifications.reason.mentioned_in_comment', + const dsTexts = secondRowNotification.findAll('.info span') + const reason = dsTexts.filter((element) => + element.text().startsWith('notifications.reason.mentioned_in_comment'), ) expect(reason.exists()).toBe(true) }) @@ -139,7 +131,7 @@ describe('NotificationsTable.vue', () => { }) it("renders the Post's content", () => { - const boldTags = secondRowNotification.findAll('b') + const boldTags = secondRowNotification.findAll('p') const content = boldTags.filter( (element) => element.text() === commentNotification.from.contentExcerpt, ) diff --git a/webapp/components/NotificationsTable/NotificationsTable.vue b/webapp/components/NotificationsTable/NotificationsTable.vue index 9ff106e62..9a0b4cd45 100644 --- a/webapp/components/NotificationsTable/NotificationsTable.vue +++ b/webapp/components/NotificationsTable/NotificationsTable.vue @@ -17,50 +17,45 @@ - -
- - - - -
-
-
- - - - - - - - {{ $t(`notifications.reason.${notification.reason}`) }} - - -
+ + + + +
- - - + +
+ +
+ + +
+ + +
{{ @@ -79,19 +74,18 @@ }} - - - - - +

{{ notification.from.contentExcerpt || notification.from.descriptionExcerpt | removeHtml }} - - - - +

+
+
+
@@ -116,25 +110,18 @@ export default { mixins: [mobile(maxMobileWidth)], props: { notifications: { type: Array, default: () => [] }, + showPopover: { type: Boolean, default: true }, }, computed: { fields() { return { - icon: { - label: ' ', - width: '5', - }, user: { label: this.$t('notifications.user'), - width: '45%', + width: '50%', }, post: { label: this.$t('notifications.post'), - width: '25%', - }, - content: { - label: this.$t('notifications.content'), - width: '25%', + width: '50%', }, } }, @@ -159,7 +146,23 @@ export default { return this.isComment(notificationSource) ? `#commentId-${notificationSource.id}` : '' }, markNotificationAsRead(notificationSourceId) { - this.$emit('markNotificationAsRead', notificationSourceId) + return new Promise((resolve) => { + this.$emit('markNotificationAsRead', notificationSourceId) + resolve() + }) + }, + async handleNotificationClick(notification) { + const route = { + name: this.isGroup(notification.from) ? 'groups-id-slug' : 'post-id-slug', + params: this.params(notification.from), + hash: this.hashParam(notification.from), + } + + await this.markNotificationAsRead(notification.from.id) + + setTimeout(() => { + this.$router.push(route) + }, 10) }, }, } @@ -172,12 +175,6 @@ export default { .notification-grid .content-section { flex-wrap: nowrap; } -.notification-grid .ds-grid.header-grid { - grid-template-columns: 1fr 4fr 3fr 3fr !important; -} -.notification-grid-row { - border-top: 1px dotted #e5e3e8; -} .notification-grid .base-card { border-radius: 0; box-shadow: none; @@ -190,6 +187,19 @@ export default { grid-template-rows: 1fr; gap: 0px !important; } +.notification-grid-row { + padding: 10px; + border-bottom: 1px dotted #e5e3e8; + background-color: white; + + &:nth-child(odd) { + background-color: $color-neutral-90; + } + .base-card { + padding: 8px 0; + background-color: unset !important; + } +} @media screen and (max-width: 768px) { .notification-grid .ds-grid { grid-template-columns: 1fr !important; @@ -197,10 +207,47 @@ export default { .notification-grid .content-section { border-top: 1px dotted #e5e3e8; } - .notification-grid-row { - box-shadow: 0px 12px 26px -4px rgb(0 0 0 / 10%); - margin-top: 5px; - border-top: none; +} + +.notification-description { + margin-top: 4px; +} +.notification-container { + display: flex; + align-items: flex-start; + gap: 10px; + + .notification-icon { + flex-shrink: 0; } } + +/* Desktop icon size */ +@media (min-width: 768px) { + .notification-icon { + width: 18px; + } + + .notification-icon :deep(svg) { + width: 25px; + height: 25px; + } +} + +/* Mobile icon size */ +@media (max-width: 767px) { + .notification-icon { + width: 34px; + text-align: center; + } + + .notification-icon :deep(svg) { + width: 50px; + height: 50px; + } +} + +.notification-content { + flex: 1; +} diff --git a/webapp/components/ObserveButton.spec.js b/webapp/components/ObserveButton.spec.js index 3ecfc40b6..10a170c42 100644 --- a/webapp/components/ObserveButton.spec.js +++ b/webapp/components/ObserveButton.spec.js @@ -1,14 +1,14 @@ -import { mount } from '@vue/test-utils' +import { render, screen, fireEvent } from '@testing-library/vue' import ObserveButton from './ObserveButton.vue' const localVue = global.localVue describe('ObserveButton', () => { - let mocks - const Wrapper = (count = 1, postId = '123', isObserved = true) => { - return mount(ObserveButton, { - mocks, + return render(ObserveButton, { + mocks: { + $t: jest.fn((t) => t), + }, localVue, propsData: { count, @@ -18,43 +18,39 @@ describe('ObserveButton', () => { }) } - let wrapper - - beforeEach(() => { - mocks = { - $t: jest.fn(), - } - }) - describe('observed', () => { + let wrapper + beforeEach(() => { wrapper = Wrapper(1, '123', true) }) it('renders', () => { - expect(wrapper.element).toMatchSnapshot() + expect(wrapper.container).toMatchSnapshot() }) - it('emits toggleObservePost with false when clicked', () => { - const button = wrapper.find('.base-button') - button.trigger('click') - expect(wrapper.emitted('toggleObservePost')).toEqual([['123', false]]) + it('emits toggleObservePost with false when clicked', async () => { + const button = screen.getByRole('button') + await fireEvent.click(button) + expect(wrapper.emitted().toggleObservePost).toEqual([['123', false]]) }) }) describe('unobserved', () => { + let wrapper + beforeEach(() => { wrapper = Wrapper(1, '123', false) }) it('renders', () => { - expect(wrapper.element).toMatchSnapshot() + expect(wrapper.container).toMatchSnapshot() }) - it('emits toggleObservePost with true when clicked', () => { - const button = wrapper.find('.base-button') - button.trigger('click') - expect(wrapper.emitted('toggleObservePost')).toEqual([['123', true]]) + it('emits toggleObservePost with true when clicked', async () => { + const button = screen.getByRole('button') + await fireEvent.click(button) + expect(wrapper.emitted().toggleObservePost).toEqual([['123', true]]) }) }) }) diff --git a/webapp/components/ObserveButton.vue b/webapp/components/ObserveButton.vue index 2c275709b..2c6488a5e 100644 --- a/webapp/components/ObserveButton.vue +++ b/webapp/components/ObserveButton.vue @@ -1,26 +1,27 @@ - - diff --git a/webapp/components/PasswordReset/Request.spec.js b/webapp/components/PasswordReset/Request.spec.js index 50d6495bd..bd7dc419c 100644 --- a/webapp/components/PasswordReset/Request.spec.js +++ b/webapp/components/PasswordReset/Request.spec.js @@ -59,7 +59,12 @@ describe('Request', () => { }) it('delivers email to backend', () => { - const expected = expect.objectContaining({ variables: { email: 'mail@example.org' } }) + const expected = expect.objectContaining({ + variables: { + email: 'mail@example.org', + locale: 'en', + }, + }) expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected) }) @@ -84,19 +89,6 @@ describe('Request', () => { }) }) - describe('capital letters in a gmail address', () => { - beforeEach(async () => { - wrapper = Wrapper() - wrapper.find('input#email').setValue('mAiL@gmail.com') - await wrapper.find('form').trigger('submit') - }) - - it('normalizes email to lower case letters', () => { - const expected = expect.objectContaining({ variables: { email: 'mail@gmail.com' } }) - expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected) - }) - }) - describe('backend throws an error', () => { beforeEach(() => { mocks.$apollo.mutate = jest.fn().mockRejectedValue({ diff --git a/webapp/components/PasswordReset/Request.vue b/webapp/components/PasswordReset/Request.vue index 5398c13ed..79558a911 100644 --- a/webapp/components/PasswordReset/Request.vue +++ b/webapp/components/PasswordReset/Request.vue @@ -45,7 +45,6 @@ - diff --git a/webapp/components/Registration/RegistrationSlideNoPublic.vue b/webapp/components/Registration/RegistrationSlideNoPublic.vue index c5f815ef4..8c5edcf5b 100644 --- a/webapp/components/Registration/RegistrationSlideNoPublic.vue +++ b/webapp/components/Registration/RegistrationSlideNoPublic.vue @@ -18,12 +18,8 @@ export default { sliderData: { type: Object, required: true }, }, mounted: function () { - this.$nextTick(function () { - // Code that will run only after the entire view has been rendered - - this.sliderData.setSliderValuesCallback(true, { - sliderSettings: { buttonSliderCallback: this.onNextClick }, - }) + this.sliderData.setSliderValuesCallback(true, { + sliderSettings: { buttonSliderCallback: this.onNextClick }, }) }, methods: { diff --git a/webapp/components/Registration/RegistrationSlideNonce.vue b/webapp/components/Registration/RegistrationSlideNonce.vue index d7bda28d6..0bc466e58 100644 --- a/webapp/components/Registration/RegistrationSlideNonce.vue +++ b/webapp/components/Registration/RegistrationSlideNonce.vue @@ -29,7 +29,7 @@ diff --git a/webapp/components/ShoutButton.spec.js b/webapp/components/ShoutButton.spec.js index c3af134c1..f3ef5611f 100644 --- a/webapp/components/ShoutButton.spec.js +++ b/webapp/components/ShoutButton.spec.js @@ -1,6 +1,7 @@ -import { mount } from '@vue/test-utils' -import ShoutButton from './ShoutButton.vue' +import { render, screen, fireEvent } from '@testing-library/vue' +import '@testing-library/jest-dom' import Vue from 'vue' +import ShoutButton from './ShoutButton.vue' const localVue = global.localVue @@ -9,49 +10,54 @@ describe('ShoutButton.vue', () => { beforeEach(() => { mocks = { - $t: jest.fn(), + $t: jest.fn((t) => t), $apollo: { mutate: jest.fn(), }, } }) - describe('mount', () => { - let wrapper - const Wrapper = () => { - return mount(ShoutButton, { mocks, localVue }) - } + let wrapper - beforeEach(() => { - wrapper = Wrapper() - }) + const Wrapper = ({ isShouted = false } = {}) => { + return render(ShoutButton, { mocks, localVue, propsData: { isShouted } }) + } - it('renders button and text', () => { - expect(mocks.$t).toHaveBeenCalledWith('shoutButton.shouted') - expect(wrapper.findAll('.base-button')).toHaveLength(1) - expect(wrapper.findAll('.shout-button-text')).toHaveLength(1) - expect(wrapper.vm.shouted).toBe(false) - expect(wrapper.vm.shoutedCount).toBe(0) - }) + beforeEach(() => { + wrapper = Wrapper() + }) - it('toggle the button', async () => { - mocks.$apollo.mutate = jest.fn().mockResolvedValue({ data: { shout: 'WeDoShout' } }) - wrapper.find('.base-button').trigger('click') - expect(wrapper.vm.shouted).toBe(true) - expect(wrapper.vm.shoutedCount).toBe(1) - await Vue.nextTick() - expect(wrapper.vm.shouted).toBe(true) - expect(wrapper.vm.shoutedCount).toBe(1) - }) + it('renders button and text', () => { + expect(wrapper.container).toMatchSnapshot() + const button = screen.getByRole('button') + expect(button).toBeInTheDocument() + }) - it('toggle the button, but backend fails', async () => { - mocks.$apollo.mutate = jest.fn().mockRejectedValue({ message: 'Ouch!' }) - await wrapper.find('.base-button').trigger('click') - expect(wrapper.vm.shouted).toBe(true) - expect(wrapper.vm.shoutedCount).toBe(1) - await Vue.nextTick() - expect(wrapper.vm.shouted).toBe(false) - expect(wrapper.vm.shoutedCount).toBe(0) + it('toggle the button', async () => { + mocks.$apollo.mutate = jest.fn().mockResolvedValue({ data: { shout: 'WeDoShout' } }) + const button = screen.getByRole('button') + await fireEvent.click(button) + expect(wrapper.container).toMatchSnapshot() + const shoutedCount = screen.getByText('1') + expect(shoutedCount).toBeInTheDocument() + }) + + it('toggle the button, but backend fails', async () => { + mocks.$apollo.mutate = jest.fn().mockRejectedValue({ message: 'Ouch!' }) + const button = screen.getByRole('button') + await fireEvent.click(button) + expect(wrapper.container).toMatchSnapshot() + let shoutedCount = screen.getByText('1') + expect(shoutedCount).toBeInTheDocument() + await Vue.nextTick() + shoutedCount = screen.getByText('0') + expect(shoutedCount).toBeInTheDocument() + }) + + describe('when shouted', () => { + it('renders', () => { + wrapper = Wrapper({ isShouted: true }) + expect(wrapper.container).toMatchSnapshot() }) }) }) diff --git a/webapp/components/ShoutButton.vue b/webapp/components/ShoutButton.vue index 4b644bc25..a8aca9e74 100644 --- a/webapp/components/ShoutButton.vue +++ b/webapp/components/ShoutButton.vue @@ -1,28 +1,28 @@ - - diff --git a/webapp/components/UserTeaser/LocationInfo.spec.js b/webapp/components/UserTeaser/LocationInfo.spec.js new file mode 100644 index 000000000..2b100e66d --- /dev/null +++ b/webapp/components/UserTeaser/LocationInfo.spec.js @@ -0,0 +1,31 @@ +import { render } from '@testing-library/vue' +import LocationInfo from './LocationInfo.vue' + +const localVue = global.localVue + +describe('LocationInfo', () => { + const Wrapper = ({ withDistance }) => { + return render(LocationInfo, { + localVue, + propsData: { + locationData: { + name: 'Paris', + distanceToMe: withDistance ? 100 : null, + }, + }, + mocks: { + $t: jest.fn((t) => t), + }, + }) + } + + it('renders with distance', () => { + const wrapper = Wrapper({ withDistance: true }) + expect(wrapper.container).toMatchSnapshot() + }) + + it('renders without distance', () => { + const wrapper = Wrapper({ withDistance: false }) + expect(wrapper.container).toMatchSnapshot() + }) +}) diff --git a/webapp/components/UserTeaser/LocationInfo.vue b/webapp/components/UserTeaser/LocationInfo.vue new file mode 100644 index 000000000..67dc46c27 --- /dev/null +++ b/webapp/components/UserTeaser/LocationInfo.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/webapp/components/UserTeaser/UserTeaser.spec.js b/webapp/components/UserTeaser/UserTeaser.spec.js index 354308109..0c2adab8a 100644 --- a/webapp/components/UserTeaser/UserTeaser.spec.js +++ b/webapp/components/UserTeaser/UserTeaser.spec.js @@ -1,113 +1,256 @@ -import { mount, RouterLinkStub } from '@vue/test-utils' +import { render, screen, fireEvent } from '@testing-library/vue' +import { RouterLinkStub } from '@vue/test-utils' import UserTeaser from './UserTeaser.vue' import Vuex from 'vuex' const localVue = global.localVue -const filter = jest.fn((str) => str) -localVue.filter('truncate', filter) +// Mock Math.random, used in Dropdown +Object.assign(Math, { + random: () => 0, +}) + +const waitForPopover = async () => await new Promise((resolve) => setTimeout(resolve, 1000)) + +let mockIsTouchDevice +jest.mock('../utils/isTouchDevice', () => ({ + isTouchDevice: jest.fn(() => mockIsTouchDevice), +})) + +const userTilda = { + name: 'Tilda Swinton', + slug: 'tilda-swinton', + id: 'user1', + avatar: '/avatars/tilda-swinton', + badgeVerification: { + id: 'bv1', + icon: '/icons/verified', + description: 'Verified', + isDefault: false, + }, + badgeTrophiesSelected: [ + { + id: 'trophy1', + icon: '/icons/trophy1', + description: 'Trophy 1', + isDefault: false, + }, + { + id: 'trophy2', + icon: '/icons/trophy2', + description: 'Trophy 2', + isDefault: false, + }, + { + id: 'empty', + icon: '/icons/empty', + description: 'Empty', + isDefault: true, + }, + ], +} describe('UserTeaser', () => { - let propsData - let mocks - let stubs - let getters + const Wrapper = ({ + isModerator = false, + withLinkToProfile = true, + onTouchScreen = false, + withAvatar = true, + user = userTilda, + withPopoverEnabled = true, + }) => { + mockIsTouchDevice = onTouchScreen - beforeEach(() => { - propsData = {} - - mocks = { - $t: jest.fn(), - } - stubs = { - NuxtLink: RouterLinkStub, - } - getters = { - 'auth/user': () => { - return {} + const store = new Vuex.Store({ + getters: { + 'auth/user': () => { + return {} + }, + 'auth/isModerator': () => isModerator, }, - 'auth/isModerator': () => false, - } + }) + return render(UserTeaser, { + localVue, + store, + propsData: { + user, + linkToProfile: withLinkToProfile, + showAvatar: withAvatar, + showPopover: withPopoverEnabled, + }, + stubs: { + NuxtLink: RouterLinkStub, + 'user-teaser-popover': true, + 'v-popover': true, + 'client-only': true, + }, + mocks: { + $t: jest.fn((t) => t), + $i18n: { + locale: jest.fn(() => 'en'), + }, + $apollo: { + query: jest.fn(() => Promise.resolve({ data: { user } })), + }, + }, + }) + } + + it('renders anonymous user', () => { + const wrapper = Wrapper({ user: null }) + expect(wrapper.container).toMatchSnapshot() }) - describe('mount', () => { - const Wrapper = () => { - const store = new Vuex.Store({ - getters, + describe('given an user', () => { + describe('without linkToProfile, on touch screen', () => { + let wrapper + beforeEach(() => { + wrapper = Wrapper({ withLinkToProfile: false, onTouchScreen: true }) }) - return mount(UserTeaser, { store, propsData, mocks, stubs, localVue }) - } - it('renders anonymous user', () => { - const wrapper = Wrapper() - expect(wrapper.text()).toBe('') - expect(mocks.$t).toHaveBeenCalledWith('profile.userAnonym') + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + describe('when clicking the user name', () => { + beforeEach(async () => { + const userName = screen.getByText('Tilda Swinton') + await fireEvent.click(userName) + await waitForPopover() + }) + + it('renders the popover', () => { + expect(wrapper.container).toMatchSnapshot() + }) + }) + + describe('when clicking the user avatar', () => { + beforeEach(async () => { + const userAvatar = screen.getByAltText('Tilda Swinton') + await fireEvent.click(userAvatar) + await waitForPopover() + }) + + it('renders the popover', () => { + expect(wrapper.container).toMatchSnapshot() + }) + }) }) - describe('given an user', () => { + describe('with linkToProfile, on touch screen', () => { + let wrapper beforeEach(() => { - propsData.user = { - name: 'Tilda Swinton', - slug: 'tilda-swinton', - } + wrapper = Wrapper({ withLinkToProfile: true, onTouchScreen: true }) }) - it('renders user name', () => { - const wrapper = Wrapper() - expect(mocks.$t).not.toHaveBeenCalledWith('profile.userAnonym') - expect(wrapper.text()).toMatch('Tilda Swinton') + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() }) - describe('user is deleted', () => { - beforeEach(() => { - propsData.user.deleted = true + describe('when clicking the user name', () => { + beforeEach(async () => { + const userName = screen.getByText('Tilda Swinton') + await fireEvent.click(userName) }) + it('renders the popover', () => { + expect(wrapper.container).toMatchSnapshot() + }) + }) + }) + + describe('without linkToProfile, on desktop', () => { + let wrapper + beforeEach(() => { + wrapper = Wrapper({ withLinkToProfile: false, onTouchScreen: false }) + }) + + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + describe('when hovering the user name', () => { + beforeEach(async () => { + const userName = screen.getByText('Tilda Swinton') + await fireEvent.mouseOver(userName) + await waitForPopover() + }) + + it('renders the popover', () => { + expect(wrapper.container).toMatchSnapshot() + }) + }) + + describe('when hovering the user avatar', () => { + beforeEach(async () => { + const userAvatar = screen.getByAltText('Tilda Swinton') + await fireEvent.mouseOver(userAvatar) + await waitForPopover() + }) + + it('renders the popover', () => { + expect(wrapper.container).toMatchSnapshot() + }) + }) + }) + + describe('with linkToProfile, on desktop', () => { + let wrapper + beforeEach(() => { + wrapper = Wrapper({ withLinkToProfile: true, onTouchScreen: false }) + }) + + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + describe('when hovering the user name', () => { + beforeEach(async () => { + const userName = screen.getByText('Tilda Swinton') + await fireEvent.mouseOver(userName) + await waitForPopover() + }) + + it('renders the popover', () => { + expect(wrapper.container).toMatchSnapshot() + }) + }) + }) + + describe('avatar is disabled', () => { + it('does not render the avatar', () => { + const wrapper = Wrapper({ withAvatar: false }) + expect(wrapper.container).toMatchSnapshot() + }) + }) + + describe('user is deleted', () => { + it('renders anonymous user', () => { + const wrapper = Wrapper({ user: { ...userTilda, deleted: true } }) + expect(wrapper.container).toMatchSnapshot() + }) + + describe('even if the current user is a moderator', () => { it('renders anonymous user', () => { - const wrapper = Wrapper() - expect(wrapper.text()).not.toMatch('Tilda Swinton') - expect(mocks.$t).toHaveBeenCalledWith('profile.userAnonym') - }) - - describe('even if the current user is a moderator', () => { - beforeEach(() => { - getters['auth/isModerator'] = () => true - }) - - it('renders anonymous user', () => { - const wrapper = Wrapper() - expect(wrapper.text()).not.toMatch('Tilda Swinton') - expect(mocks.$t).toHaveBeenCalledWith('profile.userAnonym') + const wrapper = Wrapper({ + user: { ...userTilda, deleted: true }, + isModerator: true, }) + expect(wrapper.container).toMatchSnapshot() }) }) + }) - describe('user is disabled', () => { - beforeEach(() => { - propsData.user.disabled = true - }) + describe('user is disabled', () => { + it('renders anonymous user', () => { + const wrapper = Wrapper({ user: { ...userTilda, disabled: true } }) + expect(wrapper.container).toMatchSnapshot() + }) - it('renders anonymous user', () => { - const wrapper = Wrapper() - expect(wrapper.text()).not.toMatch('Tilda Swinton') - expect(mocks.$t).toHaveBeenCalledWith('profile.userAnonym') - }) - - describe('current user is a moderator', () => { - beforeEach(() => { - getters['auth/isModerator'] = () => true - }) - - it('renders user name', () => { - const wrapper = Wrapper() - expect(wrapper.text()).not.toMatch('Anonymous') - expect(wrapper.text()).toMatch('Tilda Swinton') - }) - - it('has "disabled-content" class', () => { - const wrapper = Wrapper() - expect(wrapper.classes()).toContain('disabled-content') - }) + describe('current user is a moderator', () => { + it('renders user name', () => { + const wrapper = Wrapper({ user: { ...userTilda, disabled: true }, isModerator: true }) + expect(wrapper.container).toMatchSnapshot() }) }) }) diff --git a/webapp/components/UserTeaser/UserTeaser.story.js b/webapp/components/UserTeaser/UserTeaser.story.js index aa8be58ff..1295bf2db 100644 --- a/webapp/components/UserTeaser/UserTeaser.story.js +++ b/webapp/components/UserTeaser/UserTeaser.story.js @@ -41,8 +41,8 @@ export const user = { commentedCount: 3, badges: [ { - id: 'indiegogo_en_bear', - icon: '/img/badges/indiegogo_en_bear.svg', + id: 'trophy_bear', + icon: '/img/badges/trophy_blue_bear.svg', }, ], location: { diff --git a/webapp/components/UserTeaser/UserTeaser.vue b/webapp/components/UserTeaser/UserTeaser.vue index a9e556bf4..7f9e0cb4f 100644 --- a/webapp/components/UserTeaser/UserTeaser.vue +++ b/webapp/components/UserTeaser/UserTeaser.vue @@ -4,57 +4,36 @@ {{ $t('profile.userAnonym') }}
-
- - - - -
-
- - - {{ userSlug }} - {{ userName }} - - - - {{ userSlug }} - {{ userName }} - -   - - - {{ $t('group.in') }} - - - - {{ groupSlug }} - {{ groupName }} - - - -
- - - - - -
-
+ + + +
diff --git a/webapp/components/UserTeaser/__snapshots__/LocationInfo.spec.js.snap b/webapp/components/UserTeaser/__snapshots__/LocationInfo.spec.js.snap new file mode 100644 index 000000000..50ce23f9a --- /dev/null +++ b/webapp/components/UserTeaser/__snapshots__/LocationInfo.spec.js.snap @@ -0,0 +1,51 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LocationInfo renders with distance 1`] = ` +
+
+
+ + + + + Paris + +
+ +
+ location.distance +
+
+
+`; + +exports[`LocationInfo renders without distance 1`] = ` +
+
+
+ + + + + Paris + +
+ + +
+
+`; diff --git a/webapp/components/UserTeaser/__snapshots__/UserTeaser.spec.js.snap b/webapp/components/UserTeaser/__snapshots__/UserTeaser.spec.js.snap new file mode 100644 index 000000000..b1d196a42 --- /dev/null +++ b/webapp/components/UserTeaser/__snapshots__/UserTeaser.spec.js.snap @@ -0,0 +1,1196 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UserTeaser given an user avatar is disabled does not render the avatar 1`] = ` +
+
+ + + + + + +
+ + +
+
+`; + +exports[`UserTeaser given an user user is deleted even if the current user is a moderator renders anonymous user 1`] = ` +
+
+
+ + + + + + + + + +
+ + + profile.userAnonym + +
+
+`; + +exports[`UserTeaser given an user user is deleted renders anonymous user 1`] = ` +
+
+
+ + + + + + + + + +
+ + + profile.userAnonym + +
+
+`; + +exports[`UserTeaser given an user user is disabled current user is a moderator renders user name 1`] = ` +
+
+ + + +
+ + TS + + + + + Tilda Swinton +
+
+ + + +
+ + +
+
+`; + +exports[`UserTeaser given an user user is disabled renders anonymous user 1`] = ` +
+
+
+ + + + + + + + + +
+ + + profile.userAnonym + +
+
+`; + +exports[`UserTeaser given an user with linkToProfile, on desktop renders 1`] = ` +
+
+ + + +
+ + TS + + + + + Tilda Swinton +
+
+ + + +
+ + +
+
+`; + +exports[`UserTeaser given an user with linkToProfile, on desktop when hovering the user name renders the popover 1`] = ` +
+
+ + + +
+ + TS + + + + + Tilda Swinton +
+
+ + + +
+ +
+
+
+
+
+`; + +exports[`UserTeaser given an user with linkToProfile, on touch screen renders 1`] = ` +
+
+ + + + +
+
+ + + + + + + + + + +
+ + +
+ +
+ + +
+
+`; + +exports[`UserTeaser given an user with linkToProfile, on touch screen when clicking the user name renders the popover 1`] = ` +
+
+ + + + +
+
+ + + + + + + + + + +
+ + +
+ +
+ +
+
+
+
+
+`; + +exports[`UserTeaser given an user without linkToProfile, on desktop renders 1`] = ` +
+
+ + + +
+ + TS + + + + + Tilda Swinton +
+
+ +
+
+ + + @tilda-swinton + + + + Tilda Swinton + + + + + + + + + + + +
+ + +
+ +
+ + +
+
+`; + +exports[`UserTeaser given an user without linkToProfile, on desktop when hovering the user avatar renders the popover 1`] = ` +
+
+ + + +
+ + TS + + + + + Tilda Swinton +
+
+ +
+
+ + + @tilda-swinton + + + + Tilda Swinton + + + + + + + + + + + +
+ + +
+ +
+ +
+
+
+
+
+`; + +exports[`UserTeaser given an user without linkToProfile, on desktop when hovering the user name renders the popover 1`] = ` +
+
+ + + +
+ + TS + + + + + Tilda Swinton +
+
+ +
+
+ + + @tilda-swinton + + + + Tilda Swinton + + + + + + + + + + + +
+ + +
+ +
+ +
+
+
+
+
+`; + +exports[`UserTeaser given an user without linkToProfile, on touch screen renders 1`] = ` +
+
+ + + + +
+
+ + + + + + + + + + +
+ + +
+ +
+ + +
+
+`; + +exports[`UserTeaser given an user without linkToProfile, on touch screen when clicking the user avatar renders the popover 1`] = ` +
+
+ + + + +
+
+ + + + + + + + + + +
+ + +
+ +
+ +
+
+
+
+
+`; + +exports[`UserTeaser given an user without linkToProfile, on touch screen when clicking the user name renders the popover 1`] = ` +
+
+ + + + +
+
+ + + + + + + + + + +
+ + +
+ +
+ +
+
+
+
+
+`; + +exports[`UserTeaser renders anonymous user 1`] = ` +
+
+
+ + + + + + + + + +
+ + + profile.userAnonym + +
+
+`; diff --git a/webapp/components/UserTeaser/__snapshots__/UserTeaserHelper.spec.js.snap b/webapp/components/UserTeaser/__snapshots__/UserTeaserHelper.spec.js.snap new file mode 100644 index 000000000..2257e8a51 --- /dev/null +++ b/webapp/components/UserTeaser/__snapshots__/UserTeaserHelper.spec.js.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UserTeaserHelper with linkToProfile and popover enabled, on touch screen renders button 1`] = ` +
+
+`; + +exports[`UserTeaserHelper with linkToProfile, on desktop renders link 1`] = ` +
+ +
+`; + +exports[`UserTeaserHelper without linkToProfile renders span 1`] = ` +
+ +
+`; diff --git a/webapp/components/UserTeaser/__snapshots__/UserTeaserPopover.spec.js.snap b/webapp/components/UserTeaser/__snapshots__/UserTeaserPopover.spec.js.snap new file mode 100644 index 000000000..3eab03611 --- /dev/null +++ b/webapp/components/UserTeaser/__snapshots__/UserTeaserPopover.spec.js.snap @@ -0,0 +1,547 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UserTeaserPopover does not show badges when disabled 1`] = ` +
+
+ + + + +
    +
  • +
    +

    + 0 +

    +

    + + profile.followers + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.post + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.comment + +

    +
    +
  • +
+ + +
+
+`; + +exports[`UserTeaserPopover given a non-touch device does not show button when userLink is provided 1`] = ` +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + + +
    +
  • +
    +

    + 0 +

    +

    + + profile.followers + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.post + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.comment + +

    +
    +
  • +
+ + +
+
+`; + +exports[`UserTeaserPopover given a touch device does not show button when userLink is not provided 1`] = ` +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + + +
    +
  • +
    +

    + 0 +

    +

    + + profile.followers + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.post + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.comment + +

    +
    +
  • +
+ + +
+
+`; + +exports[`UserTeaserPopover given a touch device shows button when userLink is provided 1`] = ` +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + + +
    +
  • +
    +

    + 0 +

    +

    + + profile.followers + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.post + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.comment + +

    +
    +
  • +
+ + +
+
+`; + +exports[`UserTeaserPopover shows badges when enabled 1`] = ` +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + + +
    +
  • +
    +

    + 0 +

    +

    + + profile.followers + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.post + +

    +
    +
  • + +
  • +
    +

    + 0 +

    +

    + + common.comment + +

    +
    +
  • +
+ + +
+
+`; diff --git a/webapp/components/__snapshots__/ActionButton.spec.js.snap b/webapp/components/__snapshots__/ActionButton.spec.js.snap new file mode 100644 index 000000000..c5d1d4581 --- /dev/null +++ b/webapp/components/__snapshots__/ActionButton.spec.js.snap @@ -0,0 +1,60 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ActionButton.vue when disabled renders 1`] = ` +
+
+ + +
+ 7 +
+
+
+`; + +exports[`ActionButton.vue when not disabled renders 1`] = ` +
+
+ + +
+ 7 +
+
+
+`; diff --git a/webapp/components/__snapshots__/BadgeSelection.spec.js.snap b/webapp/components/__snapshots__/BadgeSelection.spec.js.snap new file mode 100644 index 000000000..a31d547c9 --- /dev/null +++ b/webapp/components/__snapshots__/BadgeSelection.spec.js.snap @@ -0,0 +1,84 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Badges.vue with badges renders 1`] = ` +
+
+ + + +
+
+`; + +exports[`Badges.vue without badges renders 1`] = ` +
+
+
+`; diff --git a/webapp/components/__snapshots__/Badges.spec.js.snap b/webapp/components/__snapshots__/Badges.spec.js.snap new file mode 100644 index 000000000..425d2ace7 --- /dev/null +++ b/webapp/components/__snapshots__/Badges.spec.js.snap @@ -0,0 +1,165 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Badges.vue with badges in presentation mode renders 1`] = ` +
+
+
+ +
+
+ +
+
+ +
+
+
+`; + +exports[`Badges.vue with badges in selection mode clicking on second badge selects badge 1`] = ` +
+
+
+ +
+ + +
+
+`; + +exports[`Badges.vue with badges in selection mode clicking twice on second badge deselects badge 1`] = ` +
+
+
+ +
+ + +
+
+`; + +exports[`Badges.vue with badges in selection mode renders 1`] = ` +
+
+
+ +
+ + +
+
+`; + +exports[`Badges.vue without badges renders in presentation mode 1`] = ` +
+
+
+`; + +exports[`Badges.vue without badges renders in selection mode 1`] = ` +
+
+
+`; diff --git a/webapp/components/__snapshots__/ObserveButton.spec.js.snap b/webapp/components/__snapshots__/ObserveButton.spec.js.snap index c3ba629be..064e6256f 100644 --- a/webapp/components/__snapshots__/ObserveButton.spec.js.snap +++ b/webapp/components/__snapshots__/ObserveButton.spec.js.snap @@ -1,81 +1,61 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ObserveButton observed renders 1`] = ` -
- - +
- -

-

- 1x -

- - - -

+ + + + + + + + +
+ 1 +
+
`; exports[`ObserveButton unobserved renders 1`] = ` -
- - +
- -

-

- 1x -

- - - -

+ + + + + + + + +
+ 1 +
+
`; diff --git a/webapp/components/__snapshots__/ShoutButton.spec.js.snap b/webapp/components/__snapshots__/ShoutButton.spec.js.snap new file mode 100644 index 000000000..94254b870 --- /dev/null +++ b/webapp/components/__snapshots__/ShoutButton.spec.js.snap @@ -0,0 +1,117 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ShoutButton.vue renders button and text 1`] = ` +
+
+ + +
+ 0 +
+
+
+`; + +exports[`ShoutButton.vue toggle the button 1`] = ` +
+
+ + +
+ 1 +
+
+
+`; + +exports[`ShoutButton.vue toggle the button, but backend fails 1`] = ` +
+
+ + +
+ 1 +
+
+
+`; + +exports[`ShoutButton.vue when shouted renders 1`] = ` +
+
+ + +
+ 0 +
+
+
+`; diff --git a/webapp/components/_new/features/Admin/Badges/BadgesSection.spec.js b/webapp/components/_new/features/Admin/Badges/BadgesSection.spec.js new file mode 100644 index 000000000..8abf8d679 --- /dev/null +++ b/webapp/components/_new/features/Admin/Badges/BadgesSection.spec.js @@ -0,0 +1,61 @@ +import { render, fireEvent, screen } from '@testing-library/vue' +import BadgesSection from './BadgesSection.vue' + +const localVue = global.localVue + +const badge1 = { + id: 'badge1', + icon: 'icon1', + type: 'type1', + description: 'description1', + isActive: true, +} +const badge2 = { + id: 'badge2', + icon: 'icon2', + type: 'type1', + description: 'description2', + isActive: false, +} + +describe('Admin/BadgesSection', () => { + let wrapper + + const Wrapper = (withBadges = true) => { + return render(BadgesSection, { + localVue, + propsData: { + badges: withBadges ? [badge1, badge2] : [], + }, + mocks: { + $t: jest.fn((t) => t), + }, + }) + } + + describe('without badges', () => { + beforeEach(() => { + wrapper = Wrapper(false) + }) + + it('renders', () => { + expect(wrapper.baseElement).toMatchSnapshot() + }) + }) + + describe('with badges', () => { + beforeEach(() => { + wrapper = Wrapper(true) + }) + + it('renders', () => { + expect(wrapper.baseElement).toMatchSnapshot() + }) + + it('emits toggleButton', async () => { + const button = screen.getByAltText(badge1.description) + await fireEvent.click(button) + expect(wrapper.emitted().toggleBadge[0][0]).toEqual(badge1) + }) + }) +}) diff --git a/webapp/components/_new/features/Admin/Badges/BadgesSection.vue b/webapp/components/_new/features/Admin/Badges/BadgesSection.vue new file mode 100644 index 000000000..fc89d2a50 --- /dev/null +++ b/webapp/components/_new/features/Admin/Badges/BadgesSection.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/webapp/components/_new/features/Admin/Badges/__snapshots__/BadgesSection.spec.js.snap b/webapp/components/_new/features/Admin/Badges/__snapshots__/BadgesSection.spec.js.snap new file mode 100644 index 000000000..a78f44edc --- /dev/null +++ b/webapp/components/_new/features/Admin/Badges/__snapshots__/BadgesSection.spec.js.snap @@ -0,0 +1,56 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Admin/BadgesSection with badges renders 1`] = ` + +
+
+

+ +

+ +
+ + +
+
+
+ +`; + +exports[`Admin/BadgesSection without badges renders 1`] = ` + +
+
+

+ +

+ +
+ + admin.badges.noBadges + +
+
+
+ +`; diff --git a/webapp/components/_new/features/InternalPage/InternalPage.vue b/webapp/components/_new/features/InternalPage/InternalPage.vue index 99982e89b..c0520f935 100644 --- a/webapp/components/_new/features/InternalPage/InternalPage.vue +++ b/webapp/components/_new/features/InternalPage/InternalPage.vue @@ -32,3 +32,16 @@ export default { }, } + + diff --git a/webapp/components/_new/features/Invitations/CreateInvitation.spec.js b/webapp/components/_new/features/Invitations/CreateInvitation.spec.js new file mode 100644 index 000000000..10e13d62c --- /dev/null +++ b/webapp/components/_new/features/Invitations/CreateInvitation.spec.js @@ -0,0 +1,51 @@ +import { render, screen, fireEvent } from '@testing-library/vue' + +import CreateInvitation from './CreateInvitation.vue' + +const localVue = global.localVue + +describe('CreateInvitation.vue', () => { + let wrapper + + const Wrapper = ({ isDisabled = false }) => { + return render(CreateInvitation, { + localVue, + propsData: { + isDisabled, + }, + mocks: { + $t: jest.fn((v) => v), + }, + }) + } + + it('renders', () => { + wrapper = Wrapper({}) + expect(wrapper.container).toMatchSnapshot() + }) + + it('renders with disabled button', () => { + wrapper = Wrapper({ isDisabled: true }) + expect(wrapper.container).toMatchSnapshot() + }) + + describe('when the form is submitted', () => { + beforeEach(() => { + wrapper = Wrapper({}) + }) + + it('emits generate-invite-code with empty comment', async () => { + const button = screen.getByRole('button') + await fireEvent.click(button) + expect(wrapper.emitted()['generate-invite-code']).toEqual([['']]) + }) + + it('emits generate-invite-code with comment', async () => { + const button = screen.getByRole('button') + const input = screen.getByPlaceholderText('invite-codes.comment-placeholder') + await fireEvent.update(input, 'Test comment') + await fireEvent.click(button) + expect(wrapper.emitted()['generate-invite-code']).toEqual([['Test comment']]) + }) + }) +}) diff --git a/webapp/components/_new/features/Invitations/CreateInvitation.vue b/webapp/components/_new/features/Invitations/CreateInvitation.vue new file mode 100644 index 000000000..6147ca682 --- /dev/null +++ b/webapp/components/_new/features/Invitations/CreateInvitation.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/webapp/components/_new/features/Invitations/Invitation.spec.js b/webapp/components/_new/features/Invitations/Invitation.spec.js new file mode 100644 index 000000000..dd62847ef --- /dev/null +++ b/webapp/components/_new/features/Invitations/Invitation.spec.js @@ -0,0 +1,115 @@ +import { render, screen, fireEvent } from '@testing-library/vue' +import '@testing-library/jest-dom' + +import Invitation from './Invitation.vue' + +const localVue = global.localVue + +Object.assign(navigator, { + clipboard: { + writeText: jest.fn(), + }, +}) + +const mutations = { + 'modal/SET_OPEN': jest.fn().mockResolvedValue(), +} + +describe('Invitation.vue', () => { + let wrapper + + const Wrapper = ({ wasRedeemed = false, withCopymessage = false }) => { + const propsData = { + inviteCode: { + code: 'test-invite-code', + comment: 'test-comment', + redeemedByCount: wasRedeemed ? 1 : 0, + }, + copyMessage: withCopymessage ? 'test-copy-message' : undefined, + } + return render(Invitation, { + localVue, + propsData, + mocks: { + $t: jest.fn((v) => v), + $toast: { + success: jest.fn(), + error: jest.fn(), + }, + }, + mutations, + }) + } + + describe('when the invite code was redeemed', () => { + beforeEach(() => { + wrapper = Wrapper({ wasRedeemed: true }) + }) + + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + it('says how many times the code was redeemed', () => { + const redeemedCount = screen.getByText('invite-codes.redeemed-count') + expect(redeemedCount).toBeInTheDocument() + }) + }) + + describe('when the invite code was not redeemed', () => { + beforeEach(() => { + wrapper = Wrapper({ wasRedeemed: false }) + }) + + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + it('says it was not redeemed', () => { + const redeemedCount = screen.queryByText('invite-codes.redeemed-count-0') + expect(redeemedCount).toBeInTheDocument() + }) + }) + + describe('without copy message', () => { + beforeEach(() => { + wrapper = Wrapper({ withCopymessage: false }) + }) + + it('can copy the link', async () => { + const clipboardMock = jest.spyOn(navigator.clipboard, 'writeText').mockResolvedValue() + const copyButton = screen.getByLabelText('invite-codes.copy-code') + await fireEvent.click(copyButton) + expect(clipboardMock).toHaveBeenCalledWith( + 'http://localhost/registration?method=invite-code&inviteCode=test-invite-code', + ) + }) + }) + + describe('with copy message', () => { + beforeEach(() => { + wrapper = Wrapper({ withCopymessage: true }) + }) + + it('can copy the link with message', async () => { + const clipboardMock = jest.spyOn(navigator.clipboard, 'writeText').mockResolvedValue() + const copyButton = screen.getByLabelText('invite-codes.copy-code') + await fireEvent.click(copyButton) + expect(clipboardMock).toHaveBeenCalledWith( + 'test-copy-message http://localhost/registration?method=invite-code&inviteCode=test-invite-code', + ) + }) + }) + + describe.skip('invalidate button', () => { + beforeEach(() => { + wrapper = Wrapper({ wasRedeemed: false }) + }) + + it('opens the delete modal', async () => { + const deleteButton = screen.getByLabelText('invite-codes.invalidate') + await fireEvent.click(deleteButton) + expect(mutations['modal/SET_OPEN']).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/webapp/components/_new/features/Invitations/Invitation.vue b/webapp/components/_new/features/Invitations/Invitation.vue new file mode 100644 index 000000000..f34beeb58 --- /dev/null +++ b/webapp/components/_new/features/Invitations/Invitation.vue @@ -0,0 +1,156 @@ + + + + + diff --git a/webapp/components/_new/features/Invitations/InvitationList.spec.js b/webapp/components/_new/features/Invitations/InvitationList.spec.js new file mode 100644 index 000000000..dc60bb93f --- /dev/null +++ b/webapp/components/_new/features/Invitations/InvitationList.spec.js @@ -0,0 +1,113 @@ +import { render, screen, fireEvent } from '@testing-library/vue' +import '@testing-library/jest-dom' + +import InvitationList from './InvitationList.vue' + +const localVue = global.localVue + +Object.assign(navigator, { + clipboard: { + writeText: jest.fn(), + }, +}) + +const sampleInviteCodes = [ + { + code: 'test-invite-code-1', + comment: 'test-comment', + redeemedByCount: 0, + isValid: true, + }, + { + code: 'test-invite-code-2', + comment: 'test-comment-2', + redeemedByCount: 1, + isValid: true, + }, + { + code: 'test-invite-code-3', + comment: 'test-comment-3', + redeemedByCount: 0, + isValid: false, + }, +] + +describe('InvitationList.vue', () => { + let wrapper + + const Wrapper = ({ withInviteCodes, withCopymessage = false, limit = 3 }) => { + const propsData = { + inviteCodes: withInviteCodes ? sampleInviteCodes : [], + copyMessage: withCopymessage ? 'test-copy-message' : undefined, + } + return render(InvitationList, { + localVue, + propsData, + mocks: { + $t: jest.fn((v) => v), + $toast: { + success: jest.fn(), + error: jest.fn(), + }, + $env: { + INVITE_LINK_LIMIT: limit, + }, + }, + stubs: { + 'client-only': true, + }, + }) + } + + it('renders', () => { + wrapper = Wrapper({ withInviteCodes: true }) + expect(wrapper.container).toMatchSnapshot() + }) + + it('renders empty state', () => { + wrapper = Wrapper({ withInviteCodes: false }) + expect(wrapper.container).toMatchSnapshot() + }) + + it('does not render invalid invite codes', () => { + wrapper = Wrapper({ withInviteCodes: true }) + const invalidInviteCode = screen.queryByText('invite-codes.test-invite-code-3') + expect(invalidInviteCode).not.toBeInTheDocument() + }) + + describe('without copy message', () => { + beforeEach(() => { + wrapper = Wrapper({ withCopymessage: false, withInviteCodes: true }) + }) + + it('can copy a link', async () => { + const clipboardMock = jest.spyOn(navigator.clipboard, 'writeText').mockResolvedValue() + const copyButton = screen.getAllByLabelText('invite-codes.copy-code')[0] + await fireEvent.click(copyButton) + expect(clipboardMock).toHaveBeenCalledWith( + 'http://localhost/registration?method=invite-code&inviteCode=test-invite-code-1', + ) + }) + }) + + describe('with copy message', () => { + beforeEach(() => { + wrapper = Wrapper({ withCopymessage: true, withInviteCodes: true }) + }) + + it('can copy the link with message', async () => { + const clipboardMock = jest.spyOn(navigator.clipboard, 'writeText').mockResolvedValue() + const copyButton = screen.getAllByLabelText('invite-codes.copy-code')[0] + await fireEvent.click(copyButton) + expect(clipboardMock).toHaveBeenCalledWith( + 'test-copy-message http://localhost/registration?method=invite-code&inviteCode=test-invite-code-1', + ) + }) + }) + + it('cannot generate more than the limit of invite codes', () => { + wrapper = Wrapper({ withInviteCodes: true, limit: 2 }) + const generateButton = screen.getByLabelText('invite-codes.generate-code') + expect(generateButton).toBeDisabled() + }) +}) diff --git a/webapp/components/_new/features/Invitations/InvitationList.vue b/webapp/components/_new/features/Invitations/InvitationList.vue new file mode 100644 index 000000000..355139cd2 --- /dev/null +++ b/webapp/components/_new/features/Invitations/InvitationList.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/webapp/components/_new/features/Invitations/__snapshots__/CreateInvitation.spec.js.snap b/webapp/components/_new/features/Invitations/__snapshots__/CreateInvitation.spec.js.snap new file mode 100644 index 000000000..ac2eb0a09 --- /dev/null +++ b/webapp/components/_new/features/Invitations/__snapshots__/CreateInvitation.spec.js.snap @@ -0,0 +1,140 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CreateInvitation.vue renders 1`] = ` +
+
+
+ invite-codes.generate-code-explanation +
+ +
+
+ +
+ + + +
+ + + +
+ + +
+
+
+`; + +exports[`CreateInvitation.vue renders with disabled button 1`] = ` +
+
+
+ invite-codes.generate-code-explanation +
+ +
+
+ +
+ + + +
+ + + +
+ + +
+
+
+`; diff --git a/webapp/components/_new/features/Invitations/__snapshots__/Invitation.spec.js.snap b/webapp/components/_new/features/Invitations/__snapshots__/Invitation.spec.js.snap new file mode 100644 index 000000000..10e9e68a5 --- /dev/null +++ b/webapp/components/_new/features/Invitations/__snapshots__/Invitation.spec.js.snap @@ -0,0 +1,147 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Invitation.vue when the invite code was not redeemed renders 1`] = ` +
+
  • +
    +
    + + test-invite-code + + + — + + + + test-comment + +
    + +
    + + + invite-codes.redeemed-count-0 + + +
    +
    + +
    + + + +
    +
  • +
    +`; + +exports[`Invitation.vue when the invite code was redeemed renders 1`] = ` +
    +
  • +
    +
    + + test-invite-code + + + — + + + + test-comment + +
    + +
    + + + invite-codes.redeemed-count + + +
    +
    + +
    + + + +
    +
  • +
    +`; diff --git a/webapp/components/_new/features/Invitations/__snapshots__/InvitationList.spec.js.snap b/webapp/components/_new/features/Invitations/__snapshots__/InvitationList.spec.js.snap new file mode 100644 index 000000000..6bad7db58 --- /dev/null +++ b/webapp/components/_new/features/Invitations/__snapshots__/InvitationList.spec.js.snap @@ -0,0 +1,296 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`InvitationList.vue renders 1`] = ` +
    +
    +
      + +
    • +
      +
      + + test-invite-code-1 + + + — + + + + test-comment + +
      + +
      + + + invite-codes.redeemed-count-0 + + +
      +
      + +
      + + + +
      +
    • +
    • +
      +
      + + test-invite-code-2 + + + — + + + + test-comment-2 + +
      + +
      + + + invite-codes.redeemed-count + + +
      +
      + +
      + + + +
      +
    • +
      +
    + +
    +
    + invite-codes.generate-code-explanation +
    + +
    +
    + +
    + + + +
    + + + +
    + + +
    +
    +
    +
    +`; + +exports[`InvitationList.vue renders empty state 1`] = ` +
    +
    +
    + + invite-codes.no-links + +
    + +
    +
    + invite-codes.generate-code-explanation +
    + +
    +
    + +
    + + + +
    + + + +
    + + +
    +
    +
    +
    +`; diff --git a/webapp/components/_new/features/SearchResults/SearchResults.spec.js b/webapp/components/_new/features/SearchResults/SearchResults.spec.js index ace02bd46..b80d10c71 100644 --- a/webapp/components/_new/features/SearchResults/SearchResults.spec.js +++ b/webapp/components/_new/features/SearchResults/SearchResults.spec.js @@ -26,15 +26,13 @@ describe('SearchResults', () => { beforeEach(() => { mocks = { $t: jest.fn(), - $env: { - CATEGORIES_ACTIVE: false, - }, } getters = { 'auth/user': () => { return { id: 'u343', name: 'Matt' } }, 'auth/isModerator': () => false, + 'categories/categoriesActive': () => false, } propsData = { pageSize: 12, diff --git a/webapp/components/_new/features/SearchResults/SearchResults.vue b/webapp/components/_new/features/SearchResults/SearchResults.vue index 94d569e70..71f94a489 100644 --- a/webapp/components/_new/features/SearchResults/SearchResults.vue +++ b/webapp/components/_new/features/SearchResults/SearchResults.vue @@ -48,6 +48,8 @@ @removePostFromList="posts = removePostFromList(post, posts)" @pinPost="pinPost(post, refetchPostList)" @unpinPost="unpinPost(post, refetchPostList)" + @pushPost="pushPost(post, refetchPostList)" + @unpushPost="unpushPost(post, refetchPostList)" @toggleObservePost=" (postId, value) => toggleObservePost(postId, value, refetchPostList) " diff --git a/webapp/components/_new/generic/BaseButton/BaseButton.vue b/webapp/components/_new/generic/BaseButton/BaseButton.vue index a51c3101c..7985e66c7 100644 --- a/webapp/components/_new/generic/BaseButton/BaseButton.vue +++ b/webapp/components/_new/generic/BaseButton/BaseButton.vue @@ -112,7 +112,8 @@ export default { } &.--circle { - width: $size-button-base; + width: var(--circle-button-width, $size-button-base); + height: var(--circle-button-width, $size-button-base); border-radius: 50%; } diff --git a/webapp/components/_new/generic/BaseCard/BaseCard.story.js b/webapp/components/_new/generic/BaseCard/BaseCard.story.js index 928aba5e6..87c905fda 100644 --- a/webapp/components/_new/generic/BaseCard/BaseCard.story.js +++ b/webapp/components/_new/generic/BaseCard/BaseCard.story.js @@ -1,6 +1,6 @@ import { storiesOf } from '@storybook/vue' import helpers from '~/storybook/helpers' -import logos from '~/constants/logos.js' +import logos from '~/constants/logosBranded.js' import BaseCard from './BaseCard.vue' storiesOf('Generic/BaseCard', module) diff --git a/webapp/components/_new/generic/BaseCard/BaseCard.vue b/webapp/components/_new/generic/BaseCard/BaseCard.vue index 86b1a6652..e6fcd5e49 100644 --- a/webapp/components/_new/generic/BaseCard/BaseCard.vue +++ b/webapp/components/_new/generic/BaseCard/BaseCard.vue @@ -64,7 +64,7 @@ export default { } &.--highlight { - border: $border-size-base solid $color-warning; + border: $border-size-base solid $color-ribbon-announcement; } &.--wide-content { diff --git a/webapp/components/_new/generic/BaseIcon/BaseIcon.vue b/webapp/components/_new/generic/BaseIcon/BaseIcon.vue index ef09e2e03..48d408c25 100644 --- a/webapp/components/_new/generic/BaseIcon/BaseIcon.vue +++ b/webapp/components/_new/generic/BaseIcon/BaseIcon.vue @@ -55,7 +55,7 @@ export default { } &.--regular { - height: 1.2em; + height: var(--icon-size, 1.2em); } &.--large { diff --git a/webapp/components/generic/SearchableInput/SearchableInput.spec.js b/webapp/components/generic/SearchableInput/SearchableInput.spec.js index 223ee6186..c5fc4b072 100644 --- a/webapp/components/generic/SearchableInput/SearchableInput.spec.js +++ b/webapp/components/generic/SearchableInput/SearchableInput.spec.js @@ -11,6 +11,7 @@ localVue.filter('dateTime', () => Date.now) const stubs = { 'nuxt-link': true, + 'client-only': true, } describe('SearchableInput.vue', () => { diff --git a/webapp/components/utils/NormalizeEmail.js b/webapp/components/utils/NormalizeEmail.js deleted file mode 100644 index 45f8126e3..000000000 --- a/webapp/components/utils/NormalizeEmail.js +++ /dev/null @@ -1,11 +0,0 @@ -import { normalizeEmail } from 'validator' - -export default (email) => - normalizeEmail(email, { - // gmail_remove_dots: false, default - gmail_remove_subaddress: false, - // gmail_convert_googlemaildotcom: true, default - outlookdotcom_remove_subaddress: false, - yahoo_remove_subaddress: false, - icloud_remove_subaddress: false, - }) diff --git a/webapp/components/utils/isTouchDevice.js b/webapp/components/utils/isTouchDevice.js new file mode 100644 index 000000000..a6bc17752 --- /dev/null +++ b/webapp/components/utils/isTouchDevice.js @@ -0,0 +1,2 @@ +export const isTouchDevice = () => + 'ontouchstart' in window || navigator.MaxTouchPoints > 0 || navigator.msMaxTouchPoints > 0 diff --git a/webapp/config/index.js b/webapp/config/index.js index 5da17010b..400b5e14d 100644 --- a/webapp/config/index.js +++ b/webapp/config/index.js @@ -34,7 +34,16 @@ const options = { // Cookies COOKIE_EXPIRE_TIME: process.env.COOKIE_EXPIRE_TIME || 730, // Two years by default COOKIE_HTTPS_ONLY: process.env.COOKIE_HTTPS_ONLY || process.env.NODE_ENV === 'production', // ensure true in production if not set explicitly - CATEGORIES_ACTIVE: process.env.CATEGORIES_ACTIVE === 'true' || false, + BADGES_ENABLED: process.env.BADGES_ENABLED === 'true' || false, + INVITE_LINK_LIMIT: process.env.INVITE_LINK_LIMIT || 7, + NETWORK_NAME: process.env.NETWORK_NAME || 'Ocelot.social', + ASK_FOR_REAL_NAME: process.env.ASK_FOR_REAL_NAME === 'true' || false, + REQUIRE_LOCATION: process.env.REQUIRE_LOCATION === 'true' || false, +} + +const language = { + LANGUAGE_DEFAULT: process.env.LANGUAGE_DEFAULT || 'en', + LANGUAGE_FALLBACK: process.env.LANGUAGE_FALLBACK || 'en', } const CONFIG = { @@ -42,6 +51,7 @@ const CONFIG = { ...server, ...sentry, ...options, + ...language, } // override process.env with the values here since they contain default values diff --git a/webapp/constants/badges.js b/webapp/constants/badges.js new file mode 100644 index 000000000..bccebb39a --- /dev/null +++ b/webapp/constants/badges.js @@ -0,0 +1,2 @@ +// this file is duplicated in `backend/src/constants/badges` and `webapp/constants/badges.js` +export const TROPHY_BADGES_SELECTED_MAX = 9 diff --git a/webapp/constants/chat.js b/webapp/constants/chat.js index c278dfd62..d471b85b8 100644 --- a/webapp/constants/chat.js +++ b/webapp/constants/chat.js @@ -44,9 +44,9 @@ const STYLE = { }, sidemenu: { - background: '#fff', - backgroundHover: '#f6f6f6', - backgroundActive: styleData.colorPrimaryLight, + background: styleData.chatSidemenuBg, + backgroundHover: styleData.chatSidemenuBackgroundOver, + backgroundActive: styleData.chatSidemenuBackgroundActive, colorActive: '#1976d2', borderColorSearch: '#e1e5e8', }, @@ -114,12 +114,12 @@ const STYLE = { }, emoji: { - background: '#fff', + background: 'white', }, icons: { search: '#9ca6af', - add: styleData.colorPrimary, + add: styleData.chatIconAdd, toggle: styleData.colorNeutral30, menu: styleData.colorNeutral30, close: '#9ca6af', @@ -128,9 +128,9 @@ const STYLE = { paperclip: styleData.colorPrimary, closeOutline: '#000', closePreview: '#fff', - send: styleData.colorPrimary, + send: styleData.chatIconSend, sendDisabled: '#9ca6af', - emoji: styleData.colorPrimary, + emoji: styleData.chatIconEmoji, emojiReaction: 'rgba(0, 0, 0, 0.3)', document: styleData.colorPrimary, pencil: '#9e9e9e', diff --git a/webapp/constants/links.js b/webapp/constants/links.js index a252b98cd..3b537b81b 100644 --- a/webapp/constants/links.js +++ b/webapp/constants/links.js @@ -22,7 +22,7 @@ const ORGANIZATION = defaultPageParamsPages.ORGANIZATION.overwrite({ const DONATE = defaultPageParamsPages.DONATE.overwrite({ // if defined it's dominating externalLink: { - url: 'https://busfaktor.org/en/spenden', + url: 'https://ocelot.social/en/donate/', target: '_blank', }, @@ -38,7 +38,7 @@ const DONATE = defaultPageParamsPages.DONATE.overwrite({ }) const IMPRINT = defaultPageParamsPages.IMPRINT.overwrite({ externalLink: { - url: 'http://ocelot.social/en/impressum', + url: 'https://ocelot.social/en/imprint/', target: '_blank', }, diff --git a/webapp/constants/login.js b/webapp/constants/login.js new file mode 100644 index 000000000..b1c6ea436 --- /dev/null +++ b/webapp/constants/login.js @@ -0,0 +1 @@ +export default {} diff --git a/webapp/constants/loginBranded.js b/webapp/constants/loginBranded.js new file mode 100644 index 000000000..41f4cafdc --- /dev/null +++ b/webapp/constants/loginBranded.js @@ -0,0 +1,8 @@ +import { merge } from 'lodash' +import login from '~/constants/login.js' + +const defaultLogin = { + LAYOUT: 'no-header', +} + +export default merge(defaultLogin, login) diff --git a/webapp/constants/logos.js b/webapp/constants/logos.js index 714e78a2c..163c2fd6a 100644 --- a/webapp/constants/logos.js +++ b/webapp/constants/logos.js @@ -1,24 +1,3 @@ // this file is duplicated in `backend/src/config/logos.js` and `webapp/constants/logos.js` and replaced on rebranding // this are the paths in the webapp -export default { - LOGO_HEADER_PATH: '/img/custom/logo-horizontal.svg', - LOGO_HEADER_WIDTH: '130px', - LOGO_HEADER_CLICK: { - // externalLink: { - // url: 'https://ocelot.social', - // target: '_blank', - // }, - externalLink: null, - internalPath: { - to: { - name: 'index', - }, - scrollTo: '.main-navigation', - }, - }, - LOGO_SIGNUP_PATH: '/img/custom/logo-squared.svg', - LOGO_WELCOME_PATH: '/img/custom/logo-squared.svg', - LOGO_LOGOUT_PATH: '/img/custom/logo-squared.svg', - LOGO_PASSWORD_RESET_PATH: '/img/custom/logo-squared.svg', - LOGO_MAINTENACE_RESET_PATH: '/img/custom/logo-squared.svg', -} +export default {} diff --git a/webapp/constants/logosBranded.js b/webapp/constants/logosBranded.js new file mode 100644 index 000000000..25d1541a6 --- /dev/null +++ b/webapp/constants/logosBranded.js @@ -0,0 +1,31 @@ +// this file is duplicated in `backend/src/config/logos.js` and `webapp/constants/logos.js` and replaced on rebranding +// this are the paths in the webapp +import { merge } from 'lodash' +import logos from '~/constants/logos' + +const defaultLogos = { + LOGO_HEADER_PATH: '/img/custom/logo-horizontal.svg', + LOGO_HEADER_MOBILE_PATH: '/img/custom/logo-horizontal.svg', + LOGO_HEADER_WIDTH: '130px', + LOGO_HEADER_MOBILE_WIDTH: '100px', + LOGO_HEADER_CLICK: { + // externalLink: { + // url: 'https://ocelot.social', + // target: '_blank', + // }, + externalLink: null, + internalPath: { + to: { + name: 'index', + }, + scrollTo: '.main-navigation', + }, + }, + LOGO_SIGNUP_PATH: '/img/custom/logo-squared.svg', + LOGO_WELCOME_PATH: '/img/custom/logo-squared.svg', + LOGO_LOGOUT_PATH: '/img/custom/logo-squared.svg', + LOGO_PASSWORD_RESET_PATH: '/img/custom/logo-squared.svg', + LOGO_MAINTENACE_RESET_PATH: '/img/custom/logo-squared.svg', +} + +export default merge(defaultLogos, logos) diff --git a/webapp/constants/registration.js b/webapp/constants/registration.js index 9e63e478e..8ebb40573 100644 --- a/webapp/constants/registration.js +++ b/webapp/constants/registration.js @@ -1,5 +1,2 @@ -// this file is duplicated in `backend/src/config/metadata.js` and `webapp/constants/metadata.js` -export default { - NONCE_LENGTH: 5, - INVITE_CODE_LENGTH: 6, -} +// this file is duplicated in `backend/src/config/registration.ts` and `webapp/constants/registration.js` +export default {} diff --git a/webapp/constants/registrationBranded.js b/webapp/constants/registrationBranded.js new file mode 100644 index 000000000..8082a41e4 --- /dev/null +++ b/webapp/constants/registrationBranded.js @@ -0,0 +1,12 @@ +// this file is duplicated in `backend/src/config/registrationBranded.ts` and `webapp/constants/registrationBranded.js` +import { merge } from 'lodash' + +import registration from '~/constants/registration.js' + +const defaultRegistration = { + NONCE_LENGTH: 5, + INVITE_CODE_LENGTH: 6, + LAYOUT: 'no-header', +} + +export default merge(defaultRegistration, registration) diff --git a/webapp/graphql/CommentMutations.js b/webapp/graphql/CommentMutations.js index dd00527be..f413a4496 100644 --- a/webapp/graphql/CommentMutations.js +++ b/webapp/graphql/CommentMutations.js @@ -29,7 +29,7 @@ export default (i18n) => { commentedCount followedByCount followedByCurrentUser - badges { + badgeTrophies { id icon } diff --git a/webapp/graphql/Fragments.js b/webapp/graphql/Fragments.js index 32337230b..4f82eea23 100644 --- a/webapp/graphql/Fragments.js +++ b/webapp/graphql/Fragments.js @@ -17,18 +17,26 @@ export const locationFragment = (lang) => gql` fragment location on User { locationName location { + id name: name${lang} lng lat + distanceToMe } } ` export const badgesFragment = gql` fragment badges on User { - badges { + badgeTrophiesSelected { id icon + description + } + badgeVerification { + id + icon + description } } ` @@ -44,6 +52,19 @@ export const userCountsFragment = gql` } ` +export const userTeaserFragment = (lang) => gql` + ${badgesFragment} + ${locationFragment(lang)} + + fragment userTeaser on User { + followedByCount + contributionsCount + commentedCount + ...badges + ...location + } +` + export const postFragment = gql` fragment post on Post { id @@ -52,6 +73,7 @@ export const postFragment = gql` contentExcerpt createdAt updatedAt + sortDate disabled deleted slug @@ -137,5 +159,7 @@ export const commentFragment = gql` contentExcerpt isPostObservedByMe postObservingUsersCount + shoutedByCurrentUser + shoutedCount } ` diff --git a/webapp/graphql/InviteCode.js b/webapp/graphql/InviteCode.js new file mode 100644 index 000000000..10981327d --- /dev/null +++ b/webapp/graphql/InviteCode.js @@ -0,0 +1,138 @@ +import gql from 'graphql-tag' + +export const validateInviteCode = () => gql` + query validateInviteCode($code: String!) { + validateInviteCode(code: $code) { + code + invitedTo { + slug + groupType + name + about + avatar { + url + } + } + generatedBy { + name + avatar { + url + } + } + isValid + } + } +` + +export const generatePersonalInviteCode = () => gql` + mutation generatePersonalInviteCode($expiresAt: String, $comment: String) { + generatePersonalInviteCode(expiresAt: $expiresAt, comment: $comment) { + code + createdAt + generatedBy { + id + name + avatar { + url + } + } + redeemedBy { + id + name + avatar { + url + } + } + redeemedByCount + expiresAt + comment + invitedTo { + groupType + name + about + avatar { + url + } + } + isValid + } + } +` + +export const generateGroupInviteCode = () => gql` + mutation generateGroupInviteCode($groupId: ID!, $expiresAt: String, $comment: String) { + generateGroupInviteCode(groupId: $groupId, expiresAt: $expiresAt, comment: $comment) { + code + createdAt + generatedBy { + id + name + avatar { + url + } + } + redeemedBy { + id + name + avatar { + url + } + } + redeemedByCount + expiresAt + comment + invitedTo { + id + groupType + name + about + avatar { + url + } + } + isValid + } + } +` + +export const invalidateInviteCode = () => gql` + mutation invalidateInviteCode($code: String!) { + invalidateInviteCode(code: $code) { + code + createdAt + generatedBy { + id + name + avatar { + url + } + } + redeemedBy { + id + name + avatar { + url + } + } + redeemedByCount + expiresAt + comment + invitedTo { + id + groupType + name + about + avatar { + url + } + } + isValid + } + } +` + +export const redeemInviteCode = () => gql` + mutation redeemInviteCode($code: String!) { + redeemInviteCode(code: $code) + } +` diff --git a/webapp/graphql/PostMutations.js b/webapp/graphql/PostMutations.js index 5f29534a3..862615e09 100644 --- a/webapp/graphql/PostMutations.js +++ b/webapp/graphql/PostMutations.js @@ -168,6 +168,40 @@ export default () => { } } `, + pushPost: gql` + mutation ($id: ID!) { + pushPost(id: $id) { + id + title + slug + content + contentExcerpt + language + pinnedBy { + id + name + role + } + } + } + `, + unpushPost: gql` + mutation ($id: ID!) { + unpushPost(id: $id) { + id + title + slug + content + contentExcerpt + language + pinnedBy { + id + name + role + } + } + } + `, markTeaserAsViewed: gql` mutation ($id: ID!) { markTeaserAsViewed(id: $id) { diff --git a/webapp/graphql/PostQuery.js b/webapp/graphql/PostQuery.js index 29b7a1f07..a3dd67193 100644 --- a/webapp/graphql/PostQuery.js +++ b/webapp/graphql/PostQuery.js @@ -53,6 +53,7 @@ export default (i18n) => { id name slug + groupType } } } @@ -95,6 +96,7 @@ export const filterPosts = (i18n) => { id name slug + groupType } } } @@ -136,6 +138,7 @@ export const profilePagePosts = (i18n) => { id name slug + groupType } } } @@ -187,3 +190,14 @@ export const relatedContributions = (i18n) => { } ` } + +export const postsPinnedCountsQuery = () => { + return gql` + query { + PostsPinnedCounts { + maxPinnedPosts + currentlyPinnedPosts + } + } + ` +} diff --git a/webapp/graphql/Registration.js b/webapp/graphql/Registration.js index b6cc610d9..002f09c1a 100644 --- a/webapp/graphql/Registration.js +++ b/webapp/graphql/Registration.js @@ -9,6 +9,7 @@ export const SignupVerificationMutation = gql` $about: String $termsAndConditionsAgreedVersion: String! $locale: String + $locationName: String ) { SignupVerification( nonce: $nonce @@ -19,6 +20,7 @@ export const SignupVerificationMutation = gql` about: $about termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion locale: $locale + locationName: $locationName ) { id name diff --git a/webapp/graphql/User.js b/webapp/graphql/User.js index 4b743a0e3..e51ebd28f 100644 --- a/webapp/graphql/User.js +++ b/webapp/graphql/User.js @@ -7,6 +7,7 @@ import { postFragment, commentFragment, groupFragment, + userTeaserFragment, } from './Fragments' export const profileUserQuery = (i18n) => { @@ -46,7 +47,6 @@ export const profileUserQuery = (i18n) => { url } showShoutsPublicly - sendNotificationEmails } } ` @@ -91,6 +91,23 @@ export const adminUserQuery = () => { ` } +export const adminUserBadgesQuery = () => { + return gql` + query User($id: ID!) { + User(id: $id) { + id + name + badgeTrophies { + id + } + badgeVerification { + id + } + } + } + ` +} + export const mapUserQuery = (i18n) => { const lang = i18n.locale().toUpperCase() return gql` @@ -109,7 +126,7 @@ export const mapUserQuery = (i18n) => { ` } -export const notificationQuery = (_i18n) => { +export const notificationQuery = () => { return gql` ${userFragment} ${commentFragment} @@ -335,7 +352,7 @@ export const updateUserMutation = () => { $about: String $allowEmbedIframes: Boolean $showShoutsPublicly: Boolean - $sendNotificationEmails: Boolean + $emailNotificationSettings: [EmailNotificationSettingsInput] $termsAndConditionsAgreedVersion: String $avatar: ImageInput $locationName: String # empty string '' sets it to null @@ -347,7 +364,7 @@ export const updateUserMutation = () => { about: $about allowEmbedIframes: $allowEmbedIframes showShoutsPublicly: $showShoutsPublicly - sendNotificationEmails: $sendNotificationEmails + emailNotificationSettings: $emailNotificationSettings termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion avatar: $avatar locationName: $locationName @@ -359,12 +376,23 @@ export const updateUserMutation = () => { about allowEmbedIframes showShoutsPublicly - sendNotificationEmails + emailNotificationSettings { + type + settings { + name + value + } + } locale termsAndConditionsAgreedVersion avatar { url } + badgeVerification { + id + description + icon + } } } ` @@ -383,6 +411,31 @@ export const currentUserQuery = gql` query { currentUser { ...user + inviteCodes { + code + isValid + redeemedBy { + id + } + comment + redeemedByCount + } + badgeTrophiesSelected { + id + icon + description + isDefault + } + badgeTrophiesUnused { + id + icon + description + } + badgeVerification { + id + icon + description + } email role about @@ -390,7 +443,13 @@ export const currentUserQuery = gql` locale allowEmbedIframes showShoutsPublicly - sendNotificationEmails + emailNotificationSettings { + type + settings { + name + value + } + } termsAndConditionsAgreedVersion socialMedia { id @@ -438,3 +497,55 @@ export const userDataQuery = (i18n) => { } ` } + +export const userTeaserQuery = (i18n) => { + const lang = i18n.locale().toUpperCase() + return gql` + ${userTeaserFragment(lang)} + query ($id: ID!) { + User(id: $id) { + ...userTeaser + } + } + ` +} + +export const setTrophyBadgeSelected = gql` + mutation ($slot: Int!, $badgeId: ID) { + setTrophyBadgeSelected(slot: $slot, badgeId: $badgeId) { + badgeTrophiesCount + badgeTrophiesSelected { + id + icon + description + isDefault + } + badgeTrophiesUnused { + id + icon + description + } + badgeTrophiesUnusedCount + } + } +` + +export const resetTrophyBadgesSelected = gql` + mutation { + resetTrophyBadgesSelected { + badgeTrophiesCount + badgeTrophiesSelected { + id + icon + description + isDefault + } + badgeTrophiesUnused { + id + icon + description + } + badgeTrophiesUnusedCount + } + } +` diff --git a/webapp/graphql/admin/Badges.js b/webapp/graphql/admin/Badges.js new file mode 100644 index 000000000..2c037f2f3 --- /dev/null +++ b/webapp/graphql/admin/Badges.js @@ -0,0 +1,54 @@ +import gql from 'graphql-tag' + +export const queryBadges = () => gql` + query { + Badge { + id + type + icon + description + } + } +` + +export const setVerificationBadge = () => gql` + mutation ($badgeId: ID!, $userId: ID!) { + setVerificationBadge(badgeId: $badgeId, userId: $userId) { + id + badgeVerification { + id + } + badgeTrophies { + id + } + } + } +` + +export const rewardTrophyBadge = () => gql` + mutation ($badgeId: ID!, $userId: ID!) { + rewardTrophyBadge(badgeId: $badgeId, userId: $userId) { + id + badgeVerification { + id + } + badgeTrophies { + id + } + } + } +` + +export const revokeBadge = () => gql` + mutation ($badgeId: ID!, $userId: ID!) { + revokeBadge(badgeId: $badgeId, userId: $userId) { + id + badgeVerification { + id + } + badgeTrophies { + id + } + } + } +` diff --git a/webapp/graphql/admin/Statistics.js b/webapp/graphql/admin/Statistics.js index 94c3f91f0..f21af88ba 100644 --- a/webapp/graphql/admin/Statistics.js +++ b/webapp/graphql/admin/Statistics.js @@ -3,13 +3,27 @@ import gql from 'graphql-tag' export const Statistics = gql` query { statistics { - countUsers - countPosts - countComments - countNotifications - countInvites - countFollows - countShouts + users + usersDeleted + posts + comments + notifications + emails + follows + shouts + invites + chatMessages + chatRooms + tags + locations + groups + inviteCodes + inviteCodesExpired + inviteCodesRedeemed + badgesRewarded + badgesDisplayed + usersVerified + reports } } ` diff --git a/webapp/graphql/groups.js b/webapp/graphql/groups.js index 6aedc205d..5ce33407b 100644 --- a/webapp/graphql/groups.js +++ b/webapp/graphql/groups.js @@ -177,6 +177,7 @@ export const groupQuery = (i18n) => { descriptionExcerpt groupType actionRadius + isMutedByMe categories { id slug @@ -194,6 +195,16 @@ export const groupQuery = (i18n) => { lat } myRole + inviteCodes { + createdAt + code + isValid + redeemedBy { + id + } + comment + redeemedByCount + } } } ` diff --git a/webapp/graphql/inviteCodes.js b/webapp/graphql/inviteCodes.js new file mode 100644 index 000000000..9ff171759 --- /dev/null +++ b/webapp/graphql/inviteCodes.js @@ -0,0 +1,19 @@ +import gql from 'graphql-tag' + +export const validateInviteCodeQuery = gql` + query ($code: String!) { + validateInviteCode(code: $code) { + invitedTo { + id + slug + groupType + } + } + } +` + +export const redeemInviteCodeMutation = gql` + mutation ($code: String!) { + redeemInviteCode(code: $code) + } +` diff --git a/webapp/graphql/settings/MutedGroups.js b/webapp/graphql/settings/MutedGroups.js new file mode 100644 index 000000000..847fac4c4 --- /dev/null +++ b/webapp/graphql/settings/MutedGroups.js @@ -0,0 +1,25 @@ +import gql from 'graphql-tag' + +export const muteGroup = () => { + return gql` + mutation ($groupId: ID!) { + muteGroup(groupId: $groupId) { + id + name + isMutedByMe + } + } + ` +} + +export const unmuteGroup = () => { + return gql` + mutation ($groupId: ID!) { + unmuteGroup(groupId: $groupId) { + id + name + isMutedByMe + } + } + ` +} diff --git a/webapp/jest.config.js b/webapp/jest.config.js index 947d6019d..fb51fc7a7 100644 --- a/webapp/jest.config.js +++ b/webapp/jest.config.js @@ -17,7 +17,7 @@ module.exports = { ], coverageThreshold: { global: { - lines: 83, + lines: 82, }, }, coverageProvider: 'v8', diff --git a/webapp/jsconfig.json b/webapp/jsconfig.json index 7e3695e4e..98874805a 100644 --- a/webapp/jsconfig.json +++ b/webapp/jsconfig.json @@ -3,11 +3,20 @@ "baseUrl": ".", "paths": { "~/*": [ - "./*" + "*" ], "~*": [ + "*" + ], + "~@": [ + "*" + ], + "@": [ + "*" + ], + "@@/*": [ "./*" ], } } -} \ No newline at end of file +} diff --git a/webapp/layouts/basic.vue b/webapp/layouts/basic.vue index 5eadb42af..ac0e63114 100644 --- a/webapp/layouts/basic.vue +++ b/webapp/layouts/basic.vue @@ -23,7 +23,7 @@
    -
    +
    @@ -61,7 +61,7 @@ export default { } - diff --git a/webapp/layouts/blank.vue b/webapp/layouts/blank.vue index bd8d4f29a..06010ee3b 100644 --- a/webapp/layouts/blank.vue +++ b/webapp/layouts/blank.vue @@ -1,7 +1,7 @@ + @@ -75,7 +85,6 @@ diff --git a/webapp/pages/groups/edit/_id/__snapshots__/invites.spec.js.snap b/webapp/pages/groups/edit/_id/__snapshots__/invites.spec.js.snap new file mode 100644 index 000000000..2d1155691 --- /dev/null +++ b/webapp/pages/groups/edit/_id/__snapshots__/invites.spec.js.snap @@ -0,0 +1,167 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`invites.vue renders 1`] = ` +
    +
    +
    +

    + invite-codes.group-invite-links +

    + +
    + +
    +
      + +
    • +
      +
      + + INVITE1 + + + — + + + + Test invite 1 + +
      + +
      + + + invite-codes.redeemed-count-0 + + +
      +
      + +
      + + + +
      +
    • +
      +
    + +
    +
    + invite-codes.generate-code-explanation +
    + +
    +
    + +
    + + + +
    + + + +
    + + +
    +
    +
    + + +
    +
    +
    +`; diff --git a/webapp/pages/groups/edit/_id/invites.spec.js b/webapp/pages/groups/edit/_id/invites.spec.js new file mode 100644 index 000000000..8c163a4e9 --- /dev/null +++ b/webapp/pages/groups/edit/_id/invites.spec.js @@ -0,0 +1,86 @@ +import { render, screen, fireEvent } from '@testing-library/vue' + +import invites from './invites.vue' + +const localVue = global.localVue + +describe('invites.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn((v) => v), + $apollo: { + mutate: jest.fn(), + }, + $env: { + NETWORK_NAME: 'test-network', + INVITE_LINK_LIMIT: 5, + }, + $toast: { + success: jest.fn(), + error: jest.fn(), + }, + localVue, + } + }) + + const Wrapper = () => { + return render(invites, { + localVue, + propsData: { + group: { + id: 'group1', + name: 'Group 1', + inviteCodes: [ + { + code: 'INVITE1', + comment: 'Test invite 1', + redeemedByCount: 0, + isValid: true, + }, + { + code: 'INVITE2', + comment: 'Test invite 2', + redeemedByCount: 1, + isValid: false, + }, + ], + }, + }, + mocks, + stubs: { + 'client-only': true, + }, + }) + } + + it('renders', () => { + wrapper = Wrapper() + expect(wrapper.container).toMatchSnapshot() + }) + + describe('when a new invite code is generated', () => { + beforeEach(async () => { + wrapper = Wrapper() + const createButton = screen.getByLabelText('invite-codes.generate-code') + await fireEvent.click(createButton) + }) + + it('calls the mutation to generate a new invite code', () => { + expect(mocks.$apollo.mutate).toHaveBeenCalledWith({ + mutation: expect.anything(), + update: expect.anything(), + variables: { + groupId: 'group1', + comment: '', + }, + }) + }) + + it('shows a success message', () => { + expect(mocks.$toast.success).toHaveBeenCalledWith('invite-codes.create-success') + }) + }) +}) diff --git a/webapp/pages/groups/edit/_id/invites.vue b/webapp/pages/groups/edit/_id/invites.vue new file mode 100644 index 000000000..5181c21f1 --- /dev/null +++ b/webapp/pages/groups/edit/_id/invites.vue @@ -0,0 +1,81 @@ + + + diff --git a/webapp/pages/index.spec.js b/webapp/pages/index.spec.js index 86a852a83..3fd38530a 100644 --- a/webapp/pages/index.spec.js +++ b/webapp/pages/index.spec.js @@ -32,12 +32,17 @@ describe('PostIndex', () => { 'posts/articleSetInPostTypeFilter': () => false, 'posts/eventSetInPostTypeFilter': () => false, 'posts/eventsEnded': () => '', - 'posts/orderBy': () => 'createdAt_desc', + 'posts/orderBy': () => 'sortDate_desc', 'auth/user': () => { return { id: 'u23' } }, + 'categories/categoriesActive': () => true, + 'categories/categories': () => ['cat1', 'cat2', 'cat3'], }, mutations, + actions: { + 'categories/init': jest.fn(), + }, }) mocks = { $t: (key) => key, @@ -79,9 +84,6 @@ describe('PostIndex', () => { $route: { query: {}, }, - $env: { - CATEGORIES_ACTIVE: true, - }, } }) diff --git a/webapp/pages/index.vue b/webapp/pages/index.vue index c780b0ae3..5484c2a2e 100644 --- a/webapp/pages/index.vue +++ b/webapp/pages/index.vue @@ -116,6 +116,8 @@ @removePostFromList="posts = removePostFromList(post, posts)" @pinPost="pinPost(post, refetchPostList)" @unpinPost="unpinPost(post, refetchPostList)" + @pushPost="pushPost(post, refetchPostList)" + @unpushPost="unpushPost(post, refetchPostList)" @toggleObservePost=" (postId, value) => toggleObservePost(postId, value, refetchPostList) " @@ -155,6 +157,7 @@ import UpdateQuery from '~/components/utils/UpdateQuery' import FilterMenuComponent from '~/components/FilterMenu/FilterMenuComponent' import { SHOW_CONTENT_FILTER_MASONRY_GRID } from '~/constants/filter.js' import { POST_ADD_BUTTON_POSITION_TOP } from '~/constants/posts.js' +import GetCategories from '~/mixins/getCategoriesMixin.js' export default { components: { @@ -167,7 +170,7 @@ export default { FilterMenuComponent, HeaderButton, }, - mixins: [postListActions, mobile()], + mixins: [postListActions, mobile(), GetCategories], data() { const { hashtag = null } = this.$route.query return { @@ -184,7 +187,6 @@ export default { offset: 0, pageSize: 12, hashtag, - categoriesActive: this.$env.CATEGORIES_ACTIVE, SHOW_CONTENT_FILTER_MASONRY_GRID, POST_ADD_BUTTON_POSITION_TOP, } diff --git a/webapp/pages/login.spec.js b/webapp/pages/login.spec.js index 31ff2b489..6f54b2693 100644 --- a/webapp/pages/login.spec.js +++ b/webapp/pages/login.spec.js @@ -1,14 +1,20 @@ import Vuex from 'vuex' import { mount } from '@vue/test-utils' import login from './login.vue' +import LoginForm from '~/components/LoginForm/LoginForm.vue' const localVue = global.localVue const stubs = { 'client-only': true, 'nuxt-link': true, + 'router-link': true, } +const routerPushMock = jest.fn() +const routerReplaceMock = jest.fn() +const i18nSetMock = jest.fn() + describe('Login.vue', () => { let store let mocks @@ -22,6 +28,14 @@ describe('Login.vue', () => { $t: jest.fn(), $i18n: { locale: () => 'en', + set: i18nSetMock, + }, + $route: { + query: {}, + }, + $router: { + replace: routerReplaceMock, + push: routerPushMock, }, } asyncData = false @@ -73,5 +87,51 @@ describe('Login.vue', () => { wrapper = await Wrapper() expect(redirect).toHaveBeenCalledWith('/') }) + + describe('handle succcess', () => { + beforeEach(async () => { + asyncData = true + tosVersion = '0.0.4' + }) + + describe('with route query to invite code', () => { + beforeEach(async () => { + mocks.$route.query = { + inviteCode: 'ABCDEF', + } + wrapper = await Wrapper() + wrapper.findComponent(LoginForm).vm.$emit('success') + }) + + it('calls i18n.set', () => { + expect(i18nSetMock).toBeCalledWith('en') + }) + + it('call router push to registration page', () => { + expect(routerPushMock).toBeCalledWith({ + name: 'registration', + query: { + inviteCode: 'ABCDEF', + }, + }) + }) + }) + + describe('without route query to invite code', () => { + beforeEach(async () => { + mocks.$route.query = {} + wrapper = await Wrapper() + wrapper.findComponent(LoginForm).vm.$emit('success') + }) + + it('calls i18n.set', () => { + expect(i18nSetMock).toBeCalledWith('en') + }) + + it('call router push to registration page', () => { + expect(routerReplaceMock).toBeCalledWith('/') + }) + }) + }) }) }) diff --git a/webapp/pages/login.vue b/webapp/pages/login.vue index c90b29146..883626112 100644 --- a/webapp/pages/login.vue +++ b/webapp/pages/login.vue @@ -1,16 +1,19 @@ diff --git a/webapp/pages/password-reset.vue b/webapp/pages/password-reset.vue index 6d67c4cae..fa3223f6d 100644 --- a/webapp/pages/password-reset.vue +++ b/webapp/pages/password-reset.vue @@ -17,6 +17,7 @@ + + diff --git a/webapp/pages/settings/__snapshots__/badges.spec.js.snap b/webapp/pages/settings/__snapshots__/badges.spec.js.snap new file mode 100644 index 000000000..4f80bc37e --- /dev/null +++ b/webapp/pages/settings/__snapshots__/badges.spec.js.snap @@ -0,0 +1,429 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`badge settings with badges more badges available selecting an empty slot shows list with available badges 1`] = ` +
    +
    +

    + settings.badges.name +

    + +

    + settings.badges.description +

    + +
    +
    +
    +
    + +
    + + + +
    +
    + + + +
    + + + settings.badges.click-to-use + + +
    + + + +
    +
    + + +
    +
    +
    + + +
    +
    +`; + +exports[`badge settings with badges no more badges available selecting an empty slot shows no more badges available message 1`] = ` +
    +
    +

    + settings.badges.name +

    + +

    + settings.badges.description +

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

    + + settings.badges.no-badges-available + +

    + + + + + + +
    + + +
    +
    +`; + +exports[`badge settings with badges renders 1`] = ` +
    +
    +

    + settings.badges.name +

    + +

    + settings.badges.description +

    + +
    +
    +
    +
    + +
    + + + +
    +
    + + + +
    + + + settings.badges.click-to-select + + +
    + + + + +
    + + +
    +
    +`; + +exports[`badge settings with badges selecting a used badge clicking remove badge button with successful server request removes the badge 1`] = ` +
    +
    +

    + settings.badges.name +

    + +

    + settings.badges.description +

    + +
    +
    +
    + + + + +
    +
    + + + + + + +
    + + +
    +
    +`; + +exports[`badge settings without badges renders 1`] = ` +
    +
    +

    + settings.badges.name +

    + +

    + settings.badges.description +

    + +
    +
    +
    +
    + +
    +
    +
    + + + + + + + + +
    + + +
    +
    +`; diff --git a/webapp/pages/settings/__snapshots__/notifications.spec.js.snap b/webapp/pages/settings/__snapshots__/notifications.spec.js.snap new file mode 100644 index 000000000..2078c26a2 --- /dev/null +++ b/webapp/pages/settings/__snapshots__/notifications.spec.js.snap @@ -0,0 +1,53 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`notifications.vue mount renders 1`] = ` +
    +

    settings.notifications.name

    +
    +
    +

    settings.notifications.post

    +
    +
    +
    +
    +
    +
    +
    +
    +

    settings.notifications.group

    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +`; diff --git a/webapp/pages/settings/badges.spec.js b/webapp/pages/settings/badges.spec.js new file mode 100644 index 000000000..291fd75d6 --- /dev/null +++ b/webapp/pages/settings/badges.spec.js @@ -0,0 +1,302 @@ +import { render, screen, fireEvent } from '@testing-library/vue' +import '@testing-library/jest-dom' +import badges from './badges.vue' + +const localVue = global.localVue + +describe('badge settings', () => { + let mocks + + const apolloMutateMock = jest.fn() + + const Wrapper = () => { + return render(badges, { + localVue, + mocks, + }) + } + + beforeEach(() => { + mocks = { + $t: jest.fn((t) => t), + $toast: { + success: jest.fn(), + error: jest.fn(), + }, + $apollo: { + mutate: apolloMutateMock, + }, + } + }) + + describe('without badges', () => { + beforeEach(() => { + mocks.$store = { + getters: { + 'auth/isModerator': () => false, + 'auth/user': { + id: 'u23', + badgeVerification: { + id: 'bv1', + icon: '/verification/icon', + description: 'Verification description', + isDefault: true, + }, + badgeTrophiesSelected: [], + badgeTrophiesUnused: [], + }, + }, + } + }) + + it('renders', () => { + const wrapper = Wrapper() + expect(wrapper.container).toMatchSnapshot() + }) + }) + + describe('with badges', () => { + const badgeTrophiesSelected = [ + { + id: '1', + icon: '/path/to/some/icon', + isDefault: false, + description: 'Some description', + }, + { + id: '2', + icon: '/path/to/empty/icon', + isDefault: true, + description: 'Empty', + }, + { + id: '3', + icon: '/path/to/third/icon', + isDefault: false, + description: 'Third description', + }, + ] + + const badgeTrophiesUnused = [ + { + id: '4', + icon: '/path/to/fourth/icon', + description: 'Fourth description', + }, + { + id: '5', + icon: '/path/to/fifth/icon', + description: 'Fifth description', + }, + ] + + let wrapper + + beforeEach(() => { + mocks.$store = { + getters: { + 'auth/isModerator': () => false, + 'auth/user': { + id: 'u23', + badgeVerification: { + id: 'bv1', + icon: '/verification/icon', + description: 'Verification description', + isDefault: false, + }, + badgeTrophiesSelected, + badgeTrophiesUnused, + }, + }, + } + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + describe('selecting a used badge', () => { + beforeEach(async () => { + const badge = screen.getByTitle(badgeTrophiesSelected[0].description) + await fireEvent.click(badge) + }) + + it('shows remove badge button', () => { + expect(screen.getByText('settings.badges.remove')).toBeInTheDocument() + }) + + describe('clicking remove badge button', () => { + const clickButton = async () => { + const removeButton = screen.getByText('settings.badges.remove') + await fireEvent.click(removeButton) + } + + describe('with successful server request', () => { + beforeEach(() => { + apolloMutateMock.mockResolvedValue({ + data: { + setTrophyBadgeSelected: { + id: 'u23', + badgeTrophiesSelected: [ + { + id: '2', + icon: '/path/to/empty/icon', + isDefault: true, + description: 'Empty', + }, + { + id: '2', + icon: '/path/to/empty/icon', + isDefault: true, + description: 'Empty', + }, + { + id: '3', + icon: '/path/to/third/icon', + isDefault: false, + description: 'Third description', + }, + ], + }, + }, + }) + clickButton() + }) + + it('calls the server', () => { + expect(apolloMutateMock).toHaveBeenCalledWith({ + mutation: expect.anything(), + update: expect.anything(), + variables: { + badgeId: null, + slot: 0, + }, + }) + }) + + /* To test this, we would need a better apollo mock */ + it.skip('removes the badge', async () => { + expect(wrapper.container).toMatchSnapshot() + }) + + it('shows a success message', () => { + expect(mocks.$toast.success).toHaveBeenCalledWith('settings.badges.success-update') + }) + }) + + describe('with failed server request', () => { + beforeEach(() => { + apolloMutateMock.mockRejectedValue({ message: 'Ouch!' }) + clickButton() + }) + + it('shows an error message', () => { + expect(mocks.$toast.error).toHaveBeenCalledWith('settings.badges.error-update') + }) + }) + }) + }) + + describe('no more badges available', () => { + beforeEach(async () => { + mocks.$store.getters['auth/user'].badgeTrophiesUnused = [] + }) + + describe('selecting an empty slot', () => { + beforeEach(async () => { + const emptySlot = screen.getAllByTitle('Empty')[0] + await fireEvent.click(emptySlot) + }) + + it('shows no more badges available message', () => { + expect(wrapper.container).toMatchSnapshot() + }) + }) + }) + + describe('more badges available', () => { + describe('selecting an empty slot', () => { + beforeEach(async () => { + const emptySlot = screen.getAllByTitle('Empty')[0] + await fireEvent.click(emptySlot) + }) + + it('shows list with available badges', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + describe('clicking on an available badge', () => { + const clickBadge = async () => { + const badge = screen.getByText(badgeTrophiesUnused[0].description) + await fireEvent.click(badge) + } + + describe('with successful server request', () => { + beforeEach(() => { + apolloMutateMock.mockResolvedValue({ + data: { + setTrophyBadgeSelected: { + id: 'u23', + badgeTrophiesSelected: [ + { + id: '4', + icon: '/path/to/fourth/icon', + description: 'Fourth description', + isDefault: false, + }, + { + id: '2', + icon: '/path/to/empty/icon', + isDefault: true, + description: 'Empty', + }, + { + id: '3', + icon: '/path/to/third/icon', + isDefault: false, + description: 'Third description', + }, + ], + }, + }, + }) + clickBadge() + }) + + it('calls the server', () => { + expect(apolloMutateMock).toHaveBeenCalledWith({ + mutation: expect.anything(), + update: expect.anything(), + variables: { + badgeId: '4', + slot: 1, + }, + }) + }) + + /* To test this, we would need a better apollo mock */ + it.skip('adds the badge', async () => { + expect(wrapper.container).toMatchSnapshot() + }) + + it('shows a success message', () => { + expect(mocks.$toast.success).toHaveBeenCalledWith('settings.badges.success-update') + }) + }) + + describe('with failed server request', () => { + beforeEach(() => { + apolloMutateMock.mockRejectedValue({ message: 'Ouch!' }) + clickBadge() + }) + + it('shows an error message', () => { + expect(mocks.$toast.error).toHaveBeenCalledWith('settings.badges.error-update') + }) + }) + }) + }) + }) + }) +}) diff --git a/webapp/pages/settings/badges.vue b/webapp/pages/settings/badges.vue new file mode 100644 index 000000000..3f0e7c7e7 --- /dev/null +++ b/webapp/pages/settings/badges.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/webapp/pages/settings/blocked-users.vue b/webapp/pages/settings/blocked-users.vue index 90519452f..0eed6d370 100644 --- a/webapp/pages/settings/blocked-users.vue +++ b/webapp/pages/settings/blocked-users.vue @@ -75,8 +75,10 @@ + + diff --git a/webapp/pages/settings/index.spec.js b/webapp/pages/settings/index.spec.js index 0de675840..fda60e6c5 100644 --- a/webapp/pages/settings/index.spec.js +++ b/webapp/pages/settings/index.spec.js @@ -1,6 +1,7 @@ import { mount } from '@vue/test-utils' import index from './index.vue' import Vuex from 'vuex' +import LocationSelect from '~/components/Select/LocationSelect' const localVue = global.localVue @@ -67,6 +68,9 @@ describe('index.vue', () => { error: jest.fn(), success: jest.fn(), }, + $env: { + ASK_FOR_REAL_NAME: false, + }, } getters = { 'auth/user': () => ({}), @@ -158,17 +162,7 @@ describe('index.vue', () => { describe('given a new location and hitting submit', () => { it('calls updateUser mutation', async () => { const wrapper = Wrapper() - wrapper.setData({ - cities: [ - { - label: 'Berlin, Germany', - value: 'Berlin, Germany', - id: '1', - }, - ], - }) - await wrapper.vm.$nextTick() - wrapper.find('.ds-select-option').trigger('click') + wrapper.findComponent(LocationSelect).vm.$emit('input', 'Berlin, Germany') wrapper.find('.ds-form').trigger('submit') await expect(mocks.$apollo.mutate).toHaveBeenCalledWith( @@ -201,25 +195,9 @@ describe('index.vue', () => { describe('given new username, slug, location and about then hitting submit', () => { it('calls updateUser mutation', async () => { const wrapper = Wrapper() - - wrapper.setData({ - cities: [ - { - label: 'Berlin, Germany', - value: 'Berlin, Germany', - id: '1', - }, - { - label: 'Hamburg, Germany', - value: 'Hamburg, Germany', - id: '2', - }, - ], - }) - await wrapper.vm.$nextTick() wrapper.find('#name').setValue('Peter') wrapper.find('#slug').setValue('peter-der-lustige') - wrapper.findAll('.ds-select-option').at(1).trigger('click') + await wrapper.findComponent(LocationSelect).vm.$emit('input', 'Hamburg, Germany') wrapper.find('#about').setValue('I am Peter!111elf') wrapper.find('.ds-form').trigger('submit') diff --git a/webapp/pages/settings/index.vue b/webapp/pages/settings/index.vue index 6cae7e44c..0bac6ea1c 100644 --- a/webapp/pages/settings/index.vue +++ b/webapp/pages/settings/index.vue @@ -1,5 +1,5 @@