Merge branch 'master' of github.com:Ocelot-Social-Community/Ocelot-Social into brand-reformer-network-first-step

This commit is contained in:
Wolfgang Huß 2025-04-12 10:45:07 +02:00
commit 713de2006e
417 changed files with 23771 additions and 12490 deletions

166
.github/dependabot.yml vendored
View File

@ -22,7 +22,7 @@ updates:
babel:
applies-to: version-updates
patterns:
- "@babel/*"
- "@babel*"
cypress:
applies-to: version-updates
patterns:
@ -53,14 +53,14 @@ updates:
timezone: "Europe/Berlin"
time: "03:00"
groups:
apollo:
apollo-server:
applies-to: version-updates
patterns:
- "*apollo*"
- "*apollo-server*"
babel:
applies-to: version-updates
patterns:
- "*babel*"
- "@babel*"
metascraper:
applies-to: version-updates
patterns:
@ -99,10 +99,6 @@ updates:
applies-to: version-updates
patterns:
- "@babel*"
eslint:
applies-to: version-updates
patterns:
- "eslint*"
jest:
applies-to: version-updates
patterns:
@ -111,10 +107,6 @@ updates:
applies-to: version-updates
patterns:
- "*mapbox*"
sass:
applies-to: version-updates
patterns:
- "sass*"
storybook:
applies-to: version-updates
patterns:
@ -145,78 +137,78 @@ updates:
time: "03:00"
# frontend
- package-ecosystem: npm
open-pull-requests-limit: 99
directory: "/frontend"
rebase-strategy: "disabled"
schedule:
interval: weekly
day: "saturday"
timezone: "Europe/Berlin"
time: "03:00"
groups:
eslint:
applies-to: version-updates
patterns:
- "eslint*"
- "@eslint*"
pinia:
applies-to: version-updates
patterns:
- "pinia*"
react:
applies-to: version-updates
patterns:
- "react*"
remark:
applies-to: version-updates
patterns:
- "remark*"
storybook:
applies-to: version-updates
patterns:
- "storybook"
- "@storybook*"
stylelint:
applies-to: version-updates
patterns:
- "stylelint*"
typescript:
applies-to: version-updates
patterns:
- "ts*"
- "@types*"
- "typescript"
vite:
applies-to: version-updates
patterns:
- "vite"
- "vite-plugin*"
- "@vitejs/plugin-vue"
vitest:
applies-to: version-updates
patterns:
- "vitest"
- "@vitest*"
vue:
applies-to: version-updates
patterns:
- "*vue?(/)*"
exclude-patterns:
- "vuetify"
- "*vuepress*"
- "vue-tsc"
vuepress:
applies-to: version-updates
patterns:
- "vuepress"
- "@vuepress*"
- package-ecosystem: docker
open-pull-requests-limit: 99
directory: "/frontend"
rebase-strategy: "disabled"
schedule:
interval: weekly
day: "saturday"
timezone: "Europe/Berlin"
time: "03:00"
# - package-ecosystem: npm
# open-pull-requests-limit: 99
# directory: "/frontend"
# rebase-strategy: "disabled"
# schedule:
# interval: weekly
# day: "saturday"
# timezone: "Europe/Berlin"
# time: "03:00"
# groups:
# eslint:
# applies-to: version-updates
# patterns:
# - "eslint*"
# - "@eslint*"
# pinia:
# applies-to: version-updates
# patterns:
# - "pinia*"
# react:
# applies-to: version-updates
# patterns:
# - "react*"
# remark:
# applies-to: version-updates
# patterns:
# - "remark*"
# storybook:
# applies-to: version-updates
# patterns:
# - "storybook"
# - "@storybook*"
# stylelint:
# applies-to: version-updates
# patterns:
# - "stylelint*"
# typescript:
# applies-to: version-updates
# patterns:
# - "ts*"
# - "@types*"
# - "typescript"
# vite:
# applies-to: version-updates
# patterns:
# - "vite"
# - "vite-plugin*"
# - "@vitejs/plugin-vue"
# vitest:
# applies-to: version-updates
# patterns:
# - "vitest"
# - "@vitest*"
# vue:
# applies-to: version-updates
# patterns:
# - "*vue?(/)*"
# exclude-patterns:
# - "vuetify"
# - "*vuepress*"
# - "vue-tsc"
# vuepress:
# applies-to: version-updates
# patterns:
# - "vuepress"
# - "@vuepress*"
# - package-ecosystem: docker
# open-pull-requests-limit: 99
# directory: "/frontend"
# rebase-strategy: "disabled"
# schedule:
# interval: weekly
# day: "saturday"
# timezone: "Europe/Berlin"
# time: "03:00"

View File

@ -30,11 +30,11 @@ jobs:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
- name: Remove old documentation files
run: rm -rf ./deployment/src/old/ ./CHANGELOG.md # workaround until https://github.com/gaurav-nelson/github-action-markdown-link-check/pull/183 has been done
- name: Remove uncheckable documentation files
run: rm -rf ./CHANGELOG.md # workaround until https://github.com/gaurav-nelson/github-action-markdown-link-check/pull/183 has been done
- name: Check Markdown Links
uses: gaurav-nelson/github-action-markdown-link-check@7d83e59a57f3c201c76eed3d33dff64ec4452d27 # 1.0.15
uses: gaurav-nelson/github-action-markdown-link-check@1b916f2cf6c36510a6059943104e3c42ce6c16bc # 1.0.15
with:
use-quiet-mode: 'yes'
use-verbose-mode: 'no'
@ -54,7 +54,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
- name: Setup Node 20
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.0.3
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.0.3
with:
node-version: '20'

View File

@ -30,7 +30,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
- name: Setup Node 20
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.0.3
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.0.3
with:
node-version: 20
@ -38,7 +38,7 @@ jobs:
run: npm install && npm run docs:build
- name: Deploy Vuepress to Github Pages
uses: crazy-max/ghaction-github-pages@fbf0a4fa4e00f45accd6cf3232368436ec06ed59 # v4.0.0
uses: crazy-max/ghaction-github-pages@df5cc2bfa78282ded844b354faee141f06b41865 # v4.0.0
with:
target_branch: gh-pages
build_dir: .vuepress/dist

91
.github/workflows/docker-push.yml vendored Normal file
View File

@ -0,0 +1,91 @@
name: docker-push
on: push
jobs:
build-and-push-images:
strategy:
matrix:
app:
- name: neo4j
context: neo4j
file: neo4j/Dockerfile
target: community
- name: backend-base
context: backend
file: backend/Dockerfile
target: base
- name: backend-build
context: backend
file: backend/Dockerfile
target: build
- name: backend
context: backend
file: backend/Dockerfile
target: production
- name: webapp-base
context: webapp
file: webapp/Dockerfile
target: base
- name: webapp-build
context: webapp
file: webapp/Dockerfile
target: build
- name: webapp
context: webapp
file: webapp/Dockerfile
target: production
- name: maintenance-base
context: webapp
file: webapp/Dockerfile.maintenance
target: base
- name: maintenance-build
context: webapp
file: webapp/Dockerfile.maintenance
target: build
- name: maintenance
context: webapp
file: webapp/Dockerfile.maintenance
target: production
runs-on: ubuntu-latest
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}/${{ matrix.app.name }}
permissions:
contents: read
packages: write
attestations: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
- name: Log in to the Container registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=schedule
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=ref,event=branch
type=ref,event=pr
type=sha
- name: Build and push Docker images
id: push
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4
with:
context: ${{ matrix.app.context }}
target: ${{ matrix.app.target }}
file: ${{ matrix.app.file }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@ -6,241 +6,12 @@ on:
- master
jobs:
##############################################################################
# JOB: DOCKER BUILD COMMUNITY NEO4J ##########################################
##############################################################################
build_production_neo4j:
name: Docker Build Production - Neo4J
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
- name: Setup env
run: |
echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
- run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
- name: Neo4J | Build `community` image
run: |
docker build --target community \
--tag "ocelotsocialnetwork/neo4j-community:latest" \
--tag "ocelotsocialnetwork/neo4j-community:${VERSION}" \
--tag "ocelotsocialnetwork/neo4j-community:${BUILD_VERSION}" \
--build-arg BBUILD_DATE=$BUILD_DATE \
--build-arg BBUILD_VERSION=$BUILD_VERSION \
--build-arg BBUILD_COMMIT=$BUILD_COMMIT \
neo4j/
- name: Neo4J | Save docker image
run: docker save "ocelotsocialnetwork/neo4j-community" > /tmp/neo4j.tar
- name: Upload Artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: docker-neo4j-community
path: /tmp/neo4j.tar
##############################################################################
# JOB: DOCKER BUILD PRODUCTION BACKEND #######################################
##############################################################################
build_production_backend:
name: Docker Build Production - Backend
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
- name: Setup env
run: |
echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
- run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
- name: Backend | Build `production` image
run: |
docker build --target base \
--tag "ocelotsocialnetwork/backend:latest-base" \
--tag "ocelotsocialnetwork/backend:${VERSION}-base" \
--tag "ocelotsocialnetwork/backend:${BUILD_VERSION}-base" \
--build-arg BBUILD_DATE=$BUILD_DATE \
--build-arg BBUILD_VERSION=$BUILD_VERSION \
--build-arg BBUILD_COMMIT=$BUILD_COMMIT \
backend/
docker build --target code \
--tag "ocelotsocialnetwork/backend:latest-code" \
--tag "ocelotsocialnetwork/backend:${VERSION}-code" \
--tag "ocelotsocialnetwork/backend:${BUILD_VERSION}-code" \
--build-arg BBUILD_DATE=$BUILD_DATE \
--build-arg BBUILD_VERSION=$BUILD_VERSION \
--build-arg BBUILD_COMMIT=$BUILD_COMMIT \
backend/
docker build --target production \
--tag "ocelotsocialnetwork/backend:latest" \
--tag "ocelotsocialnetwork/backend:${VERSION}" \
--tag "ocelotsocialnetwork/backend:${BUILD_VERSION}" \
--build-arg BBUILD_DATE=$BUILD_DATE \
--build-arg BBUILD_VERSION=$BUILD_VERSION \
--build-arg BBUILD_COMMIT=$BUILD_COMMIT \
backend/
- name: Backend | Save docker image
run: docker save "ocelotsocialnetwork/backend" > /tmp/backend.tar
- name: Upload Artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: docker-backend-production
path: /tmp/backend.tar
##############################################################################
# JOB: DOCKER BUILD PRODUCTION WEBAPP ########################################
##############################################################################
build_production_webapp:
name: Docker Build Production - WebApp
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
- name: Setup env
run: |
echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
- run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
- name: Webapp | Build `production` image
run: |
docker build --target base \
--tag "ocelotsocialnetwork/webapp:latest-base" \
--tag "ocelotsocialnetwork/webapp:${VERSION}-base" \
--tag "ocelotsocialnetwork/webapp:${BUILD_VERSION}-base" \
--build-arg BBUILD_DATE=$BUILD_DATE \
--build-arg BBUILD_VERSION=$BUILD_VERSION \
--build-arg BBUILD_COMMIT=$BUILD_COMMIT \
webapp/
docker build --target code \
--tag "ocelotsocialnetwork/webapp:latest-code" \
--tag "ocelotsocialnetwork/webapp:${VERSION}-code" \
--tag "ocelotsocialnetwork/webapp:${BUILD_VERSION}-code" \
--build-arg BBUILD_DATE=$BUILD_DATE \
--build-arg BBUILD_VERSION=$BUILD_VERSION \
--build-arg BBUILD_COMMIT=$BUILD_COMMIT \
webapp/
docker build --target production \
--tag "ocelotsocialnetwork/webapp:latest" \
--tag "ocelotsocialnetwork/webapp:${VERSION}" \
--tag "ocelotsocialnetwork/webapp:${BUILD_VERSION}" \
--build-arg BBUILD_DATE=$BUILD_DATE \
--build-arg BBUILD_VERSION=$BUILD_VERSION \
--build-arg BBUILD_COMMIT=$BUILD_COMMIT \
webapp/
- name: Webapp | Save docker image
run: docker save "ocelotsocialnetwork/webapp" > /tmp/webapp.tar
- name: Upload Artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: docker-webapp-production
path: /tmp/webapp.tar
##############################################################################
# JOB: DOCKER BUILD PRODUCTION MAINTENANCE ###################################
##############################################################################
build_production_maintenance:
name: Docker Build Production - Maintenance
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
- name: Setup env
run: |
echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
- run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
- name: Maintenance | Build `production` image
run: |
docker build --target base \
--tag "ocelotsocialnetwork/maintenance:latest-base" \
--tag "ocelotsocialnetwork/maintenance:${VERSION}-base" \
--tag "ocelotsocialnetwork/maintenance:${BUILD_VERSION}-base" \
--build-arg BBUILD_DATE=$BUILD_DATE \
--build-arg BBUILD_VERSION=$BUILD_VERSION \
--build-arg BBUILD_COMMIT=$BUILD_COMMIT \
-f webapp/Dockerfile.maintenance \
webapp/
docker build --target code \
--tag "ocelotsocialnetwork/maintenance:latest-code" \
--tag "ocelotsocialnetwork/maintenance:${VERSION}-code" \
--tag "ocelotsocialnetwork/maintenance:${BUILD_VERSION}-code" \
--build-arg BBUILD_DATE=$BUILD_DATE \
--build-arg BBUILD_VERSION=$BUILD_VERSION \
--build-arg BBUILD_COMMIT=$BUILD_COMMIT \
-f webapp/Dockerfile.maintenance \
webapp/
docker build --target production \
--tag "ocelotsocialnetwork/maintenance:latest" \
--tag "ocelotsocialnetwork/maintenance:${VERSION}" \
--tag "ocelotsocialnetwork/maintenance:${BUILD_VERSION}" \
--build-arg BBUILD_DATE=$BUILD_DATE \
--build-arg BBUILD_VERSION=$BUILD_VERSION \
--build-arg BBUILD_COMMIT=$BUILD_COMMIT \
-f webapp/Dockerfile.maintenance \
webapp/
- name: Maintenance | Save docker image
run: docker save "ocelotsocialnetwork/maintenance" > /tmp/maintenance.tar
- name: Upload Artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: docker-maintenance-production
path: /tmp/maintenance.tar
##############################################################################
# JOB: UPLOAD TO DOCKERHUB ###################################################
##############################################################################
upload_to_dockerhub:
name: Upload to Dockerhub
runs-on: ubuntu-latest
needs: [build_production_neo4j,build_production_backend,build_production_webapp,build_production_maintenance]
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
steps:
- name: Download Docker Image (Neo4J)
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: docker-neo4j-community
path: /tmp
- run: docker load < /tmp/neo4j.tar
- name: Download Docker Image (Backend)
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: docker-backend-production
path: /tmp
- run: docker load < /tmp/backend.tar
- name: Download Docker Image (WebApp)
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: docker-webapp-production
path: /tmp
- run: docker load < /tmp/webapp.tar
- name: Download Docker Image (Maintenance)
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: docker-maintenance-production
path: /tmp
- run: docker load < /tmp/maintenance.tar
- name: login to dockerhub
run: echo "${DOCKERHUB_TOKEN}" | docker login -u "${DOCKERHUB_USERNAME}" --password-stdin
- name: Push images
run: |
docker push --all-tags ocelotsocialnetwork/neo4j-community
docker push --all-tags ocelotsocialnetwork/backend
docker push --all-tags ocelotsocialnetwork/webapp
docker push --all-tags ocelotsocialnetwork/maintenance
##############################################################################
# JOB: GITHUB TAG LATEST VERSION #############################################
##############################################################################
github_tag:
name: Tag latest version on Github
runs-on: ubuntu-latest
needs: [upload_to_dockerhub]
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
@ -293,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@cc7d8686e214c240128fe2680a5c1c5486ae19c4 # v3.0.0
# uses: peter-evans/repository-dispatch@7d980a9b9f8ecf8955ea90507b3ed89122f53215 # v3.0.0
# with:
# token: ${{ github.token }}
# event-type: trigger-ocelot-build-success
@ -301,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@cc7d8686e214c240128fe2680a5c1c5486ae19c4 # v3.0.0
uses: peter-evans/repository-dispatch@7d980a9b9f8ecf8955ea90507b3ed89122f53215 # 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
@ -309,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@cc7d8686e214c240128fe2680a5c1c5486ae19c4 # v3.0.0
uses: peter-evans/repository-dispatch@7d980a9b9f8ecf8955ea90507b3ed89122f53215 # 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

View File

@ -37,7 +37,7 @@ jobs:
- name: Cache docker images
id: cache-neo4j
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.0.2
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2
with:
path: /tmp/neo4j.tar
key: ${{ github.run_id }}-backend-neo4j-cache
@ -58,7 +58,7 @@ jobs:
- name: Cache docker images
id: cache-backend
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.0.2
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2
with:
path: /tmp/backend.tar
key: ${{ github.run_id }}-backend-cache
@ -87,14 +87,14 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
- name: Restore Neo4J cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.0.2
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2
with:
path: /tmp/neo4j.tar
key: ${{ github.run_id }}-backend-neo4j-cache
fail-on-cache-miss: true
- name: Restore Backend cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.0.2
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2
with:
path: /tmp/backend.tar
key: ${{ github.run_id }}-backend-cache
@ -112,7 +112,8 @@ jobs:
cp backend/.env.template backend/.env
- name: backend | docker compose
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps neo4j backend
# 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
- name: backend | Initialize Database
run: docker compose exec -T backend yarn db:migrate init

View File

@ -37,7 +37,7 @@ jobs:
- name: Cache docker images
id: cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.0.2
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2
with:
path: |
/opt/cucumber-json-formatter
@ -59,7 +59,7 @@ jobs:
job: [1, 2, 3, 4, 5, 6, 7, 8]
steps:
- name: Restore cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.0.2
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2
id: cache
with:
path: |
@ -77,7 +77,7 @@ jobs:
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
docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp neo4j backend --build
sleep 90s
- name: Full stack tests | run tests
@ -93,7 +93,7 @@ jobs:
- name: Full stack tests | if tests failed, upload report
id: e2e-report
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
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

View File

@ -50,7 +50,7 @@ jobs:
docker save "ocelotsocialnetwork/webapp:test" > /tmp/webapp.tar
- name: Cache docker image
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.0.2
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2
with:
path: /tmp/webapp.tar
key: ${{ github.run_id }}-webapp-cache
@ -79,7 +79,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
- name: Restore webapp cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.0.2
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2
with:
path: /tmp/webapp.tar
key: ${{ github.run_id }}-webapp-cache
@ -94,7 +94,8 @@ jobs:
cp backend/.env.template backend/.env
- name: backend | docker compose
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp
# 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 webapp --build
- name: webapp | Unit tests incl. coverage check
run: docker compose exec -T webapp yarn test

View File

@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.actor != 'dependabot[bot]' }}
steps:
- uses: amannn/action-semantic-pull-request@80c0371c57c5142ed6c844270bba1864bac8a4c6 # v5.5.3
- uses: amannn/action-semantic-pull-request@04501d43b574e4c1d23c629ffe4dcec27acfdeff # v5.5.3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:

1
.gitignore vendored
View File

@ -15,6 +15,7 @@ node_modules/
cypress/videos
cypress/screenshots/
cypress.env.json
deployment/configurations/
.vuepress/.cache/
.vuepress/.temp/

1
.tool-versions Normal file
View File

@ -0,0 +1 @@
nodejs 20.12.1

View File

@ -4,8 +4,306 @@ 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.2.1](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.2.0...3.2.1)
- 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)
- feat(other): major improvement of deployment [`#7925`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7925)
- Bump the metascraper group across 1 directory with 12 updates [`#8187`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8187)
- Bump peter-evans/repository-dispatch from 39d2331fbbe4be56c4434ca745a23633155f9cdf to b0b38f73c8333be75d585a92b2c630a10d2a78f5 [`#8206`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8206)
- Bump gaurav-nelson/github-action-markdown-link-check from 7d83e59a57f3c201c76eed3d33dff64ec4452d27 to 1b916f2cf6c36510a6059943104e3c42ce6c16bc [`#8178`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8178)
- Bump xregexp from 4.3.0 to 5.1.2 in /webapp [`#8216`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8216)
- Bump xregexp from 4.3.0 to 5.1.2 in /backend [`#8225`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8225)
- Bump eslint-import-resolver-typescript from 3.7.0 to 3.8.3 in /backend [`#8222`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8222)
- Bump nodemailer from 6.9.16 to 6.10.0 in /backend [`#8173`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8173)
- Bump @faker-js/faker from 9.3.0 to 9.5.0 in /backend [`#8204`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8204)
- Bump the babel group across 1 directory with 2 updates [`#8205`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8205)
- Bump the babel group across 1 directory with 2 updates [`#8197`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8197)
- Bump @types/node from 22.10.2 to 22.13.5 in /backend [`#8223`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8223)
- Bump eslint-plugin-jest from 28.10.0 to 28.11.0 in /backend [`#8160`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8160)
- Bump @faker-js/faker from 9.3.0 to 9.5.0 in /webapp [`#8211`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8211)
- Bump @faker-js/faker from 9.3.0 to 9.5.0 [`#8196`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8196)
- Bump eslint-config-prettier from 9.1.0 to 10.0.1 in /backend [`#8165`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8165)
- Bump eslint-plugin-prettier from 5.2.1 to 5.2.3 in /backend [`#8170`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8170)
- Bump prettier from 3.4.2 to 3.5.2 in /backend [`#8228`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8228)
- Bump eslint-config-prettier from 6.15.0 to 10.0.1 in /webapp [`#8152`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8152)
- Bump eslint-plugin-prettier from 5.2.1 to 5.2.3 in /webapp [`#8179`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8179)
- Bump prettier from 3.4.2 to 3.5.2 in /webapp [`#8227`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8227)
- Bump sass from 1.83.0 to 1.85.0 in /webapp [`#8207`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8207)
- Bump the cypress group across 1 directory with 3 updates [`#8219`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8219)
- Bump actions/setup-node from 4.1.0 to 4.2.0 [`#8188`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8188)
- Bump actions/upload-artifact from 4.4.3 to 4.6.1 [`#8220`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8220)
- Bump actions/cache from 4.1.2 to 4.2.1 [`#8221`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8221)
- Bump typescript from 5.7.2 to 5.7.3 in /backend [`#8143`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8143)
- Bump the cypress group across 1 directory with 5 updates [`#8175`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8175)
- fix(frontend): hide invite button on mobile if disabled [`#8128`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8128)
- Bump eslint-plugin-promise from 7.1.0 to 7.2.1 in /webapp [`#8096`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8096)
- Bump eslint-plugin-vue from 9.31.0 to 9.32.0 in /webapp [`#8104`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8104)
- Bump sass from 1.75.0 to 1.83.0 in /webapp [`#8121`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8121)
- Bump prettier from 3.3.3 to 3.4.2 in /webapp [`#8100`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8100)
- Bump @faker-js/faker from 9.2.0 to 9.3.0 in /webapp [`#8101`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8101)
- Bump express from 4.21.1 to 4.21.2 in /webapp [`#8105`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8105)
- Bump dotenv from 16.4.5 to 16.4.7 [`#8119`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8119)
- Bump @faker-js/faker from 9.2.0 to 9.3.0 [`#8120`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8120)
- Bump the cypress group across 1 directory with 2 updates [`#8134`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8134)
- Bump sanitize-html from 2.13.1 to 2.14.0 in /backend [`#8132`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8132)
- Bump eslint-import-resolver-typescript from 3.6.3 to 3.7.0 in /backend [`#8113`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8113)
- Bump eslint-plugin-jest from 28.9.0 to 28.10.0 in /backend [`#8133`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8133)
- Bump @babel/cli from 7.25.9 to 7.26.4 in /backend in the babel group [`#8108`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8108)
- Bump express from 4.21.1 to 4.21.2 in /backend [`#8109`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8109)
- Bump linkify-html from 4.1.4 to 4.2.0 in /backend [`#8116`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8116)
- Bump @faker-js/faker from 9.2.0 to 9.3.0 in /backend [`#8115`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8115)
- Bump graphql-redis-subscriptions from 2.2.1 to 2.7.0 in /backend [`#8122`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8122)
- Bump @types/node from 22.9.3 to 22.10.2 in /backend [`#8124`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8124)
- Bump nodemon from 3.1.7 to 3.1.9 in /backend [`#8123`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8123)
- Bump linkifyjs from 4.1.4 to 4.2.0 in /backend [`#8112`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8112)
- Bump prettier from 3.3.3 to 3.4.2 in /backend [`#8111`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8111)
- Bump dotenv from 16.4.5 to 16.4.7 in /backend [`#8110`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8110)
- Bump the cypress group with 2 updates [`#8092`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8092)
- Bump @types/node from 22.9.0 to 22.9.3 in /backend [`#8093`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8093)
- Bump typescript from 5.6.3 to 5.7.2 in /backend [`#8086`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8086)
- Bump linkify-html from 4.1.3 to 4.1.4 in /backend [`#8085`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8085)
- Bump linkifyjs from 4.1.3 to 4.1.4 in /backend [`#8088`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8088)
- Bump eslint-plugin-vue from 9.30.0 to 9.31.0 in /webapp [`#8081`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8081)
- Bump @badeball/cypress-cucumber-preprocessor from 21.0.2 to 21.0.3 in the cypress group [`#8083`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8083)
- Bump amannn/action-semantic-pull-request from 80c0371c57c5142ed6c844270bba1864bac8a4c6 to 40166f00814508ec3201fc8595b393d451c8cd80 [`#8078`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8078)
- Bump peter-evans/repository-dispatch from 28a02cc85a65462275a97039562642d3731c318c to 39d2331fbbe4be56c4434ca745a23633155f9cdf [`#8077`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8077)
- Bump @faker-js/faker from 9.1.0 to 9.2.0 [`#8069`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8069)
- Bump peter-evans/repository-dispatch from af19ba8f0cf25cd177db9cf116a43798bc7446c7 to 28a02cc85a65462275a97039562642d3731c318c [`#8067`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8067)
- Bump cypress from 13.15.1 to 13.15.2 in the cypress group [`#8068`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8068)
- Bump @faker-js/faker from 9.1.0 to 9.2.0 in /webapp [`#8074`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8074)
- Bump eslint-plugin-jest from 28.8.3 to 28.9.0 in /backend [`#8073`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8073)
- Bump aws-sdk from 2.1691.0 to 2.1692.0 in /backend [`#8072`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8072)
- Bump @faker-js/faker from 9.1.0 to 9.2.0 in /backend [`#8071`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8071)
- Bump @types/node from 22.8.7 to 22.9.0 in /backend [`#8070`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8070)
- Bump @types/node from 22.8.4 to 22.8.7 in /backend [`#8066`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8066)
- Bump peter-evans/repository-dispatch from cc7d8686e214c240128fe2680a5c1c5486ae19c4 to af19ba8f0cf25cd177db9cf116a43798bc7446c7 [`#8061`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8061)
- Bump eslint-config-standard from 14.1.1 to 15.0.1 in /webapp [`#8063`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8063)
- Bump eslint-plugin-promise from 4.3.1 to 7.1.0 in /webapp [`#8056`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8056)
- Bump eslint-plugin-import from 2.20.2 to 2.31.0 in /webapp [`#8055`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8055)
- Bump eslint-plugin-prettier from 5.1.3 to 5.2.1 in /webapp [`#8051`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8051)
- Bump eslint-plugin-vue from 9.26.0 to 9.30.0 in /webapp [`#8057`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8057)
- fix(frontend): adapt to different logo sizes, including fixes for Safari [`#8044`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8044)
- refactor(other): dependabot: further package update regroupings [`#8045`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8045)
- Bump @types/node from 22.8.1 to 22.8.4 in /backend [`#8048`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8048)
- Bump the cypress group across 1 directory with 4 updates [`#8019`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8019)
- refactor(maintenance): dependabot: disable frontend dependency checks [`#8041`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8041)
- build(backend): update minor backend dependencies [`#8046`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8046)
- refactor(other): adapt cypress config to cypress-cucumber-preprocessor update [`#8043`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8043)
- Bump crazy-max/ghaction-github-pages from 08f571653184e9ff3d598bdda53ffd4ed00ed562 to fbf0a4fa4e00f45accd6cf3232368436ec06ed59 [`#8025`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8025)
- fix(other): revert a change, which accedentally was merged [`#8039`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8039)
- Bump nodemailer from 6.9.13 to 6.9.16 in /backend [`#8026`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8026)
- Bump @faker-js/faker from 9.0.3 to 9.1.0 [`#8029`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8029)
- Bump @faker-js/faker from 9.0.3 to 9.1.0 in /webapp [`#8036`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8036)
- Bump @faker-js/faker from 9.0.3 to 9.1.0 in /backend [`#8031`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8031)
- refactor(other): dependabot: degroups webapp package updates for babel, graphql, neo4j, and nuxt [`#8024`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8024)
- Bump slug from 9.1.0 to 10.0.0 [`#7962`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7962)
- Bump auto-changelog from 2.4.0 to 2.5.0 [`#7842`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7842)
- Bump helmet from 7.1.0 to 8.0.0 in /backend [`#7916`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7916)
- refactor(webapp): migrate sass devisions [`#7988`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7988)
- Bump the vitest group across 1 directory with 2 updates [`#7963`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7963)
- Bump actions/cache from 8469c94c6a180dfb41a1bd7e1b46ac557ea124f1 to 6849a6489940f00c2f30c0fb92c6274307ccb58a [`#7990`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7990)
- Bump pinia-plugin-persistedstate from 4.1.1 to 4.1.2 in /frontend in the pinia group [`#8010`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8010)
- Bump the babel group across 1 directory with 3 updates [`#8017`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8017)
- Bump sass from 1.77.6 to 1.80.4 in /frontend [`#8023`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8023)
- Bump @types/node from 22.7.7 to 22.8.1 in /backend [`#8021`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8021)
- Bump the babel group across 1 directory with 6 updates [`#8022`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8022)
- Bump @types/jest from 29.5.13 to 29.5.14 in /backend [`#8002`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8002)
- Bump actions/setup-node from aca7b64a59c0063db8564e0ffdadd3887f1cbae5 to 39370e3970a6d050c480ffad4ff0ed4d3fdee5af [`#8020`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8020)
- Bump actions/checkout from 163217dfcd28294438ea1c1c149cfaf66eec283e to 11bd71901bbe5b1630ceea73d27597364c9af683 [`#8018`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8018)
- Bump peter-evans/repository-dispatch from e614736e88253e8888967eafadb9b75b237d52ba to cc7d8686e214c240128fe2680a5c1c5486ae19c4 [`#8016`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8016)
- Bump the vuepress group across 1 directory with 4 updates [`#7999`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7999)
- fix(webapp): change theme color to be brandable [`#8004`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8004)
- fix(webapp): notification page has unbranded button [`#8001`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8001)
- refactor(webapp): degroup sass packages in dependabot [`#7985`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7985)
- refactor(webapp): degroup vue packages in dependabot [`#7984`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7984)
- refactor(other): change set node version in readme files [`#7983`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7983)
- Bump actions/cache from 81382a721fc89d96eca335d0c3ba33144b2baa9d to 8469c94c6a180dfb41a1bd7e1b46ac557ea124f1 [`#7927`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7927)
- Bump @faker-js/faker from 9.0.0 to 9.0.3 [`#7885`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7885)
- Bump sirv from 2.0.4 to 3.0.0 in /frontend [`#7942`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7942)
- Bump the babel group across 1 directory with 3 updates [`#7944`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7944)
- build(webapp): bump the babel group across 1 directory with 2 updates [`#7950`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7950)
- Bump @types/node from 22.5.5 to 22.7.7 in /backend [`#7975`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7975)
- Bump the babel group across 1 directory with 6 updates [`#7938`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7938)
- Bump eslint-plugin-import from 2.30.0 to 2.31.0 in /backend [`#7915`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7915)
- Bump sanitize-html from 2.13.0 to 2.13.1 in /backend [`#7917`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7917)
- Bump @faker-js/faker from 9.0.0 to 9.0.3 in /backend [`#7882`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7882)
- Bump nodemon from 3.1.0 to 3.1.7 in /backend [`#7872`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7872)
- Bump actions/checkout from 6d193bf28034eafb982f37bd894289fe649468fc to 163217dfcd28294438ea1c1c149cfaf66eec283e [`#7972`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7972)
- Bump express from 4.21.0 to 4.21.1 in /backend [`#7932`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7932)
- Bump actions/setup-node from 97ca147735c170fb35096b39ef17a0fc5d9270ac to aca7b64a59c0063db8564e0ffdadd3887f1cbae5 [`#7889`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7889)
- Bump typescript from 5.6.2 to 5.6.3 in /backend [`#7940`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7940)
- fix(backend): fix geodata in mapbox-related resolver test [`#7976`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7976)
- Bump the pinia group across 1 directory with 2 updates [`#7905`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7905)
- Bump actions/upload-artifact from 4.4.0 to 4.4.3 [`#7930`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7930)
- Bump express from 4.19.2 to 4.21.1 in /frontend [`#7933`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7933)
- Bump express from 4.21.0 to 4.21.1 in /webapp [`#7949`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7949)
- Bump the stylelint group across 1 directory with 2 updates [`#7937`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7937)
- Bump @vue/compiler-sfc from 3.5.3 to 3.5.12 in /frontend [`#7941`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7941)
- Bump peter-evans/repository-dispatch from c2fad29759e65af76987543a0aabefb8906d379a to e614736e88253e8888967eafadb9b75b237d52ba [`#7973`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7973)
- Bump the storybook group across 1 directory with 8 updates [`#7970`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7970)
- Bump @faker-js/faker from 9.0.0 to 9.0.3 in /webapp [`#7878`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7878)
- Bump eslint from 8.57.0 to 8.57.1 in /backend [`#7870`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7870)
- chore(other): adjust docs for deployment, neo4j cypher commands, and have new scripts [`#7848`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7848)
- refactor(other): remove package wait-on [`#7819`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7819)
- Bump crazy-max/ghaction-github-pages from cda5497acf90563d34489ed832a67c2c50353a16 to 08f571653184e9ff3d598bdda53ffd4ed00ed562 [`#7833`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7833)
- Bump actions/setup-node from 1c7b2db92075f828bee89d7e19d33a911d15e7b3 to 97ca147735c170fb35096b39ef17a0fc5d9270ac [`#7834`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7834)
- refactor(frontend): remove chromatic [`#7818`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7818)
- Bump express from 4.19.2 to 4.21.0 in /webapp [`#7844`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7844)
- Bump express from 4.19.2 to 4.21.0 in /backend [`#7840`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7840)
- Bump @types/jest from 29.5.12 to 29.5.13 in /backend [`#7837`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7837)
- Bump @types/node from 22.5.4 to 22.5.5 in /backend [`#7838`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7838)
- Bump typescript from 5.4.5 to 5.6.2 in /backend [`#7839`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7839)
- build(other): bump cheerio from 1.0.0-rc.12 to 1.0.0 in /backend [`#7709`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7709)
- Bump @faker-js/faker from 8.4.1 to 9.0.0 [`#7791`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7791)
- build(other): bump @faker-js/faker from 8.4.1 to 9.0.0 [`#7817`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7817)
- Bump vike from 0.4.177 to 0.4.195 in /frontend [`#7820`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7820)
- Bump apollo-client from 2.6.8 to 2.6.10 in /webapp in the apollo group [`#7692`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7692)
- Bump date-fns from 3.3.1 to 3.6.0 [`#7139`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7139)
- Bump @types/node from 20.12.7 to 22.5.4 in /backend [`#7812`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7812)
- Bump slug from 9.0.0 to 9.1.0 [`#7457`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7457)
- Bump the vitest group across 1 directory with 2 updates [`#7683`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7683)
- Bump the stylelint group across 1 directory with 2 updates [`#7773`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7773)
- Bump validator from 13.11.0 to 13.12.0 in /webapp [`#7393`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7393)
- Bump peter-evans/repository-dispatch from 733a1daa12a9e7f9b219279836ac0190f48cf46d to c2fad29759e65af76987543a0aabefb8906d379a [`#7805`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7805)
- Bump @vue/compiler-sfc from 3.4.30 to 3.5.3 in /frontend [`#7795`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7795)
- Bump actions/checkout from 9a9194f87191a7e9055e3e9b95b8cfb13023bb08 to 6d193bf28034eafb982f37bd894289fe649468fc [`#7804`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7804)
- Bump actions/cache from 40c3b67b2955d93d83b27ed164edd0756bc24049 to 81382a721fc89d96eca335d0c3ba33144b2baa9d [`#7781`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7781)
- Bump actions/setup-node from 26961cf329f22f6837d5f54c3efd76b480300ace to 1c7b2db92075f828bee89d7e19d33a911d15e7b3 [`#7803`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7803)
- Bump vue-i18n from 9.13.1 to 9.14.0 in /frontend [`#7728`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7728)
- Bump eslint-import-resolver-typescript from 3.6.1 to 3.6.3 in /backend [`#7788`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7788)
- Bump ts-jest from 29.1.2 to 29.2.5 in /backend in the typescript group across 1 directory [`#7758`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7758)
- Bump the babel group across 1 directory with 6 updates [`#7787`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7787)
- Bump eslint-plugin-jest from 28.2.0 to 28.8.3 in /backend [`#7814`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7814)
- Bump eslint-plugin-import from 2.29.1 to 2.30.0 in /backend [`#7815`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7815)
- Bump slug from 9.0.0 to 9.1.0 in /backend [`#7455`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7455)
- Bump peter-evans/repository-dispatch from 1ebfb41781aa0fae446773941d0b3025198fc1a9 to 733a1daa12a9e7f9b219279836ac0190f48cf46d [`#7782`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7782)
- Bump actions/upload-artifact from 4.3.5 to 4.4.0 [`#7780`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7780)
- Bump prettier from 3.2.5 to 3.3.3 in /webapp [`#7629`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7629)
- Bump prettier from 3.3.2 to 3.3.3 in /frontend [`#7641`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7641)
- fix(workflow): fix typo in publishing workflow [`#7770`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7770)
- fix(workflow): reset pkgdeps/git-tag-action to v3.0.0 in publish.yml [`#7769`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7769)
- chore(other): remove obsolete version element from all docker yaml files [`#7564`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7564)
- refactor(workflow): pin all github actions by commit hash [`#7701`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7701)
- fix(webapp): fix maintenance dockerfiles image name [`#7768`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7768)
- refactor(workflow): replace docker-compose by docker compose in workflow files [`#7762`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7762)
- fix(backend): fix backend tests by changing mapbox location coordinates [`#7766`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7766)
- Bump @vue/compiler-sfc from 3.4.29 to 3.4.30 in /frontend [`#7563`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7563)
- Bump prettier from 3.2.5 to 3.3.2 in /frontend [`#7530`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7530)
- chore(frontend): update frontend packages 2024 06 22 [`#7562`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7562)
- Bump the typescript group across 1 directory with 4 updates [`#7487`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7487)
- Bump chromatic from 11.3.2 to 11.5.0 in /frontend [`#7491`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7491)
- Bump vike from 0.4.171 to 0.4.172 in /frontend [`#7489`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7489)
- Bump the vite group in /frontend with 2 updates [`#7486`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7486)
- Bump the vuepress group across 1 directory with 3 updates [`#7485`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7485)
- Bump stylelint from 16.5.0 to 16.6.1 in /frontend in the stylelint group across 1 directory [`#7484`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7484)
- refactor(other): group dependabot updates [`#7431`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7431)
- chore(frontend): update packages 2024 05 12 [`#7401`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7401)
- feat(frontend): implement ocelot color tokens in frontend with themes [`#7144`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7144)
- Bump eslint-plugin-vue from 9.25.0 to 9.26.0 in /webapp [`#7399`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7399)
- refactor(other): bump @faker-js/faker from 5.1.0 to 8.4.1 in /webapp and adapt repo-wide code to deprecation warnings [`#6999`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6999)
- Bump @badeball/cypress-cucumber-preprocessor from 20.0.3 to 20.0.4 [`#7333`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7333)
- Bump cypress from 13.7.3 to 13.8.1 [`#7334`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7334)
- Bump @cucumber/cucumber from 10.4.0 to 10.6.0 [`#7335`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7335)
- Bump cropperjs from 1.6.1 to 1.6.2 in /webapp [`#7307`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7307)
- feat(frontend): add icon components as vuetify assets with icons of actual webapp [`#7129`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7129)
- build(other): bump linkifyjs from 2.1.8 to 4.1.3 in /backend [`#7220`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7220)
- Bump jsonwebtoken from 9.0.0 to 9.0.2 in /webapp [`#7219`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7219)
- Bump uuid from 8.3.2 to 9.0.1 in /backend [`#7206`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7206)
- Bump @faker-js/faker from 7.6.0 to 8.4.1 in /backend [`#7181`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7181)
- Bump slug from 6.0.0 to 9.0.0 in /backend [`#7193`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7193)
- Bump sass from 1.30.0 to 1.75.0 in /webapp [`#7269`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7269)
- Bump express from 4.18.3 to 4.19.2 in /webapp [`#7185`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7185)
- Bump nodemon from 2.0.2 to 3.1.0 in /backend [`#7171`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7171)
- build(other): bump neo4j-driver from 4.4.7 to latest v4 patch available [`#7197`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7197)
- Bump typescript from 4.9.5 to 5.4.5 in /backend [`#7273`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7273)
- Bump validator from 13.0.0 to 13.11.0 in /webapp [`#7208`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7208)
- Bump jest from 29.5.0 to 29.7.0 in /webapp [`#7195`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7195)
- Bump eslint-plugin-vue from 9.24.0 to 9.25.0 in /webapp [`#7282`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7282)
- Bump @typescript-eslint/parser from 5.60.0 to 5.62.0 in /backend [`#7188`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7188)
- Bump eslint-plugin-security from 2.1.1 to 3.0.0 in /backend [`#7275`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7275)
- Bump eslint-plugin-jest from 27.2.2 to 28.2.0 in /backend [`#7204`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7204)
- Bump vue-advanced-chat from 2.0.10 to 2.0.11 in /webapp [`#7175`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7175)
- chore(other): update docker image versions [`#7238`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7238)
- Bump babel-jest from 29.5.0 to 29.7.0 in /webapp [`#7169`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7169)
- Bump @babel/core from 7.23.7 to 7.24.4 in /webapp [`#7226`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7226)
- Bump eslint from 8.43.0 to 8.57.0 in /backend [`#7174`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7174)
- Bump @storybook/addon-a11y from 8.0.5 to 8.0.8 in /webapp [`#7268`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7268)
- chore(frontend): add .vscode to gitignore in /frontend [`#7278`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7278)
- Bump vue-i18n from 9.12.0 to 9.12.1 in /frontend [`#7280`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7280)
- Bump eslint-plugin-vue from 9.24.1 to 9.25.0 in /frontend [`#7279`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7279)
- chore(frontend): update frontend packages 2024 04 14 [`#7277`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7277)
- Bump cypress from 13.7.2 to 13.7.3 [`#7241`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7241)
- Bump aws-sdk from 2.1595.0 to 2.1599.0 in /backend [`#7274`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7274)
- Bump @types/node from 20.12.5 to 20.12.7 in /backend [`#7276`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7276)
- Bump aws-sdk from 2.1594.0 to 2.1595.0 in /backend [`#7236`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7236)
- chore(backend): update packages [`#7235`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7235)
- Bump @cucumber/cucumber from 10.3.1 to 10.4.0 [`#7163`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7163)
- Bump @badeball/cypress-cucumber-preprocessor from 20.0.2 to 20.0.3 [`#7161`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7161)
- Bump minimatch from 9.0.3 to 9.0.4 in /backend [`#7214`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7214)
- Bump sanitize-html from 2.11.0 to 2.13.0 in /backend [`#7218`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7218)
- Bump happy-dom from 14.7.0 to 14.7.1 in /frontend [`#7234`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7234)
- chore(frontend): update boilerplate 07 04 [`#7233`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7233)
- Bump helmet from 7.0.0 to 7.1.0 in /backend [`#7160`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7160)
- build(other): bump vike from 0.4.163 to 0.4.168 in /frontend [`#7153`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7153)
- refactor(other): fix dependabot pr limit problem [`#7159`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7159)
- chore(other): set dependabot package update pr limit to none [`#7158`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7158)
- chore(frontend): update frontend boilerplate 06 04 [`#7157`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7157)
- Bump eslint-plugin-vue from 9.19.2 to 9.24.0 in /webapp [`#7147`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7147)
- Bump @babel/preset-env from 7.24.0 to 7.24.4 [`#7150`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7150)
- Bump cypress from 13.7.0 to 13.7.2 [`#7151`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7151)
- Bump @storybook/addon-a11y from 6.3.6 to 8.0.5 in /webapp [`#7146`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7146)
- Bump actions/cache from 4.0.1 to 4.0.2 [`#7133`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7133)
- Bump multiple-cucumber-html-reporter from 3.6.1 to 3.6.2 [`#7138`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7138)
- Bump dotenv from 16.4.4 to 16.4.5 [`#7137`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7137)
- Bump express from 4.17.1 to 4.18.3 in /webapp [`#7115`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7115)
- Bump slug from 8.2.3 to 9.0.0 [`#7126`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7126)
- Bump vuepress-theme-hope from 2.0.0-rc.29 to 2.0.0-rc.31 [`#7124`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7124)
- Bump migrate from 2.0.1 to 2.1.0 in /backend [`#7118`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7118)
- Bump @badeball/cypress-cucumber-preprocessor from 20.0.1 to 20.0.2 [`#7122`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7122)
- Bump cypress from 13.6.6 to 13.7.0 [`#7123`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7123)
- fix(webapp): remove reported unused locale keys [`#7105`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7105)
- build(other): bump minimatch from 3.0.4 to 9.0.3 in /backend [`#6511`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6511)
- build(other): bump prettier from 2.7.1 to 3.2.5 in /webapp [`#6997`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6997)
- build(other): bump prettier from 2.8.8 to 3.2.5 in /backend [`#7000`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7000)
- Bump eslint-plugin-security from 1.7.1 to 2.1.1 in /backend [`#7041`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7041)
- Bump vuepress-theme-hope from 2.0.0-rc.23 to 2.0.0-rc.29 [`#7047`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7047)
- Bump cypress from 13.6.4 to 13.6.6 [`#7045`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7045)
- Bump @babel/preset-env from 7.23.9 to 7.24.0 [`#7048`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7048)
- Bump dorny/paths-filter from 3.0.1 to 3.0.2 [`#7049`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7049)
- feat(webapp): normalize locale files [`#7038`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7038)
- feat(webapp): update locales event placeholders [`#6793`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6793)
- Bump date-fns from 2.30.0 to 3.3.1 [`#7037`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7037)
- Bump @babel/register from 7.22.15 to 7.23.7 [`#6931`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6931)
- Bump @babel/core from 7.23.7 to 7.24.0 [`#7034`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7034)
- build(other): bump node-fetch from 2.6.1 to 2.7.0 in /backend [`#6960`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6960)
- Bump peter-evans/repository-dispatch from 2 to 3 [`#6966`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6966)
- Bump @faker-js/faker from 8.4.0 to 8.4.1 [`#7026`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7026)
- Bump babel-jest from 25.2.6 to 29.7.0 in /backend [`#6961`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6961)
- Bump actions/cache from 4.0.0 to 4.0.1 [`#7028`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7028)
- chore(frontend): update frontend boilerplate 26 02 [`#7027`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7027)
- fix(docker): docker rework [`#92`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/92)
- chore(docu): update vuepress [`#91`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/91)
- Bump dorny/paths-filter from 3.0.0 to 3.0.1 [`#82`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/82)
- chore(frontend): update packages 04.02 [`#90`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/90)
- chore(frontend): update frontend boilerplate [`#7017`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7017)
- chore(frontend): update packages 19.02 [`#86`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/86)
- refactor(other): set ocelot color for docs website [`#7014`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7014)
- refactor(other): adapt docs website navbar and footer to ocelot.social website [`#7016`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7016)
- refactor(other): replace deprecated testbadgelink in main readme file [`#7012`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7012)
- 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)
> 14 February 2024
- chore(other): release v3.2.0 redesign of newsfeed, fix event datetime utc problem etc. [`#7002`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7002)
- feat(webapp): implement config for `date-time` format [`#6985`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6985)
- build(other): bump eslint-plugin-n from 15.7.0 to 16.6.2 in /backend [`#6962`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6962)
- feat(other): frontend workflows & test suites [`#6987`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/6987)

View File

@ -186,6 +186,9 @@ $ cp .env.template .env
# in folder backend/
$ cp .env.template .env
# in folder frontend/
$ cp .env.template .env
```
For Development:

221
backend/.eslintrc.cjs Normal file
View File

@ -0,0 +1,221 @@
// 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',
],
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', // TODO // 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', // 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',
],
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',
},
},
],
}

View File

@ -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',
},
},
],
};

View File

@ -1,103 +1,42 @@
##################################################################################
# BASE (Is pushed to DockerHub for rebranding) ###################################
##################################################################################
FROM node:20.12.1-alpine3.19 AS base
# ENVs
## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
ENV DOCKER_WORKDIR="/app"
## We Cannot do `$(date -u +'%Y-%m-%dT%H:%M:%SZ')` here so we use unix timestamp=0
ARG BBUILD_DATE="1970-01-01T00:00:00.00Z"
ENV BUILD_DATE=$BBUILD_DATE
## We cannot do $(yarn run version)-${BUILD_NUMBER} here so we default to 0.0.0-0
ARG BBUILD_VERSION="0.0.0-0"
ENV BUILD_VERSION=$BBUILD_VERSION
## We cannot do `$(git rev-parse --short HEAD)` here so we default to 0000000
ARG BBUILD_COMMIT="0000000"
ENV BUILD_COMMIT=$BBUILD_COMMIT
## SET NODE_ENV
ENV NODE_ENV="production"
## App relevant Envs
ENV PORT="4000"
# Labels
LABEL org.label-schema.build-date="${BUILD_DATE}"
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"
LABEL org.label-schema.url="https://ocelot.social"
LABEL org.label-schema.vcs-url="https://github.com/Ocelot-Social-Community/Ocelot-Social/tree/master/backend"
LABEL org.label-schema.vcs-ref="${BUILD_COMMIT}"
LABEL org.label-schema.vendor="ocelot.social Community"
LABEL org.label-schema.version="${BUILD_VERSION}"
LABEL org.label-schema.schema-version="1.0"
LABEL maintainer="devops@ocelot.social"
# Install Additional Software
## install: git
RUN apk --no-cache add git python3 make g++
# Settings
## Expose Container Port
ENV NODE_ENV="production"
ENV PORT="4000"
EXPOSE ${PORT}
RUN apk --no-cache add git python3 make g++ bash
RUN mkdir -p /app
WORKDIR /app
CMD ["/bin/bash", "-c", "yarn run start"]
## Workdir
RUN mkdir -p ${DOCKER_WORKDIR}
WORKDIR ${DOCKER_WORKDIR}
##################################################################################
# DEVELOPMENT (Connected to the local environment, to reload on demand) ##########
##################################################################################
FROM base AS development
CMD ["/bin/sh", "-c", "yarn install && yarn run dev"]
# We don't need to copy or build anything since we gonna bind to the
# local filesystem which will need a rebuild anyway
# Run command
# (for development we need to execute yarn install since the
# node_modules are on another volume and need updating)
CMD /bin/sh -c "yarn install && yarn run dev"
##################################################################################
# CODE (Does contain all code files and is pushed to DockerHub for rebranding) ###
##################################################################################
FROM base AS code
# copy everything, but do not build.
FROM base AS build
COPY . .
ONBUILD COPY ./branding/constants/ src/config/tmp
ONBUILD RUN tools/replace-constants.sh
ONBUILD COPY ./branding/email/ src/middleware/helpers/email/
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 ./package.json yarn.lock /build
ONBUILD RUN cd /build && yarn install --production=true --frozen-lockfile --non-interactive
##################################################################################
# BUILD (Does contain all files and the compilate and is therefore bloated) ######
##################################################################################
FROM code AS build
# yarn install
RUN yarn install --production=false --frozen-lockfile --non-interactive
# yarn build
RUN /bin/sh -c "yarn run build"
##################################################################################
# TEST ###########################################################################
##################################################################################
FROM build AS test
# required for the migrations
# ONBUILD RUN cp -r ./src /src
CMD ["/bin/bash", "-c", "yarn run dev"]
# Run command
CMD /bin/sh -c "yarn run dev"
FROM build AS production_build
##################################################################################
# PRODUCTION (Does contain only "binary"- and static-files to reduce image size) #
##################################################################################
FROM base AS production
# Copy "binary"-files from build image
COPY --from=build ${DOCKER_WORKDIR}/build ./build
COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
# Copy static files
# TODO - externalize the uploads so we can copy the whole folder
COPY --from=build ${DOCKER_WORKDIR}/public/img/ ./public/img/
COPY --from=build ${DOCKER_WORKDIR}/public/providers.json ./public/providers.json
# Copy package.json for script definitions (lock file should not be needed)
COPY --from=build ${DOCKER_WORKDIR}/package.json ./package.json
# Run command
CMD /bin/sh -c "yarn run start"
COPY --from=production_build /build .

View File

@ -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,120 @@ 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 backend yarn db:migrate init
# for docker production:
docker exec backend 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 backend yarn db:migrate up
# for docker production
docker exec backend 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 <admin@example.org> 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
```
:::
### Seed Data
#### Seed Database
For a predefined set of test data you can seed the database with:
If you want your backend to return anything else than an empty response, you
need to seed your database:
```sh
# in backend with database running (In docker or local)
yarn db:seed
::: tabs
@tab:active Docker
In another terminal run:
```bash
# in main folder while docker-compose is running
$ docker exec backend yarn run db:seed
# for docker
docker exec backend yarn db:seed
```
To reset the database run:
### Reset Data
```bash
# in main folder while docker-compose is running
$ docker exec backend yarn run db:reset
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 backend yarn db:reset
# or deleting the migrations as well
docker exec backend 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 backend 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 backend yarn run test
```

26
backend/jest.config.cjs Normal file
View File

@ -0,0 +1,26 @@
/* eslint-disable import/no-commonjs */
const { pathsToModuleNameMapper } = require('ts-jest')
const requireJSON5 = require('require-json5')
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: ['<rootDir>/test/setup.ts'],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '<rootDir>/' }),
}

View File

@ -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: ['<rootDir>/test/setup.ts']
}

View File

@ -1,6 +1,6 @@
{
"name": "ocelot-social-backend",
"version": "3.2.0",
"version": "3.2.1",
"description": "GraphQL Backend for ocelot.social",
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
"author": "ocelot.social Community",
@ -8,26 +8,27 @@
"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",
"build": "tsc && tsc-alias && ./scripts/build.copy.files.sh",
"dev": "nodemon --exec ts-node --require tsconfig-paths/register 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",
"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: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"
},
"dependencies": {
"@babel/cli": "~7.25.9",
"@babel/core": "^7.26.0",
"@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.0",
"@babel/preset-env": "~7.26.9",
"@babel/register": "^7.23.7",
"@sentry/node": "^5.15.4",
"apollo-cache-inmemory": "~1.6.6",
@ -36,92 +37,102 @@
"apollo-link-http": "~1.5.17",
"apollo-server": "~2.14.2",
"apollo-server-express": "^2.14.2",
"aws-sdk": "^2.1599.0",
"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",
"body-parser": "^1.20.3",
"cheerio": "~1.0.0",
"cors": "~2.8.5",
"cross-env": "~7.0.3",
"dotenv": "~16.4.5",
"express": "^4.21.1",
"dotenv": "~16.4.7",
"express": "^5.1.0",
"graphql": "^14.6.0",
"graphql-middleware": "~4.0.2",
"graphql-middleware-sentry": "^3.2.1",
"graphql-redis-subscriptions": "^2.2.1",
"graphql-redis-subscriptions": "^2.7.0",
"graphql-shield": "~7.2.2",
"graphql-subscriptions": "^1.1.0",
"graphql-tag": "~2.10.3",
"helmet": "~8.0.0",
"graphql-upload": "^11.0.0",
"helmet": "~8.1.0",
"ioredis": "^4.16.1",
"jsonwebtoken": "~8.5.1",
"languagedetect": "^2.0.0",
"linkifyjs": "^4.1.3",
"linkify-html": "^4.1.3",
"linkify-html": "^4.2.0",
"linkifyjs": "^4.2.0",
"lodash": "~4.17.21",
"merge-graphql-schemas": "^1.7.8",
"metascraper": "^5.33.5",
"metascraper-author": "^5.33.5",
"metascraper-date": "^5.33.5",
"metascraper-description": "^5.33.5",
"metascraper-image": "^5.33.5",
"metascraper-lang": "^5.33.5",
"metascraper": "^5.46.11",
"metascraper-author": "^5.46.11",
"metascraper-date": "^5.46.11",
"metascraper-description": "^5.46.11",
"metascraper-image": "^5.46.11",
"metascraper-lang": "^5.46.11",
"metascraper-lang-detector": "^4.10.2",
"metascraper-logo": "^5.33.5",
"metascraper-publisher": "^5.33.5",
"metascraper-logo": "^5.46.11",
"metascraper-publisher": "^5.46.11",
"metascraper-soundcloud": "^5.34.4",
"metascraper-title": "^5.34.7",
"metascraper-url": "^5.34.2",
"metascraper-video": "^5.33.5",
"metascraper-youtube": "^5.33.5",
"metascraper-title": "^5.46.11",
"metascraper-url": "^5.46.11",
"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.9.16",
"nodemailer": "^6.10.0",
"nodemailer-html-to-text": "^3.2.0",
"request": "~2.88.2",
"sanitize-html": "~2.13.1",
"sanitize-html": "~2.15.0",
"slug": "~9.1.0",
"subscriptions-transport-ws": "^0.9.19",
"trunc-html": "~1.1.2",
"uuid": "~9.0.1",
"validator": "^13.11.0",
"xregexp": "^4.3.0"
"validator": "^13.15.0",
"xregexp": "^5.1.2"
},
"devDependencies": {
"@faker-js/faker": "9.1.0",
"@eslint-community/eslint-plugin-eslint-comments": "^4.4.1",
"@faker-js/faker": "9.6.0",
"@types/jest": "^29.5.14",
"@types/node": "^22.8.1",
"@types/node": "^22.14.0",
"@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": "^9.1.0",
"eslint-config-prettier": "^10.1.1",
"eslint-config-standard": "^17.1.0",
"eslint-import-resolver-typescript": "^3.6.3",
"eslint-import-resolver-typescript": "^4.3.1",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.8.3",
"eslint-plugin-n": "^16.6.2",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-security": "^3.0.0",
"eslint-plugin-jest": "^28.11.0",
"eslint-plugin-n": "^17.17.0",
"eslint-plugin-no-catch-all": "^1.1.0",
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-promise": "^7.2.1",
"eslint-plugin-security": "^3.0.1",
"jest": "^29.7.0",
"nodemon": "~3.1.7",
"prettier": "^3.2.5",
"nodemon": "~3.1.9",
"prettier": "^3.5.3",
"require-json5": "^1.3.0",
"rosie": "^2.1.1",
"ts-jest": "^29.2.5",
"ts-jest": "^29.3.1",
"ts-node": "^10.9.2",
"typescript": "^5.6.3"
"tsc-alias": "^1.8.14",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.8.3"
},
"resolutions": {
"**/**/fs-capacitor": "^6.2.0",
"**/graphql-upload": "^11.0.0",
"nan": "2.17.0"
"**/graphql-upload": "^11.0.0"
},
"engines": {
"node": ">=20.12.1"
}
}

View File

@ -1,5 +1,8 @@
#!/bin/sh
# public
cp -r public/ build/public/
# html files
mkdir -p build/src/middleware/helpers/email/templates/
cp -r src/middleware/helpers/email/templates/*.html build/src/middleware/helpers/email/templates/

View File

@ -1,11 +1,15 @@
import dotenv from 'dotenv'
/* eslint-disable n/no-process-env */
/* eslint-disable n/no-unpublished-require */
/* eslint-disable n/no-missing-require */
import { config } from 'dotenv'
import emails from './emails'
import metadata from './metadata'
// Load env file
if (require.resolve) {
try {
dotenv.config({ path: require.resolve('../../.env') })
config({ path: require.resolve('../../.env') })
} catch (error) {
// This error is thrown when the .env is not found
if (error.code !== 'MODULE_NOT_FOUND') {

50
backend/src/db/admin.ts Normal file
View File

@ -0,0 +1,50 @@
import { hashSync } from 'bcryptjs'
import { v4 as uuid } from 'uuid'
import { getDriver } from './neo4j'
const defaultAdmin = {
email: 'admin@example.org',
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()
})()

View File

@ -0,0 +1,36 @@
import { categories } from '@constants/categories'
import { getDriver } from './neo4j'
const createCategories = async () => {
const driver = getDriver()
const session = driver.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
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (error) {
console.log(`Error creating categories: ${error}`) // eslint-disable-line no-console
} finally {
session.close()
driver.close()
}
}
;(async function () {
await createCategories()
})()

View File

@ -1,2 +1,7 @@
/* eslint-disable import/no-commonjs */
// eslint-disable-next-line n/no-unpublished-require
const tsNode = require('ts-node')
// eslint-disable-next-line import/no-unassigned-import, import/no-extraneous-dependencies, n/no-unpublished-require
require('tsconfig-paths/register')
module.exports = tsNode.register

View File

@ -1,11 +1,13 @@
import { v4 as uuid } from 'uuid'
import slugify from 'slug'
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 CONFIG from '@config/index'
import generateInviteCode from '@schema/resolvers/helpers/generateInviteCode'
import { getDriver, getNeode } from './neo4j'
import CONFIG from '../config/index'
import generateInviteCode from '../schema/resolvers/helpers/generateInviteCode'
const neode = getNeode()
@ -15,18 +17,19 @@ const uniqueImageUrl = (imageUrl) => {
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()
@ -70,7 +73,6 @@ Factory.define('basicUser')
termsAndConditionsAgreedAt: '2019-08-01T10:47:19.212Z',
allowEmbedIframes: false,
showShoutsPublicly: false,
sendNotificationEmails: true,
locale: 'en',
})
.attr('slug', ['slug', 'name'], (slug, name) => {
@ -173,6 +175,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 +211,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
})

View File

@ -1,104 +1,45 @@
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
}
}
}
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.driver.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
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()
neode.driver.close()
}
}
@ -122,6 +63,7 @@ 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)
@ -157,6 +99,7 @@ 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)
@ -166,4 +109,4 @@ class Store {
}
}
module.exports = Store
export default Store

View File

@ -1,4 +1,4 @@
import { getDriver } from '../../db/neo4j'
import { getDriver } from '@db/neo4j'
export const description = ''

View File

@ -1,7 +1,10 @@
/* 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 '@schema/resolvers/helpers/normalizeEmail'
export const description = `
This migration merges duplicate :User and :EmailAddress nodes. It became

View File

@ -1,6 +1,9 @@
/* 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

View File

@ -1,4 +1,4 @@
import { getDriver } from '../../db/neo4j'
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.
@ -21,6 +21,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)
@ -38,6 +39,7 @@ export function down(next) {
try {
// Rollback your migration here.
next()
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (err) {
next(err)
} finally {

View File

@ -1,4 +1,4 @@
import { getDriver } from '../../db/neo4j'
import { getDriver } from '@db/neo4j'
export const description = `
This migration swaps the value stored in Location.lat with the value

View File

@ -1,4 +1,4 @@
import { getDriver } from '../../db/neo4j'
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 +9,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
@ -39,10 +42,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

View File

@ -1,4 +1,4 @@
import { getDriver } from '../../db/neo4j'
import { getDriver } from '@db/neo4j'
export const description = `
We introduced a new node label 'Image' and we need a primary key for it. Best
@ -48,6 +48,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)

View File

@ -1,10 +1,13 @@
import { getDriver } from '../../db/neo4j'
import { existsSync, createReadStream } from 'fs'
import path from 'path'
/* eslint-disable security/detect-non-literal-fs-filename */
import https from 'https'
import { existsSync, createReadStream } from 'node:fs'
import path from 'node:path'
import { S3 } from 'aws-sdk'
import mime from 'mime-types'
import s3Configs from '../../config'
import https from 'https'
import s3Configs 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
@ -95,6 +98,7 @@ export async function down(next) {
await transaction.run(``)
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)

View File

@ -1,5 +1,5 @@
/* 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

View File

@ -1,4 +1,4 @@
import { getDriver } from '../../db/neo4j'
import { getDriver } from '@db/neo4j'
export const description =
'We should not maintain obsolete attributes for users who have been deleted.'

View File

@ -1,4 +1,4 @@
import { getDriver } from '../../db/neo4j'
import { getDriver } from '@db/neo4j'
export const description =
'We should not maintain obsolete attributes for posts which have been deleted.'

View File

@ -1,5 +1,7 @@
import { getDriver } from '../../db/neo4j'
import { existsSync } from 'fs'
/* eslint-disable security/detect-non-literal-fs-filename */
import { existsSync } from 'node:fs'
import { getDriver } from '@db/neo4j'
export const description = `
In this review:

View File

@ -1,9 +1,9 @@
'use strict'
module.exports.up = function (next) {
export async function up(next) {
next()
}
module.exports.down = function (next) {
export async function down(next) {
next()
}

View File

@ -1,10 +1,10 @@
import { getDriver } from '../../db/neo4j'
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()
@ -28,7 +28,7 @@ module.exports.up = async function (next) {
}
}
module.exports.down = async function (next) {
export async function down(next) {
const driver = getDriver()
const session = driver.session()
const transaction = session.beginTransaction()

View File

@ -1,10 +1,10 @@
import { getDriver } from '../../db/neo4j'
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()
@ -28,7 +28,7 @@ module.exports.up = async function (next) {
}
}
module.exports.down = async function (next) {
export async function down(next) {
const driver = getDriver()
const session = driver.session()
const transaction = session.beginTransaction()

View File

@ -1,6 +1,7 @@
import { getDriver } from '../../db/neo4j'
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.'

View File

@ -1,4 +1,4 @@
import { getDriver } from '../../db/neo4j'
import { getDriver } from '@db/neo4j'
export const description = ''

View File

@ -1,4 +1,4 @@
import { getDriver } from '../../db/neo4j'
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.
@ -18,9 +18,13 @@ 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) {
@ -42,6 +46,8 @@ export async function down(next) {
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,6 +58,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

View File

@ -1,4 +1,4 @@
import { getDriver } from '../../db/neo4j'
import { getDriver } from '@db/neo4j'
export const description = ''
@ -8,6 +8,8 @@ export async function up(next) {
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,6 +33,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
@ -50,10 +53,13 @@ export async function down(next) {
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

View File

@ -1,4 +1,4 @@
import { getDriver } from '../../db/neo4j'
import { getDriver } from '@db/neo4j'
export const description = 'Add to all existing posts the Article label'

View File

@ -1,4 +1,4 @@
import { getDriver } from '../../db/neo4j'
import { getDriver } from '@db/neo4j'
export const description = 'Add postType property Article to all posts'

View File

@ -1,4 +1,4 @@
import { getDriver } from '../../db/neo4j'
import { getDriver } from '@db/neo4j'
export const description = `
Transform event start and end date of format 'YYYY-MM-DD HH:MM:SS' in CEST

View File

@ -0,0 +1,61 @@
import { getDriver } from '@db/neo4j'
export const description = `
All authors observe their posts.
`
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 (author:User)-[:WROTE]->(post:Post)
MERGE (author)-[obs:OBSERVES]->(post)
ON CREATE SET
obs.active = true,
obs.createdAt = toString(datetime()),
obs.updatedAt = toString(datetime())
RETURN post
`)
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(`
MATCH (:User)-[obs:OBSERVES]->(p:Post)
DELETE obs
RETURN p
`)
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()
}
}

View File

@ -0,0 +1,62 @@
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()
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(`
MATCH (u:User)-[obs:OBSERVES]->(p:Post)<-[:COMMENTS]-(:Comment)<-[:WROTE]-(u)
WHERE NOT (u)-[:WROTE]->(post)
DELETE obs
RETURN p
`)
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()
}
}

View File

@ -0,0 +1,68 @@
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()
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(`
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()
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()
}
}

View File

@ -1,7 +1,9 @@
/* eslint-disable import/no-named-as-default-member */
import neo4j from 'neo4j-driver'
import CONFIG from './../config'
import Neode from 'neode'
import models from '../models'
import CONFIG from '@config/index'
import models from '@models/index'
let driver
const defaultOptions = {

View File

@ -0,0 +1,20 @@
/* 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)
}
})()

View File

@ -1,5 +1,7 @@
import CONFIG from '../config'
import { cleanDatabase } from '../db/factories'
/* 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 +12,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)

View File

@ -1,20 +1,23 @@
import sample from 'lodash/sample'
import { createTestClient } from 'apollo-server-testing'
import CONFIG from '../config'
import createServer from '../server'
/* eslint-disable n/no-process-exit */
import { faker } from '@faker-js/faker'
import Factory from '../db/factories'
import { getNeode, getDriver } from '../db/neo4j'
import { createTestClient } from 'apollo-server-testing'
import sample from 'lodash/sample'
import CONFIG from '@config/index'
import { categories } from '@constants/categories'
import { createCommentMutation } from '@graphql/comments'
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'
} from '@graphql/groups'
import { createMessageMutation } from '@graphql/messages'
import { createPostMutation } from '@graphql/posts'
import { createRoomMutation } from '@graphql/rooms'
import createServer from '@src/server'
import Factory from './factories'
import { getNeode, getDriver } from './neo4j'
if (CONFIG.PRODUCTION && !CONFIG.PRODUCTION_DB_CLEAN_ALLOW) {
throw new Error(`You cannot seed the database in a non-staging and real production environment!`)
@ -1565,6 +1568,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
await driver.close()
await 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)

View File

@ -46,6 +46,8 @@ export const createPostMutation = () => {
lng
lat
}
isObservedByMe
observingUsersCount
}
}
`

View File

@ -1,3 +1,5 @@
/* eslint-disable promise/prefer-await-to-callbacks */
/* eslint-disable security/detect-object-injection */
/**
* Provide a way to iterate for each element in an array while waiting for async functions to finish
*

View File

@ -1,3 +1,4 @@
/* eslint-disable promise/avoid-new */
// 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/

View File

@ -1,3 +1,5 @@
/* eslint-disable promise/prefer-await-to-callbacks */
/* eslint-disable security/detect-object-injection */
/**
* iterate through all fields and replace it with the callback result
* @property data Array

View File

@ -1,5 +1,5 @@
import createServer from './server'
import CONFIG from './config'
import createServer from './server'
const { server, httpServer } = createServer()
const url = new URL(CONFIG.GRAPHQL_URI)

View File

@ -1,5 +1,6 @@
import Factory, { cleanDatabase } from '../db/factories'
import { getDriver, getNeode } from '../db/neo4j'
import Factory, { cleanDatabase } from '@db/factories'
import { getDriver, getNeode } from '@db/neo4j'
import decode from './decode'
import encode from './encode'

View File

@ -1,5 +1,6 @@
import jwt from 'jsonwebtoken'
import CONFIG from './../config'
import CONFIG from '@config/index'
export default async (driver, authorizationHeader) => {
if (!authorizationHeader) return null
@ -8,6 +9,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
}

View File

@ -1,6 +1,8 @@
import encode from './encode'
import jwt from 'jsonwebtoken'
import CONFIG from './../config'
import CONFIG from '@config/index'
import encode from './encode'
describe('encode', () => {
let payload

View File

@ -1,5 +1,6 @@
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) {

View File

@ -1,5 +1,6 @@
import trunc from 'trunc-html'
import { DESCRIPTION_EXCERPT_HTML_LENGTH } from '../constants/groups'
import { DESCRIPTION_EXCERPT_HTML_LENGTH } from '@constants/groups'
export default {
Mutation: {

View File

@ -1,4 +1,5 @@
import * as cheerio from 'cheerio'
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 +11,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]')

View File

@ -1,8 +1,9 @@
import gql from 'graphql-tag'
import { cleanDatabase } from '../../db/factories'
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

View File

@ -1,4 +1,4 @@
import extractHashtags from '../hashtags/extractHashtags'
import extractHashtags from './extractHashtags'
const updateHashtagsOfPost = async (postId, hashtags, context) => {
if (!hashtags.length) return

View File

@ -1,5 +1,6 @@
import sanitizeHtml from 'sanitize-html'
/* eslint-disable security/detect-unsafe-regex */
import linkifyHtml from 'linkify-html'
import sanitizeHtml from 'sanitize-html'
export const removeHtmlTags = (input) => {
return sanitizeHtml(input, {

View File

@ -1,8 +1,9 @@
import CONFIG from '../../../config'
import { cleanHtml } from '../../../middleware/helpers/cleanHtml'
import nodemailer from 'nodemailer'
import { htmlToText } from 'nodemailer-html-to-text'
import CONFIG from '@config/index'
import { cleanHtml } from '@middleware/helpers/cleanHtml'
const hasEmailConfig = CONFIG.SMTP_HOST && CONFIG.SMTP_PORT
const hasAuthData = CONFIG.SMTP_USERNAME && CONFIG.SMTP_PASSWORD
const hasDKIMData =

View File

@ -1,11 +1,13 @@
import CONFIG from '../../../config'
import logosWebapp from '../../../config/logos'
import CONFIG from '@config/index'
import logosWebapp from '@config/logos'
import {
signupTemplate,
emailVerificationTemplate,
resetPasswordTemplate,
wrongAccountTemplate,
notificationTemplate,
chatMessageTemplate,
} from './templateBuilder'
const englishHint = 'English version below!'
@ -34,6 +36,17 @@ const resetPasswordTemplateData = () => ({
name: 'Mr Example',
},
})
const chatMessageTemplateData = {
email: 'test@example.org',
variables: {
senderUser: {
name: 'Sender',
},
recipientUser: {
name: 'Recipient',
},
},
}
const wrongAccountTemplateData = () => ({
email: 'test@example.org',
variables: {},
@ -163,6 +176,32 @@ describe('templateBuilder', () => {
})
})
describe('chatMessageTemplate', () => {
describe('multi language', () => {
it('e-mail is build with all data', () => {
const subject = `Neue Chat-Nachricht | New chat message - ${chatMessageTemplateData.variables.senderUser.name}`
const actionUrl = new URL('/chat', CONFIG.CLIENT_URI).toString()
const enContent = `You have received a new chat message from <b>${chatMessageTemplateData.variables.senderUser.name}</b>.`
const deContent = `Du hast eine neue Chat-Nachricht von <b>${chatMessageTemplateData.variables.senderUser.name}</b> erhalten.`
testEmailData(null, chatMessageTemplate, chatMessageTemplateData, [
...textsStandard,
{
templPropName: 'subject',
isContaining: false,
text: subject,
},
englishHint,
actionUrl,
chatMessageTemplateData.variables.senderUser,
chatMessageTemplateData.variables.recipientUser,
enContent,
deContent,
supportUrl,
])
})
})
})
describe('wrongAccountTemplate', () => {
describe('multi language', () => {
it('e-mail is build with all data', () => {

View File

@ -1,11 +1,13 @@
/* eslint-disable import/no-namespace */
import mustache from 'mustache'
import CONFIG from '../../../config'
import metadata from '../../../config/metadata'
import logosWebapp from '../../../config/logos'
import CONFIG from '@config/index'
import logosWebapp from '@config/logos'
import metadata from '@config/metadata'
import * as templates from './templates'
import * as templatesEN from './templates/en'
import * as templatesDE from './templates/de'
import * as templatesEN from './templates/en'
const from = CONFIG.EMAIL_DEFAULT_SENDER
const welcomeImageUrl = new URL(logosWebapp.LOGO_WELCOME_PATH, CONFIG.CLIENT_URI)
@ -71,6 +73,26 @@ export const resetPasswordTemplate = ({ email, variables: { nonce, name } }) =>
}
}
export const chatMessageTemplate = ({ email, variables: { senderUser, recipientUser } }) => {
const subject = `Neue Chat-Nachricht | New chat message - ${senderUser.name}`
const actionUrl = new URL('/chat', CONFIG.CLIENT_URI)
const renderParams = {
...defaultParams,
subject,
englishHint,
actionUrl,
senderUser,
recipientUser,
}
return {
from,
to: email,
subject,
html: mustache.render(templates.layout, renderParams, { content: templates.chatMessage }),
}
}
export const wrongAccountTemplate = ({ email, _variables = {} }) => {
const subject = 'Falsche Mailadresse? | Wrong E-mail?'
const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI)

View File

@ -0,0 +1,105 @@
<!-- Email Body German : BEGIN -->
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<!-- Hero Image, Flush : BEGIN -->
<tr>
<td style="background-color: #ffffff;">
<img
src="{{{ welcomeImageUrl }}}"
width="300" height="" alt="Welcome image" border="0"
style="width: 100%; max-width: 300px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block; padding: 20px;"
class="g-img">
</td>
</tr>
<!-- Hero Image, Flush : END -->
<!-- 1 Column Text + Button : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<h1
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
Hallo {{ recipientUser.name }}!</h1>
<p style="margin: 0;">Du hast eine neue Chat-Nachricht von <b>{{ senderUser.name }}</b> erhalten.</p>
</td>
</tr>
<tr>
<td style="padding: 0 20px;">
<!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
<tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">Chat anzeigen</a>
</td>
</tr>
</table>
<!-- Button : END -->
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text + Button : END -->
</table>
<!-- Email Body German : END -->
<!-- Email Body English : BEGIN -->
<table class="email-english" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<tr>
<td style="padding: 20px 0; text-align: center">
</td>
</tr>
<!-- Hero Image, Flush : BEGIN -->
<tr>
<td style="background-color: #ffffff;">
<img
src="{{{ welcomeImageUrl }}}"
width="300" height="" alt="Welcome image" border="0"
style="width: 100%; max-width: 300px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block; padding: 20px;"
class="g-img">
</td>
</tr>
<!-- Hero Image, Flush : END -->
<!-- 1 Column Text + Button : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<h1
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
Hello {{ recipientUser.name }}!</h1>
<p style="margin: 0;">You have received a new chat message from <b>{{ senderUser.name }}</b>.</p>
</td>
</tr>
<tr>
<td style="padding: 0 20px;">
<!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
<tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">Show Chat</a>
</td>
</tr>
</table>
<!-- Button : END -->
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text + Button : END -->
</table>
<!-- Email Body English : END -->

View File

@ -1,5 +1,6 @@
import fs from 'fs'
import path from 'path'
/* eslint-disable security/detect-non-literal-fs-filename */
import fs from 'node:fs'
import path from 'node:path'
const readFile = (fileName) => fs.readFileSync(path.join(__dirname, fileName), 'utf-8')

View File

@ -1,5 +1,6 @@
import fs from 'fs'
import path from 'path'
/* eslint-disable security/detect-non-literal-fs-filename */
import fs from 'node:fs'
import path from 'node:path'
const readFile = (fileName) => fs.readFileSync(path.join(__dirname, fileName), 'utf-8')

View File

@ -1,5 +1,6 @@
import fs from 'fs'
import path from 'path'
/* eslint-disable security/detect-non-literal-fs-filename */
import fs from 'node:fs'
import path from 'node:path'
const readFile = (fileName) => fs.readFileSync(path.join(__dirname, fileName), 'utf-8')
@ -7,5 +8,6 @@ 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 chatMessage = readFile('./chatMessage.html')
export const layout = readFile('./layout.html')

View File

@ -0,0 +1,44 @@
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)
})
})
})

View File

@ -0,0 +1,16 @@
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
}

View File

@ -1,20 +1,24 @@
/* eslint-disable security/detect-object-injection */
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'
import CONFIG from '@config/index'
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'
// eslint-disable-next-line import/no-cycle
import notifications from './notifications/notificationsMiddleware'
import orderBy from './orderByMiddleware'
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 default (schema) => {
const middlewares = {

View File

@ -1,8 +1,9 @@
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 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

View File

@ -1,5 +1,6 @@
import LanguageDetect from 'languagedetect'
import { removeHtmlTags } from '../helpers/cleanHtml'
import { removeHtmlTags } from '@middleware/helpers/cleanHtml'
const setPostLanguage = (text, defaultLanguage) => {
const lngDetector = new LanguageDetect()

View File

@ -1,10 +1,10 @@
import { sendMail } from '../helpers/email/sendMail'
import { sendMail } from '@middleware/helpers/email/sendMail'
import {
signupTemplate,
resetPasswordTemplate,
wrongAccountTemplate,
emailVerificationTemplate,
} from '../helpers/email/templateBuilder'
} from '@middleware/helpers/email/templateBuilder'
const sendSignupMail = async (resolve, root, args, context, resolveInfo) => {
const { inviteCode } = args

View File

@ -0,0 +1,430 @@
import { createTestClient } from 'apollo-server-testing'
import gql from 'graphql-tag'
import Factory, { cleanDatabase } from '@db/factories'
import { getNeode, getDriver } from '@db/neo4j'
import { createGroupMutation } from '@graphql/groups'
import CONFIG from '@src/config'
import createServer from '@src/server'
CONFIG.CATEGORIES_ACTIVE = false
const sendMailMock = jest.fn()
jest.mock('../helpers/email/sendMail', () => ({
sendMail: () => sendMailMock(),
}))
let server, query, mutate, authenticatedUser
let postAuthor, firstFollower, secondFollower
const driver = getDriver()
const neode = getNeode()
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
}
}
`
beforeAll(async () => {
await cleanDatabase()
const createServerResult = createServer({
context: () => {
return {
user: authenticatedUser,
neode,
driver,
cypherParams: {
currentUserId: authenticatedUser ? authenticatedUser.id : null,
},
}
},
})
server = createServerResult.server
const createTestClientResult = createTestClient(server)
query = createTestClientResult.query
mutate = createTestClientResult.mutate
})
afterAll(async () => {
await cleanDatabase()
driver.close()
})
describe('following users notifications', () => {
beforeAll(async () => {
postAuthor = await Factory.build(
'user',
{
id: 'post-author',
name: 'Post Author',
slug: 'post-author',
},
{
email: 'test@example.org',
password: '1234',
},
)
firstFollower = await Factory.build(
'user',
{
id: 'first-follower',
name: 'First Follower',
slug: 'first-follower',
},
{
email: 'test2@example.org',
password: '1234',
},
)
secondFollower = await Factory.build(
'user',
{
id: 'second-follower',
name: 'Second Follower',
slug: 'second-follower',
},
{
email: 'test3@example.org',
password: '1234',
},
)
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' },
})
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 only one email, as second follower has emails disabled', () => {
expect(sendMailMock).toHaveBeenCalledTimes(1)
})
})
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,
})
})
})
})

View File

@ -1,8 +1,8 @@
import * as cheerio from 'cheerio'
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')

View File

@ -1,15 +1,35 @@
import gql from 'graphql-tag'
import { cleanDatabase } from '../../db/factories'
import { createTestClient } from 'apollo-server-testing'
import { getNeode, getDriver } from '../../db/neo4j'
import createServer, { pubsub } from '../../server'
import gql from 'graphql-tag'
import Factory, { cleanDatabase } from '@db/factories'
import { getNeode, getDriver } from '@db/neo4j'
import {
createGroupMutation,
joinGroupMutation,
leaveGroupMutation,
changeGroupMemberRoleMutation,
removeUserFromGroupMutation,
} from '../../graphql/groups'
} from '@graphql/groups'
import { createMessageMutation } from '@graphql/messages'
import { createRoomMutation } from '@graphql/rooms'
import createServer, { pubsub } from '@src/server'
const sendMailMock = jest.fn()
jest.mock('../helpers/email/sendMail', () => ({
sendMail: () => sendMailMock(),
}))
const chatMessageTemplateMock = jest.fn()
const notificationTemplateMock = jest.fn()
jest.mock('../helpers/email/templateBuilder', () => ({
chatMessageTemplate: () => chatMessageTemplateMock(),
notificationTemplate: () => notificationTemplateMock(),
}))
let isUserOnlineMock = jest.fn()
jest.mock('../helpers/isUserOnline', () => ({
isUserOnline: () => isUserOnlineMock(),
}))
let server, query, mutate, notifiedUser, authenticatedUser
let publishSpy
@ -68,8 +88,8 @@ afterAll(async () => {
beforeEach(async () => {
publishSpy.mockClear()
notifiedUser = await neode.create(
'User',
notifiedUser = await Factory.build(
'user',
{
id: 'you',
name: 'Al Capone',
@ -169,6 +189,7 @@ describe('notifications', () => {
describe('commenter is not me', () => {
beforeEach(async () => {
jest.clearAllMocks()
commentContent = 'Commenters comment.'
commentAuthor = await neode.create(
'User',
@ -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,85 @@ 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(sendMailMock).toHaveBeenCalledTimes(1)
expect(notificationTemplateMock).toHaveBeenCalledTimes(1)
})
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(sendMailMock).not.toHaveBeenCalled()
expect(notificationTemplateMock).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)
})
})
})
@ -238,7 +303,6 @@ describe('notifications', () => {
})
it('sends me no notification', async () => {
await notifiedUser.relateTo(commentAuthor, 'blocked')
await createCommentOnPostAction()
const expected = expect.objectContaining({
data: { notifications: [] },
@ -257,6 +321,7 @@ describe('notifications', () => {
})
beforeEach(async () => {
jest.clearAllMocks()
postAuthor = await neode.create(
'User',
{
@ -279,7 +344,7 @@ describe('notifications', () => {
'Hey <a class="mention" data-mention-id="you" href="/profile/you/al-capone">@al-capone</a> how do you do?'
})
it('sends me a notification', async () => {
it('sends me a notification and email', async () => {
await createPostAction()
const expectedContent =
'Hey <a class="mention" data-mention-id="you" href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do?'
@ -307,6 +372,47 @@ describe('notifications', () => {
],
},
})
// Mail
expect(sendMailMock).toHaveBeenCalledTimes(1)
expect(notificationTemplateMock).toHaveBeenCalledTimes(1)
})
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 <a class="mention" data-mention-id="you" href="/profile/you/al-capone" target="_blank">@al-capone</a> 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(sendMailMock).not.toHaveBeenCalled()
expect(notificationTemplateMock).not.toHaveBeenCalled()
})
})
it('publishes `NOTIFICATION_ADDED` to me', async () => {
@ -634,12 +740,121 @@ describe('notifications', () => {
})
})
describe('chat email notifications', () => {
let chatSender
let chatReceiver
let roomId
beforeEach(async () => {
jest.clearAllMocks()
chatSender = await neode.create(
'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('sends no email', async () => {
isUserOnlineMock = jest.fn().mockReturnValue(true)
await mutate({
mutation: createMessageMutation(),
variables: {
roomId,
content: 'Some nice message to chatReceiver',
},
})
expect(sendMailMock).not.toHaveBeenCalled()
expect(chatMessageTemplateMock).not.toHaveBeenCalled()
})
})
describe('if the chatReceiver is offline', () => {
it('sends an email', async () => {
isUserOnlineMock = jest.fn().mockReturnValue(false)
await mutate({
mutation: createMessageMutation(),
variables: {
roomId,
content: 'Some nice message to chatReceiver',
},
})
expect(sendMailMock).toHaveBeenCalledTimes(1)
expect(chatMessageTemplateMock).toHaveBeenCalledTimes(1)
})
})
describe('if the chatReceiver has blocked chatSender', () => {
it('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(sendMailMock).not.toHaveBeenCalled()
expect(chatMessageTemplateMock).not.toHaveBeenCalled()
})
})
describe('if the chatReceiver has disabled `emailNotificationsChatMessage`', () => {
it('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(sendMailMock).not.toHaveBeenCalled()
expect(chatMessageTemplateMock).not.toHaveBeenCalled()
})
})
})
describe('group notifications', () => {
let groupOwner
beforeEach(async () => {
groupOwner = await neode.create(
'User',
groupOwner = await Factory.build(
'user',
{
id: 'group-owner',
name: 'Group Owner',
@ -666,7 +881,7 @@ describe('notifications', () => {
})
describe('user joins group', () => {
beforeEach(async () => {
const joinGroupAction = async () => {
authenticatedUser = await notifiedUser.toJson()
await mutate({
mutation: joinGroupMutation(),
@ -676,9 +891,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,
@ -702,19 +922,50 @@ describe('notifications', () => {
},
errors: undefined,
})
// Mail
expect(sendMailMock).toHaveBeenCalledTimes(1)
expect(notificationTemplateMock).toHaveBeenCalledTimes(1)
})
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(sendMailMock).not.toHaveBeenCalled()
expect(notificationTemplateMock).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: {
@ -723,9 +974,22 @@ 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,
@ -761,19 +1025,61 @@ describe('notifications', () => {
},
errors: undefined,
})
// Mail
expect(sendMailMock).toHaveBeenCalledTimes(2)
expect(notificationTemplateMock).toHaveBeenCalledTimes(2)
})
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: [
{
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(sendMailMock).toHaveBeenCalledTimes(1)
expect(notificationTemplateMock).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(),
@ -784,9 +1090,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,
@ -810,19 +1130,49 @@ describe('notifications', () => {
},
errors: undefined,
})
// Mail
expect(sendMailMock).toHaveBeenCalledTimes(1)
expect(notificationTemplateMock).toHaveBeenCalledTimes(1)
})
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(sendMailMock).not.toHaveBeenCalled()
expect(notificationTemplateMock).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(),
@ -832,9 +1182,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,
@ -858,6 +1222,44 @@ describe('notifications', () => {
},
errors: undefined,
})
// Mail
expect(sendMailMock).toHaveBeenCalledTimes(1)
expect(notificationTemplateMock).toHaveBeenCalledTimes(1)
})
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(sendMailMock).not.toHaveBeenCalled()
expect(notificationTemplateMock).not.toHaveBeenCalled()
})
})
})
})

View File

@ -1,8 +1,15 @@
import { pubsub, NOTIFICATION_ADDED } from '../../server'
/* eslint-disable security/detect-object-injection */
import { sendMail } from '@middleware/helpers/email/sendMail'
import {
chatMessageTemplate,
notificationTemplate,
} from '@middleware/helpers/email/templateBuilder'
import { isUserOnline } from '@middleware/helpers/isUserOnline'
import { validateNotifyUsers } from '@middleware/validation/validationMiddleware'
// eslint-disable-next-line import/no-cycle
import { pubsub, NOTIFICATION_ADDED } from '@src/server'
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 []
@ -31,7 +38,7 @@ const queryNotificationEmails = async (context, notificationUserIds) => {
}
}
const publishNotifications = async (context, promises) => {
const publishNotifications = async (context, promises, emailNotificationSetting: string) => {
let notifications = await Promise.all(promises)
notifications = notifications.flat()
const notificationsEmailAddresses = await queryNotificationEmails(
@ -40,7 +47,7 @@ const publishNotifications = async (context, promises) => {
)
notifications.forEach((notificationAdded, index) => {
pubsub.publish(NOTIFICATION_ADDED, { notificationAdded })
if (notificationAdded.to.sendNotificationEmails) {
if (notificationAdded.to[emailNotificationSetting] ?? true) {
sendMail(
notificationTemplate({
email: notificationsEmailAddresses[index].email,
@ -55,9 +62,11 @@ 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, [
notifyOwnersOfGroup(groupId, userId, 'user_joined_group', context),
])
await publishNotifications(
context,
[notifyOwnersOfGroup(groupId, userId, 'user_joined_group', context)],
'emailNotificationsGroupMemberJoined',
)
}
return user
}
@ -66,9 +75,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, [
notifyOwnersOfGroup(groupId, userId, 'user_left_group', context),
])
await publishNotifications(
context,
[notifyOwnersOfGroup(groupId, userId, 'user_left_group', context)],
'emailNotificationsGroupMemberLeft',
)
}
return user
}
@ -77,9 +88,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, [
notifyMemberOfGroup(groupId, userId, 'changed_group_member_role', context),
])
await publishNotifications(
context,
[notifyMemberOfGroup(groupId, userId, 'changed_group_member_role', context)],
'emailNotificationsGroupMemberRoleChanged',
)
}
return user
}
@ -88,34 +101,65 @@ 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, [
notifyMemberOfGroup(groupId, userId, 'removed_user_from_group', 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, [
notifyUsersOfMention('Post', post.id, idsOfUsers, 'mentioned_in_post', context),
])
await publishNotifications(
context,
[notifyUsersOfMention('Post', post.id, idsOfUsers, 'mentioned_in_post', context)],
'emailNotificationsMention',
)
await publishNotifications(
context,
[notifyFollowingUsers(post.id, groupId, context)],
'emailNotificationsFollowingUsers',
)
await publishNotifications(
context,
[notifyGroupMembersOfNewPost(post.id, groupId, context)],
'emailNotificationsPostInGroup',
)
}
return post
}
const handleContentDataOfComment = async (resolve, root, args, context, resolveInfo) => {
const { content } = args
let idsOfUsers = extractMentionedUsers(content)
let idsOfMentionedUsers = extractMentionedUsers(content)
const comment = await resolve(root, args, context, resolveInfo)
const [postAuthor] = await postAuthorOfComment(comment.id, { context })
idsOfUsers = idsOfUsers.filter((id) => id !== postAuthor.id)
await publishNotifications(context, [
notifyUsersOfMention('Comment', comment.id, idsOfUsers, 'mentioned_in_comment', context),
notifyUsersOfComment('Comment', comment.id, postAuthor.id, 'commented_on_post', context),
])
idsOfMentionedUsers = idsOfMentionedUsers.filter((id) => id !== postAuthor.id)
await publishNotifications(
context,
[
notifyUsersOfMention(
'Comment',
comment.id,
idsOfMentionedUsers,
'mentioned_in_comment',
context,
),
],
'emailNotificationsMention',
)
await publishNotifications(
context,
[notifyUsersOfComment('Comment', comment.id, 'commented_on_post', context)],
'emailNotificationsCommentOnObservedPost',
)
return comment
}
@ -138,6 +182,88 @@ 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 })
WITH post, author, user, 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,
post {.*, author: properties(author) } AS finalResource
RETURN notification {
.*,
from: finalResource,
to: properties(user),
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 {
const notifications = await writeTxResultPromise
return notifications
} 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 })
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.id = $userId
WITH post, author, user
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,
post {.*, author: properties(author) } AS finalResource
RETURN notification {
.*,
from: finalResource,
to: properties(user),
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 {
const notifications = await writeTxResultPromise
return notifications
} catch (error) {
throw new Error(error)
} finally {
session.close()
}
}
const notifyOwnersOfGroup = async (groupId, userId, reason, context) => {
const cypher = `
MATCH (user:User { id: $userId })
@ -269,29 +395,34 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => {
}
}
const notifyUsersOfComment = async (label, commentId, postAuthorId, reason, context) => {
if (context.user.id === postAuthorId) return []
const notifyUsersOfComment = async (label, commentId, reason, context) => {
await validateNotifyUsers(label, reason)
const session = context.driver.session()
const writeTxResultPromise = await session.writeTransaction(async (transaction) => {
const notificationTransactionResponse = await transaction.run(
`
MATCH (postAuthor:User {id: $postAuthorId})-[:WROTE]->(post:Post)<-[:COMMENTS]-(comment:Comment { id: $commentId })<-[:WROTE]-(commenter:User)
WHERE NOT (postAuthor)-[:BLOCKED]-(commenter)
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(postAuthor)
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
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, postAuthor, post, commenter,
WITH notification, observingUser, post, commenter, postAuthor,
comment {.*, __typename: labels(comment)[0], author: properties(commenter), post: post {.*, author: properties(postAuthor) } } AS finalResource
RETURN notification {
.*,
from: finalResource,
to: properties(postAuthor),
to: properties(observingUser),
relatedUser: properties(commenter)
}
`,
{ commentId, postAuthorId, reason },
{
commentId,
reason,
userId: context.user.id,
},
)
return notificationTransactionResponse.records.map((record) => record.get('notification'))
})
@ -303,6 +434,57 @@ const notifyUsersOfComment = async (label, commentId, postAuthorId, reason, cont
}
}
const handleCreateMessage = async (resolve, root, args, context, resolveInfo) => {
// Execute resolver
const result = 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.emailNotificationsChatMessage = false
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
// Send EMail if we found a user(not blocked) and he is not considered online
if (recipientUser && !isUserOnline(recipientUser)) {
void sendMail(chatMessageTemplate({ email, variables: { senderUser, recipientUser } }))
}
// Return resolver result to client
return result
} catch (error) {
throw new Error(error)
} finally {
session.close()
}
}
export default {
Mutation: {
CreatePost: handleContentDataOfPost,
@ -313,5 +495,6 @@ export default {
LeaveGroup: handleLeaveGroup,
ChangeGroupMemberRole: handleChangeGroupMemberRole,
RemoveUserFromGroup: handleRemoveUserFromGroup,
CreateMessage: handleCreateMessage,
},
}

View File

@ -0,0 +1,377 @@
import { createTestClient } from 'apollo-server-testing'
import gql from 'graphql-tag'
import CONFIG from '@config/index'
import { cleanDatabase } from '@db/factories'
import { getNeode, getDriver } from '@db/neo4j'
import createServer from '@src/server'
CONFIG.CATEGORIES_ACTIVE = false
let server, query, mutate, authenticatedUser
let postAuthor, firstCommenter, secondCommenter
const driver = getDriver()
const neode = getNeode()
const createPostMutation = gql`
mutation ($id: ID, $title: String!, $content: String!) {
CreatePost(id: $id, title: $title, content: $content) {
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 toggleObservePostMutation = gql`
mutation ($id: ID!, $value: Boolean!) {
toggleObservePost(id: $id, value: $value) {
isObservedByMe
observingUsersCount
}
}
`
beforeAll(async () => {
await cleanDatabase()
const createServerResult = createServer({
context: () => {
return {
user: authenticatedUser,
neode,
driver,
cypherParams: {
currentUserId: authenticatedUser ? authenticatedUser.id : null,
},
}
},
})
server = createServerResult.server
const createTestClientResult = createTestClient(server)
query = createTestClientResult.query
mutate = createTestClientResult.mutate
})
afterAll(async () => {
await cleanDatabase()
driver.close()
})
describe('notifications for users that observe a post', () => {
beforeAll(async () => {
postAuthor = await neode.create(
'User',
{
id: 'post-author',
name: 'Post Author',
slug: 'post-author',
},
{
email: 'test@example.org',
password: '1234',
},
)
firstCommenter = await neode.create(
'User',
{
id: 'first-commenter',
name: 'First Commenter',
slug: 'first-commenter',
},
{
email: 'test2@example.org',
password: '1234',
},
)
secondCommenter = await neode.create(
'User',
{
id: 'second-commenter',
name: 'Second Commenter',
slug: 'second-commenter',
},
{
email: 'test3@example.org',
password: '1234',
},
)
authenticatedUser = await postAuthor.toJson()
await mutate({
mutation: createPostMutation,
variables: {
id: 'post',
title: 'This is the post',
content: 'This is the content of the post',
},
})
})
describe('first comment on the post', () => {
beforeAll(async () => {
authenticatedUser = await firstCommenter.toJson()
await mutate({
mutation: createCommentMutation,
variables: {
postId: 'post',
id: 'c-1',
content: 'first comment of first commenter',
},
})
})
it('sends NO notification to the commenter', async () => {
await expect(
query({
query: notificationQuery,
}),
).resolves.toMatchObject({
data: {
notifications: [],
},
errors: undefined,
})
})
it('sends notification to the author', async () => {
authenticatedUser = await postAuthor.toJson()
await expect(
query({
query: notificationQuery,
}),
).resolves.toMatchObject({
data: {
notifications: [
{
from: {
__typename: 'Comment',
id: 'c-1',
},
read: false,
reason: 'commented_on_post',
},
],
},
errors: undefined,
})
})
describe('second comment on post', () => {
beforeAll(async () => {
authenticatedUser = await secondCommenter.toJson()
await mutate({
mutation: createCommentMutation,
variables: {
postId: 'post',
id: 'c-2',
content: 'first comment of second commenter',
},
})
})
it('sends NO notification to the commenter', async () => {
await expect(
query({
query: notificationQuery,
}),
).resolves.toMatchObject({
data: {
notifications: [],
},
errors: undefined,
})
})
it('sends notification to the author', async () => {
authenticatedUser = await postAuthor.toJson()
await expect(
query({
query: notificationQuery,
}),
).resolves.toMatchObject({
data: {
notifications: [
{
from: {
__typename: 'Comment',
id: 'c-2',
},
read: false,
reason: 'commented_on_post',
},
{
from: {
__typename: 'Comment',
id: 'c-1',
},
read: false,
reason: 'commented_on_post',
},
],
},
errors: undefined,
})
})
it('sends notification to first commenter', async () => {
authenticatedUser = await firstCommenter.toJson()
await expect(
query({
query: notificationQuery,
}),
).resolves.toMatchObject({
data: {
notifications: [
{
from: {
__typename: 'Comment',
id: 'c-2',
},
read: false,
reason: 'commented_on_post',
},
],
},
errors: undefined,
})
})
})
describe('first commenter unfollows the post and post author comments post', () => {
beforeAll(async () => {
authenticatedUser = await firstCommenter.toJson()
await mutate({
mutation: toggleObservePostMutation,
variables: {
id: 'post',
value: false,
},
})
authenticatedUser = await postAuthor.toJson()
await mutate({
mutation: createCommentMutation,
variables: {
postId: 'post',
id: 'c-3',
content: 'first comment of post author',
},
})
})
it('sends no new notification to the post author', async () => {
await expect(
query({
query: notificationQuery,
}),
).resolves.toMatchObject({
data: {
notifications: [
{
from: {
__typename: 'Comment',
id: 'c-2',
},
read: false,
reason: 'commented_on_post',
},
{
from: {
__typename: 'Comment',
id: 'c-1',
},
read: false,
reason: 'commented_on_post',
},
],
},
errors: undefined,
})
})
it('sends no new notification to first commenter', async () => {
authenticatedUser = await firstCommenter.toJson()
await expect(
query({
query: notificationQuery,
}),
).resolves.toMatchObject({
data: {
notifications: [
{
from: {
__typename: 'Comment',
id: 'c-2',
},
read: false,
reason: 'commented_on_post',
},
],
},
errors: undefined,
})
})
it('sends notification to second commenter', async () => {
authenticatedUser = await secondCommenter.toJson()
await expect(
query({
query: notificationQuery,
}),
).resolves.toMatchObject({
data: {
notifications: [
{
from: {
__typename: 'Comment',
id: 'c-3',
},
read: false,
reason: 'commented_on_post',
},
],
},
errors: undefined,
})
})
})
})
})

View File

@ -0,0 +1,400 @@
import { createTestClient } from 'apollo-server-testing'
import gql from 'graphql-tag'
import Factory, { cleanDatabase } from '@db/factories'
import { getNeode, getDriver } from '@db/neo4j'
import {
createGroupMutation,
joinGroupMutation,
changeGroupMemberRoleMutation,
} from '@graphql/groups'
import CONFIG from '@src/config'
import createServer from '@src/server'
CONFIG.CATEGORIES_ACTIVE = false
const sendMailMock = jest.fn()
jest.mock('../helpers/email/sendMail', () => ({
sendMail: () => sendMailMock(),
}))
let server, query, mutate, authenticatedUser
let postAuthor, groupMember, pendingMember
const driver = getDriver()
const neode = getNeode()
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
}
}
`,
})
beforeAll(async () => {
await cleanDatabase()
const createServerResult = createServer({
context: () => {
return {
user: authenticatedUser,
neode,
driver,
cypherParams: {
currentUserId: authenticatedUser ? authenticatedUser.id : null,
},
}
},
})
server = createServerResult.server
const createTestClientResult = createTestClient(server)
query = createTestClientResult.query
mutate = createTestClientResult.mutate
})
afterAll(async () => {
await cleanDatabase()
driver.close()
})
describe('notify group members of new posts in group', () => {
beforeAll(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: 'test2@example.org',
password: '1234',
},
)
pendingMember = await Factory.build(
'user',
{
id: 'pending-member',
name: 'Pending Member',
slug: 'pending-member',
},
{
email: 'test3@example.org',
password: '1234',
},
)
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 postAuthor.toJson()
await mutate({
mutation: changeGroupMemberRoleMutation(),
variables: {
groupId: 'g-1',
userId: 'group-member',
roleInGroup: 'usual',
},
})
})
describe('group owner posts in group', () => {
beforeAll(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(sendMailMock).toHaveBeenCalledTimes(1)
})
describe('group member mutes group', () => {
it('sets the muted status correctly', async () => {
authenticatedUser = await groupMember.toJson()
await expect(
mutate({
mutation: muteGroupMutation,
variables: {
groupId: 'g-1',
},
}),
).resolves.toMatchObject({
data: {
muteGroup: {
isMutedByMe: true,
},
},
errors: undefined,
})
})
it('sends NO notification when another post is posted', async () => {
jest.clearAllMocks()
authenticatedUser = await groupMember.toJson()
await markAllAsRead()
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',
},
})
authenticatedUser = await groupMember.toJson()
await expect(
query({
query: notificationQuery,
variables: {
read: false,
},
}),
).resolves.toMatchObject({
data: {
notifications: [],
},
errors: undefined,
})
})
it('sends NO email', () => {
expect(sendMailMock).not.toHaveBeenCalled()
})
describe('group member unmutes group again but disables email', () => {
beforeAll(async () => {
jest.clearAllMocks()
await groupMember.update({ emailNotificationsPostInGroup: false })
})
it('sets the muted status correctly', async () => {
authenticatedUser = await groupMember.toJson()
await expect(
mutate({
mutation: unmuteGroupMutation,
variables: {
groupId: 'g-1',
},
}),
).resolves.toMatchObject({
data: {
unmuteGroup: {
isMutedByMe: false,
},
},
errors: undefined,
})
})
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(sendMailMock).not.toHaveBeenCalled()
})
})
})
})
})

View File

@ -1,8 +1,9 @@
import gql from 'graphql-tag'
import { cleanDatabase } from '../db/factories'
import { getNeode, getDriver } from '../db/neo4j'
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()

View File

@ -1,9 +1,10 @@
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'
import CONFIG from '@config/index'
import Factory, { cleanDatabase } from '@db/factories'
import { getDriver, getNeode } from '@db/neo4j'
import createServer from '@src/server'
const instance = getNeode()
const driver = getDriver()

View File

@ -1,7 +1,8 @@
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 { getNeode } from '@db/neo4j'
import { validateInviteCode } from '@schema/resolvers/transactions/inviteCodes'
const debug = !!CONFIG.DEBUG
const allowExternalErrors = true
@ -383,7 +384,7 @@ export default shield(
Tag: allow,
reports: isModerator,
statistics: allow,
currentUser: allow,
currentUser: isAuthenticated,
Group: isAuthenticated,
GroupMembers: isAllowedSeeingGroupMembers,
GroupCount: isAuthenticated,
@ -391,7 +392,6 @@ export default shield(
profilePagePosts: allow,
Comment: allow,
User: or(noEmailFilter, isAdmin),
isLoggedIn: allow,
Badge: allow,
PostsEmotionsCountByEmotion: allow,
PostsEmotionsByCurrentUser: isAuthenticated,
@ -462,12 +462,17 @@ export default shield(
switchUserRole: isAdmin,
markTeaserAsViewed: allow,
saveCategorySettings: isAuthenticated,
updateOnlineStatus: isAuthenticated,
CreateRoom: isAuthenticated,
CreateMessage: isAuthenticated,
MarkMessagesAsSeen: isAuthenticated,
toggleObservePost: isAuthenticated,
muteGroup: and(isAuthenticated, isMemberOfGroup),
unmuteGroup: and(isAuthenticated, isMemberOfGroup),
},
User: {
email: or(isMyOwn, isAdmin),
emailNotificationSettings: isMyOwn,
},
Report: isModerator,
},

View File

@ -1,6 +1,8 @@
import { sentry } from 'graphql-middleware-sentry'
import CONFIG from '../config'
import CONFIG from '@config/index'
// eslint-disable-next-line import/no-mutable-exports
let sentryMiddleware: any = (resolve, root, args, context, resolveInfo) =>
resolve(root, args, context, resolveInfo)

View File

@ -1,10 +1,11 @@
import { getNeode, getDriver } from '../db/neo4j'
import createServer from '../server'
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'
import Factory, { cleanDatabase } from '@db/factories'
import { getNeode, getDriver } from '@db/neo4j'
import { signupVerificationMutation } from '@graphql/authentications'
import { createGroupMutation, updateGroupMutation } from '@graphql/groups'
import { createPostMutation } from '@graphql/posts'
import createServer from '@src/server'
let authenticatedUser
let variables
@ -21,6 +22,9 @@ const { server } = createServer({
driver,
neode,
user: authenticatedUser,
cypherParams: {
currentUserId: authenticatedUser ? authenticatedUser.id : null,
},
}
},
})

View File

@ -1,8 +1,9 @@
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 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()

View File

@ -1,8 +1,9 @@
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 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

View File

@ -1,8 +1,9 @@
import gql from 'graphql-tag'
import Factory, { cleanDatabase } from '../../db/factories'
import { getNeode, getDriver } from '../../db/neo4j'
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()

View File

@ -101,7 +101,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') ||

Some files were not shown because too many files have changed in this diff Show More