mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2026-01-20 20:01:25 +00:00
Merge branch 'master' of https://github.com/Ocelot-Social-Community/Ocelot-Social into freilernen.social-code-of-conduct-etc-tryout
# Conflicts: # webapp/constants/links.js
This commit is contained in:
commit
13a141d12f
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@ -1,3 +1,5 @@
|
||||
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
|
||||
|
||||
<!--
|
||||
Please take a look at the issue templates at https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/new/choose
|
||||
before submitting a new issue. Following one of the issue templates will ensure maintainers can route your request efficiently.
|
||||
|
||||
7
.github/ISSUE_TEMPLATE/bug_report.md
vendored
7
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,9 +1,10 @@
|
||||
---
|
||||
name: 🐛 Bug Report
|
||||
about: Create a report to help us to improve.
|
||||
name: 🐛 Bug report
|
||||
about: Create a report to help us improve
|
||||
labels: bug
|
||||
title: 🐛 [Bug]
|
||||
---
|
||||
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
|
||||
|
||||
## :bug: Bug Report
|
||||
## 🐛 Bugreport
|
||||
<!-- Describe your issue in detail. Include screenshots if needed. Give us as much information as possible. Use a clear and concise description of what the bug is.-->
|
||||
|
||||
7
.github/ISSUE_TEMPLATE/devops_ticket.md
vendored
7
.github/ISSUE_TEMPLATE/devops_ticket.md
vendored
@ -1,9 +1,10 @@
|
||||
---
|
||||
name: 💥 DevOps Ticket
|
||||
about: Help us manage our deployed app.
|
||||
name: 💥 DevOps ticket
|
||||
about: Help us manage our deployed Software.
|
||||
labels: devops
|
||||
title: 💥 [DevOps]
|
||||
---
|
||||
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
|
||||
|
||||
## 💥 DevOps Ticket
|
||||
## 💥 DevOps ticket
|
||||
<!-- Describe your issue in detail. Include screenshots if needed. Give us as much information as possible. Use a clear and concise description of what the problem is.-->
|
||||
|
||||
7
.github/ISSUE_TEMPLATE/epic.md
vendored
7
.github/ISSUE_TEMPLATE/epic.md
vendored
@ -1,12 +1,13 @@
|
||||
---
|
||||
name: 🌟 Epic
|
||||
about: Define a big development step.
|
||||
about: Define a big development Step
|
||||
labels: epic
|
||||
title: 🌟 [EPIC]
|
||||
---
|
||||
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
|
||||
|
||||
<!-- THIS ISSUE-TYPE IS NOT FOR YOU! -->
|
||||
<!-- If you need an answer right away, visit the ocelot.social Discord:
|
||||
https://discord.gg/AJSX9DCSUA -->
|
||||
<!-- Proceed only if you know what you are doing - have a chat with Project's Team first -->
|
||||
|
||||
## 🌟 EPIC
|
||||
<!-- Describe your Epic in detail. Include screenshots and drawings -->
|
||||
|
||||
7
.github/ISSUE_TEMPLATE/feature_request.md
vendored
7
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,9 +1,10 @@
|
||||
---
|
||||
name: 🚀 Feature Request
|
||||
about: Suggest an idea for this project.
|
||||
name: 🚀 Feature request
|
||||
about: Suggest an idea for this project
|
||||
labels: feature
|
||||
title: 🚀 [Feature]
|
||||
---
|
||||
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
|
||||
|
||||
## :rocket: Feature Request
|
||||
## 🚀 Feature
|
||||
<!-- Give a short summary of the Feature. Use Screenshots if you want. -->
|
||||
|
||||
9
.github/ISSUE_TEMPLATE/question.md
vendored
9
.github/ISSUE_TEMPLATE/question.md
vendored
@ -1,12 +1,13 @@
|
||||
---
|
||||
name: 💬 Question
|
||||
about: If you need help understanding ocelot.social.
|
||||
about: If you need help understanding our Software.
|
||||
labels: question
|
||||
title: 💬 [Question]
|
||||
---
|
||||
<!-- Chat with ocelot.social team -->
|
||||
<!-- If you need an answer right away, visit the ocelot.social Discord:
|
||||
https://discord.gg/AJSX9DCSUA -->
|
||||
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
|
||||
|
||||
<!-- Question the project's team -->
|
||||
<!-- If you need an answer right away, consider to take other means of communication with the project's team -->
|
||||
|
||||
## 💬 Question
|
||||
<!-- Describe your Question in detail. Include screenshots and drawings if needed. -->
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
---
|
||||
name: 🔧 Refactor
|
||||
name: 🔧 Refactor ticket
|
||||
about: Help us improve our code by refactoring it.
|
||||
labels: refactor
|
||||
title: 🔧 [Refactor]
|
||||
---
|
||||
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
|
||||
|
||||
## 🔧 Refactor
|
||||
## 🔧 Refactor ticket
|
||||
<!-- Describe your issue in detail. Include screenshots if needed. Give us as much information as possible. Use a clear and concise description of what the problem is.-->
|
||||
|
||||
13
.github/ISSUE_TEMPLATE/release.md
vendored
Normal file
13
.github/ISSUE_TEMPLATE/release.md
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
---
|
||||
name: 🎂 Release
|
||||
about: Define a Release
|
||||
labels: release
|
||||
title: 🎂 [RELEASE]
|
||||
---
|
||||
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
|
||||
|
||||
<!-- THIS ISSUE-TYPE IS NOT FOR YOU! -->
|
||||
<!-- Proceed only if you know what you are doing - have a chat with Project's Team first -->
|
||||
|
||||
## 🎂 RELEASE
|
||||
<!-- Describe your Release in detail. Include screenshots and drawings -->
|
||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,3 +1,5 @@
|
||||
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
|
||||
|
||||
## 🍰 Pullrequest
|
||||
<!-- Describe the Pullrequest. Use Screenshots if possible. -->
|
||||
|
||||
|
||||
87
.github/workflows/publish.yml
vendored
87
.github/workflows/publish.yml
vendored
@ -4,7 +4,9 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
# - 4451-new-deployment-with-base-and-code # for testing while developing
|
||||
# - 5059-epic-groups # for testing while developing
|
||||
# template branches in repo
|
||||
# - template--separate-branch-auto-deployment--5059-epic-groups
|
||||
|
||||
jobs:
|
||||
##############################################################################
|
||||
@ -56,9 +58,9 @@ jobs:
|
||||
# NEO4J ##################################################################
|
||||
##########################################################################
|
||||
- name: Neo4J | Build `community` image
|
||||
run: docker build --target community -t "ocelotsocialnetwork/neo4j:latest" -t "ocelotsocialnetwork/neo4j:community" -t "ocelotsocialnetwork/neo4j:${VERSION}" -t "ocelotsocialnetwork/neo4j:${BUILD_VERSION}" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT neo4j/
|
||||
run: docker build --target community -t "ocelotsocialnetwork/neo4j-community:latest" -t "ocelotsocialnetwork/neo4j-community:${VERSION}" -t "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" > /tmp/neo4j.tar
|
||||
run: docker save "ocelotsocialnetwork/neo4j-community" > /tmp/neo4j.tar
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
@ -238,7 +240,7 @@ jobs:
|
||||
- name: login to dockerhub
|
||||
run: echo "${DOCKERHUB_TOKEN}" | docker login -u "${DOCKERHUB_USERNAME}" --password-stdin
|
||||
- name: Push neo4j
|
||||
run: docker push --all-tags ocelotsocialnetwork/neo4j
|
||||
run: docker push --all-tags ocelotsocialnetwork/neo4j-community
|
||||
- name: Push backend
|
||||
run: docker push --all-tags ocelotsocialnetwork/backend
|
||||
- name: Push webapp
|
||||
@ -246,6 +248,81 @@ jobs:
|
||||
- name: Push maintenance
|
||||
run: docker push --all-tags ocelotsocialnetwork/maintenance
|
||||
|
||||
##############################################################################
|
||||
# JOB: KUBERNETES DEPLOY ACTUAL/LATEST VERSION ######################################
|
||||
##############################################################################
|
||||
kubernetes_deploy:
|
||||
# see example https://github.com/do-community/example-doctl-action
|
||||
# see example https://github.com/do-community/example-doctl-action/blob/main/.github/workflows/workflow.yaml
|
||||
name: Kubernetes deploy of latest version to stage.ocelot.social cluster at DigitalOcean
|
||||
runs-on: ubuntu-latest
|
||||
needs: [upload_to_dockerhub]
|
||||
steps:
|
||||
##########################################################################
|
||||
# CHECKOUT CODE ##########################################################
|
||||
##########################################################################
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
##########################################################################
|
||||
# SET ENVS ###############################################################
|
||||
##########################################################################
|
||||
- name: ENV - VERSION
|
||||
run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
|
||||
- name: ENV - BUILD_VERSION
|
||||
run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
|
||||
##########################################################################
|
||||
# Install DigitalOceans doctl and set kubeconfig #########################
|
||||
##########################################################################
|
||||
- name: Install doctl
|
||||
uses: digitalocean/action-doctl@v2
|
||||
with:
|
||||
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
|
||||
- name: Save DigitalOcean kubeconfig with short-lived credentials
|
||||
run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 cluster-stage-ocelot-social
|
||||
##########################################################################
|
||||
# Deploy new Docker images to DigitalOcean Kubernetes cluster ############
|
||||
##########################################################################
|
||||
# - name: Deploy 'latest' to DigitalOcean Kubernetes
|
||||
# run: |
|
||||
# kubectl -n default set image deployment/ocelot-webapp container-ocelot-webapp=ocelotsocialnetwork/webapp:latest
|
||||
# kubectl -n default rollout restart deployment/ocelot-webapp
|
||||
# kubectl -n default set image deployment/ocelot-backend container-ocelot-backend=ocelotsocialnetwork/backend:latest
|
||||
# kubectl -n default rollout restart deployment/ocelot-backend
|
||||
# kubectl -n default set image deployment/ocelot-maintenance container-ocelot-maintenance=ocelotsocialnetwork/maintenance:latest
|
||||
# kubectl -n default rollout restart deployment/ocelot-maintenance
|
||||
# kubectl -n default set image deployment/ocelot-neo4j container-ocelot-neo4j=ocelotsocialnetwork/neo4j-community:latest
|
||||
# kubectl -n default rollout restart deployment/ocelot-neo4j
|
||||
- name: Deploy actual version '$BUILD_VERSION' to DigitalOcean Kubernetes
|
||||
run: |
|
||||
kubectl -n default set image deployment/ocelot-webapp container-ocelot-webapp=ocelotsocialnetwork/webapp:$BUILD_VERSION
|
||||
kubectl -n default rollout restart deployment/ocelot-webapp
|
||||
kubectl -n default set image deployment/ocelot-backend container-ocelot-backend=ocelotsocialnetwork/backend:$BUILD_VERSION
|
||||
kubectl -n default rollout restart deployment/ocelot-backend
|
||||
kubectl -n default set image deployment/ocelot-maintenance container-ocelot-maintenance=ocelotsocialnetwork/maintenance:$BUILD_VERSION
|
||||
kubectl -n default rollout restart deployment/ocelot-maintenance
|
||||
kubectl -n default set image deployment/ocelot-neo4j container-ocelot-neo4j=ocelotsocialnetwork/neo4j-community:$BUILD_VERSION
|
||||
kubectl -n default rollout restart deployment/ocelot-neo4j
|
||||
# because this step 'kubectl -n default rollout status deployment/* --timeout=600s' does not work as expected
|
||||
# and we need the pods to be up again for cleaning and seeding the Neo4j database and the backend.
|
||||
# !!! this is not a perfect solution !!!
|
||||
# deployments are regularly up again after 3 minutes and 10 seconds
|
||||
- name: Sleep for 4 minutes, means 240 seconds
|
||||
run: sleep 240s
|
||||
shell: bash
|
||||
- name: Verify deployment and wait for the pods of each deployment to get ready for cleaning and seeding of the database
|
||||
run: |
|
||||
kubectl -n default rollout status deployment/ocelot-backend --timeout=600s
|
||||
kubectl -n default rollout status deployment/ocelot-neo4j --timeout=600s
|
||||
kubectl -n default rollout status deployment/ocelot-maintenance --timeout=600s
|
||||
kubectl -n default rollout status deployment/ocelot-webapp --timeout=600s
|
||||
- name: Run migrations for Neo4j database via backend for staging
|
||||
run: |
|
||||
kubectl -n default exec -it $(kubectl -n default get pods | grep ocelot-backend | awk '{ print $1 }') -- /bin/sh -c "yarn prod:migrate up"
|
||||
- name: Reset and seed Neo4j database via backend for staging
|
||||
# db cleaning and seeding is only possible in production if env 'PRODUCTION_DB_CLEAN_ALLOW=true' is set in deployment
|
||||
run: |
|
||||
kubectl -n default exec -it $(kubectl -n default get pods | grep ocelot-backend | awk '{ print $1 }') -- /bin/sh -c "node --experimental-repl-await dist/db/clean.js && node --experimental-repl-await dist/db/seed.js"
|
||||
|
||||
##############################################################################
|
||||
# JOB: GITHUB TAG LATEST VERSION #############################################
|
||||
##############################################################################
|
||||
@ -313,4 +390,4 @@ jobs:
|
||||
release_name: ${{ env.VERSION }}
|
||||
body_path: ./CHANGELOG.md
|
||||
draft: false
|
||||
prerelease: false
|
||||
prerelease: false
|
||||
|
||||
8
.github/workflows/test.yml
vendored
8
.github/workflows/test.yml
vendored
@ -43,8 +43,8 @@ jobs:
|
||||
##########################################################################
|
||||
- name: Neo4J | Build `community` image
|
||||
run: |
|
||||
docker build --target community -t "ocelotsocialnetwork/neo4j:community" neo4j/
|
||||
docker save "ocelotsocialnetwork/neo4j:community" > /tmp/neo4j.tar
|
||||
docker build --target community -t "ocelotsocialnetwork/neo4j-community:test" neo4j/
|
||||
docker save "ocelotsocialnetwork/neo4j-community:test" > /tmp/neo4j.tar
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
@ -202,6 +202,8 @@ jobs:
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps neo4j backend
|
||||
- name: backend | Initialize Database
|
||||
run: docker-compose exec -T backend yarn db:migrate init
|
||||
- name: backend | Migrate Database Up
|
||||
run: docker-compose exec -T backend yarn db:migrate up
|
||||
- name: backend | Unit test
|
||||
run: docker-compose exec -T backend yarn test
|
||||
##########################################################################
|
||||
@ -267,7 +269,7 @@ jobs:
|
||||
report_name: Coverage Webapp
|
||||
type: lcov
|
||||
result_path: ./coverage/lcov.info
|
||||
min_coverage: 65
|
||||
min_coverage: 63
|
||||
token: ${{ github.token }}
|
||||
|
||||
##############################################################################
|
||||
|
||||
122
CHANGELOG.md
122
CHANGELOG.md
@ -4,8 +4,130 @@ 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).
|
||||
|
||||
#### [2.2.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.1.0...2.2.0)
|
||||
|
||||
- refactor: 🍰 Refactor Design Of Category Filter In Filter Menu [`#5597`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5597)
|
||||
- feat: 🍰 Footer And Header Links Configurable To Have External Link Target [`#5590`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5590)
|
||||
- feat: 🍰 Header Menu To Component And Other Refinements [`#5546`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5546)
|
||||
- fix: 🍰 Comments On Posts In Groups In Webapp [`#5607`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5607)
|
||||
- docs: 🍰 Add Live Demo To Main Readme [`#5600`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5600)
|
||||
- feat: Restrict Comments on Posts in Groups [`#5599`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5599)
|
||||
- fix: 🍰 Add Space In Register Box [`#5591`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5591)
|
||||
- fix: 🍰 Update Group Avatar After Upload [`#5583`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5583)
|
||||
- feat: 🍰 Make Donation Progress Bar Color Configurable [`#5593`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5593)
|
||||
- fix: 🍰 Bug In Edit Slug Of Group [`#5596`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5596)
|
||||
- fix: 🍰 Fix Group Teaser [`#5584`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5584)
|
||||
- feat: 🍰 List All Groups [`#5582`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5582)
|
||||
- feat: 🍰 Header Logo Routing Update [`#5579`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5579)
|
||||
- chore: 🍰 Release v2.1.0 [`#5574`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5574)
|
||||
- feat: 🍰 EPIC Groups [`#5132`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5132)
|
||||
- chore: 🍰 Remove Group Branchs `5059-epic-groups` Separate Auto-Deployment [`#5552`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5552)
|
||||
- fix: [WIP] 🍰 Long Words Are Being Wrapped Now [`#5559`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5559)
|
||||
- feat: 🍰 Search For Groups [`#5543`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5543)
|
||||
- feat: 🍰 Mobile Footer Menu To Header Menu [`#5524`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5524)
|
||||
- feat: 🍰 Implement `LOGO_HEADER_CLICK` As Configuration [`#5525`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5525)
|
||||
- refactor: 🍰 Category Filter In Filter Menu [`#5527`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5527)
|
||||
- feat: 🍰 Group Invitation [`#5512`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5512)
|
||||
- feat: 🍰 Implement Post In Group In Webapp [`#5468`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5468)
|
||||
- feat: :cake: Update Issue Templates [`#5508`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5508)
|
||||
- refactor: 🍰 Disable Submit Button On group Update Changed Error [`#5489`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5489)
|
||||
- feat: 🍰 Seed Posts In Groups [`#5503`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5503)
|
||||
- feat: 🍰 Make Configurable Header Menu Translatable [`#5491`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5491)
|
||||
- feat: 🍰 Post In Groups [`#5380`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5380)
|
||||
- feat: 🍰 Refine Group Creation And Group Edit [`#5418`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5418)
|
||||
- feat: 🍰 Implement Content Menu On Group Profile [`#5419`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5419)
|
||||
- feat: 🍰 Group Members Management [`#5345`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5345)
|
||||
- feat: 🍰 Have My Groups In The User Menu And Configure Groups Button In Header [`#5411`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5411)
|
||||
- chore: 🍰 Implement Automatic Deployment For Groups Branch '5059-epic-groups' [`#5408`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5408)
|
||||
- Chore: 🍰 Release v1.1.1 – Refactor Rebranding [`#5392`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5392)
|
||||
- chore: 🍰 Refactor Rebranding [`#5390`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5390)
|
||||
- feat: 🍰 Group Profile Description Etc [`#5368`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5368)
|
||||
- feat: 🍰 Tooltips For Topics [`#5350`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5350)
|
||||
- feat: 🍰 Group Profile Members List Etc [`#5335`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5335)
|
||||
- feat: 🍰 Implement Group Profile – Visibility [`#5332`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5332)
|
||||
- feat: 🍰 Save Categories In Frontend [`#5284`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5284)
|
||||
- feat: 🍰 Add New Yunite Icons [`#5319`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5319)
|
||||
- chore: 🍰 Update Neode From v^0.4.7 To v^0.4.8 In Backend [`#5334`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5334)
|
||||
- feat: 🍰 My Groups Page [`#5148`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5148)
|
||||
- feat: 🍰 Hidden Groups Shall Not Be Visible For None Or Pending Members In Backend [`#5317`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5317)
|
||||
- feat: 🍰 Implement Group Profile [`#5197`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5197)
|
||||
- fix: Category Filter Menu Client Only [`#5301`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5301)
|
||||
- feat: Save Category Settings [`#5261`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5261)
|
||||
- feat: 🍰 Implement `UpdateGroup` Resolver [`#5224`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5224)
|
||||
- feat: Topics Menu [`#5248`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5248)
|
||||
- docs: 🍰 Document GraqhQL Playground [`#5253`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5253)
|
||||
- feat: Categories Filter Menu [`#5198`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5198)
|
||||
- feat: 🍰 Implement `JoinGroup`, `GroupMember`, `SwitchGroupMemberRole` Resolvers [`#5199`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5199)
|
||||
- fix: 🍰 Fix Test Description From `enter-nonce.vue` To `change-password` [`#5217`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5217)
|
||||
- Bump cookie-universal-nuxt from 2.1.5 to 2.2.2 in /webapp [`#5218`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5218)
|
||||
- Bump prettier from 2.2.1 to 2.7.1 in /webapp [`#5170`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5170)
|
||||
- Bump eslint-plugin-prettier from 3.1.2 to 3.4.1 in /backend [`#5211`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5211)
|
||||
- Bump slug from 4.0.2 to 6.0.0 in /backend [`#5193`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5193)
|
||||
- chore: 🍰 Fix typo in PULL_REQUEST_TEMPLATE.md file [`#5208`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5208)
|
||||
- Bump slug from 5.1.0 to 6.0.0 [`#5191`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5191)
|
||||
- Bump vue-sweetalert-icons from 4.3.0 to 4.3.1 in /webapp [`#5174`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5174)
|
||||
- feat: 🍰 Change Error Message With `Authorised` To `Authorized` All Over The Place To Have American English [`#5206`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5206)
|
||||
- Bump cross-env from 7.0.2 to 7.0.3 in /webapp [`#5168`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5168)
|
||||
- chore: 🍰 Add `--logHeapUsage` To Jest Test Call [`#5182`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5182)
|
||||
- chore: 🍰 Add Groups To Seeding [`#5185`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5185)
|
||||
- feat: 🍰 Implement Group GQL Model And CRUD Resolvers – First Step [`#5139`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5139)
|
||||
- refactor: 🍰 Rename `UserGroup` To `UserRole` [`#5143`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5143)
|
||||
- improved code and tests as suggested by @tirokk, thanks for the great review! [`631f34a`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/631f34a2e5224d68279337a92e7535794b670d70)
|
||||
- implement and test post visibilty when leaving or changing the role in a group [`76bfe48`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/76bfe484768cf9b20b2dced865d5d3e3eb999235)
|
||||
- comment out LanguagesFilter, EmotionsFilter, fix tests, fix lint [`52dcd77`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/52dcd772fa81e02a0d95e89a9fc8232e70a09d28)
|
||||
|
||||
#### [1.1.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.0.9...1.1.0)
|
||||
|
||||
> 4 August 2022
|
||||
|
||||
- chore: 🍰 Release v1.1.0 - Implement Categories Again [`#5145`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5145)
|
||||
- feat: Make Categories Optional [`#5102`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5102)
|
||||
- Update issue templates [`#5101`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5101)
|
||||
- chore: 🍰 Betters Automatic Deployment To `stage.ocelot.social` On Push To `master` Branch [`#5097`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5097)
|
||||
- add optional categories to teaser and post [`bc95500`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/bc955003f7c33aabe592bee782aca973b4f00cba)
|
||||
- env vatiable for CATEGORIES_ACTIVE and switch for categories in contribution form [`e31f250`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/e31f250ea5e1949f4f08e72fe82622d41ecd85f1)
|
||||
- fix some tests [`5393c2a`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/5393c2aeaaf070a637390c430d5f03057030ff52)
|
||||
|
||||
#### [1.0.9](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.0.8...1.0.9)
|
||||
|
||||
> 20 July 2022
|
||||
|
||||
- chore: 🍰 Release v1.0.9 [`#5095`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5095)
|
||||
- chore: 🍰 Automatic Deployment To `stage.ocelot.social` On Push To `master` Branch [`#5080`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5080)
|
||||
- change footer version-link [`#5091`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5091)
|
||||
- docs: 🍰 Add Neo4j Docu For Important Commands [`#5090`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5090)
|
||||
- 5079 legends for search field [`#5088`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5088)
|
||||
- chore: 🍰 Change `image` Entries In Docker Compose Files And Fix Apple M1 Problem [`#5073`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5073)
|
||||
- chore: 🍰 Rename Neo4j Docker Image In General To `neo4j-community:*` [`#5078`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5078)
|
||||
- chore: 🍰 Fix `ocelotsocialnetwork/webapp:latest` And `ocelotsocialnetwork/backend:latest` On Start In Cluster [`#5076`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5076)
|
||||
- Add documentation for Apple M1 Docker Compose override files [`2f3f37c`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/2f3f37c158cfc9b300540d3c8f016548b15a5277)
|
||||
- Add documentation for Docker build analyzes [`fbbcc5b`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/fbbcc5bb854d53b5fa658b83d56d381a3cbc2b1a)
|
||||
- Implement DigitalOcean Kubernetes deployment on publishing [`485e698`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/485e6986b88a14db5ab75ed12bab5cdc73592ca6)
|
||||
|
||||
#### [1.0.8](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.0.7...1.0.8)
|
||||
|
||||
> 1 July 2022
|
||||
|
||||
- chore: 🍰 Release v1.0.8 [`#5058`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5058)
|
||||
- chore: 🍰 Log E-Mail If Not Sending It [`#5038`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5038)
|
||||
- test: 🍰 Test And Refactor E-Mail Templates [`#4787`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4787)
|
||||
- fix: 🍰 Replace Hashtag In d.tube Url [`#4980`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4980)
|
||||
- Fix location backend test [`#5034`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5034)
|
||||
- fix: 🍰 Add Expiration Date To Cookies Etc. [`#4882`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4882)
|
||||
- feat: 🍰 Refactor Social Media List With Slots [`#4773`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4773)
|
||||
- bug: 🍰 Replace Deleted Faker Package – Change To Import Again [`#4975`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4975)
|
||||
- bug: 🍰 Replace Deleted Faker Package [`#4973`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4973)
|
||||
- fix: 🍰 Change Tip Tap Editor Legend Hover To Click [`#4911`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4911)
|
||||
- fix: 🍰 Fix Embed iframe Width And Height CSS [`#4897`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4897)
|
||||
- Implement MySomethingList for social media, use list item slot [`d3cc49d`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/d3cc49d37ba260f9a285c078c57e673a32a76732)
|
||||
- Split social media page and list component [`b740033`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/b7400339aba22d5fb5506dc3b25f082d7f09edfc)
|
||||
- Remove input addSocialMedia [`58464fd`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/58464fd53ef6aab52af1c2477c2615648ad889e3)
|
||||
|
||||
#### [1.0.7](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.0.6...1.0.7)
|
||||
|
||||
> 2 December 2021
|
||||
|
||||
- fix: 🍰 Fix Migration 'add donations node‘ And Release V1.0.7 [`#4821`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4821)
|
||||
- Bump rosie from 2.0.1 to 2.1.0 [`#4520`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4520)
|
||||
- fix: 🍰 Renew JWT In Decode Test [`#4798`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4798)
|
||||
- docs: 🍰 Refine Main README.md With Test Tech Stack And Video Link [`#4772`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4772)
|
||||
|
||||
86
DOCKER_MORE_CLOSELY.md
Normal file
86
DOCKER_MORE_CLOSELY.md
Normal file
@ -0,0 +1,86 @@
|
||||
# Docker More Closely
|
||||
|
||||
## Apple M1 Platform
|
||||
|
||||
***Attention:** For using Docker commands in Apple M1 environments!*
|
||||
|
||||
### Enviroment Variable For Apple M1 Platform
|
||||
|
||||
To set the Docker platform environment variable in your terminal tab, run:
|
||||
|
||||
```bash
|
||||
# set env variable for your shell
|
||||
$ export DOCKER_DEFAULT_PLATFORM=linux/amd64
|
||||
```
|
||||
|
||||
### Docker Compose Override File For Apple M1 Platform
|
||||
|
||||
For Docker compose `up` or `build` commands, you can use our Apple M1 override file that specifies the M1 platform:
|
||||
|
||||
```bash
|
||||
# in main folder
|
||||
|
||||
# for development
|
||||
$ docker compose -f docker-compose.yml -f docker-compose.override.yml -f docker-compose.apple-m1.override.yml up
|
||||
# only once: init admin user and create indexes and contraints in Neo4j database
|
||||
$ docker compose exec backend yarn prod:migrate init
|
||||
# clean db
|
||||
$ docker compose exec backend yarn db:reset
|
||||
# seed db
|
||||
$ docker compose exec backend yarn db:seed
|
||||
|
||||
# for production
|
||||
$ docker compose -f docker-compose.yml -f docker-compose.apple-m1.override.yml up
|
||||
# only once: init admin user and create indexes and contraints in Neo4j database
|
||||
$ docker compose exec backend /bin/sh -c "yarn prod:migrate init"
|
||||
```
|
||||
|
||||
## Analysing Docker Builds
|
||||
|
||||
To analyze a Docker build, there is a wonderful tool called [dive](https://github.com/wagoodman/dive). Please sponsor if you're using it!
|
||||
|
||||
The `dive build` command is exactly the right one to fulfill what we are looking for.
|
||||
We can use it just like the `docker build` command and get an analysis afterwards.
|
||||
|
||||
So, in our main folder, we use it in the following way:
|
||||
|
||||
```bash
|
||||
# in main folder
|
||||
$ dive build --target <layer-name> -t "ocelotsocialnetwork/<app-name>:local-<layer-name>" --build-arg BBUILD_DATE="<build-date>" --build-arg BBUILD_VERSION="<build-version>" --build-arg BBUILD_COMMIT="<build-commit>" <app-folder-name-or-dot>/
|
||||
```
|
||||
|
||||
The build arguments are optional.
|
||||
|
||||
For the specific applications, we use them as follows.
|
||||
|
||||
### Backend
|
||||
|
||||
#### Production For Backend
|
||||
|
||||
```bash
|
||||
# in main folder
|
||||
$ dive build --target production -t "ocelotsocialnetwork/backend:local-production" backend/
|
||||
```
|
||||
|
||||
#### Development For Backend
|
||||
|
||||
```bash
|
||||
# in main folder
|
||||
$ dive build --target development -t "ocelotsocialnetwork/backend:local-development" backend/
|
||||
```
|
||||
|
||||
### Webapp
|
||||
|
||||
#### Production For Webapp
|
||||
|
||||
```bash
|
||||
# in main folder
|
||||
$ dive build --target production -t "ocelotsocialnetwork/webapp:local-production" webapp/
|
||||
```
|
||||
|
||||
#### Development For Webapp
|
||||
|
||||
```bash
|
||||
# in main folder
|
||||
$ dive build --target development -t "ocelotsocialnetwork/webapp:local-development" webapp/
|
||||
```
|
||||
13
README.md
13
README.md
@ -65,11 +65,18 @@ Change into the new folder.
|
||||
$ cd Ocelot-Social
|
||||
```
|
||||
|
||||
## Live Demo And Developer Logins
|
||||
|
||||
**Try out our deployed [development environment](https://stage.ocelot.social).**
|
||||
|
||||
Visit our staging networks:
|
||||
|
||||
* central staging network: [stage.ocelot.social](https://stage.ocelot.social)
|
||||
<!-- - rebranded staging network: [rebrand.ocelot.social](https://stage.ocelot.social). -->
|
||||
|
||||
### Login
|
||||
|
||||
<!-- Try out our deployed [development environment](https://develop.human-connection.org/). -->
|
||||
|
||||
Logins in the browser after the following installations:
|
||||
Logins for the live demos and developers (local developers after the following installations) in the browser:
|
||||
|
||||
| email | password | role |
|
||||
| :--- | :--- | :--- |
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
* [End-to-end tests](cypress/README.md)
|
||||
* [Frontend tests](webapp/testing.md)
|
||||
* [Backend tests](backend/testing.md)
|
||||
* [Docker More Closely](DOCKER_MORE_CLOSELY.md)
|
||||
* [Deployment](https://github.com/Ocelot-Social-Community/Ocelot-Social-Deploy-Rebranding/blob/master/deployment/README.md)
|
||||
* [Contributing](CONTRIBUTING.md)
|
||||
* [Feature Specification](cypress/features.md)
|
||||
|
||||
@ -28,3 +28,5 @@ AWS_BUCKET=
|
||||
|
||||
EMAIL_DEFAULT_SENDER="devops@ocelot.social"
|
||||
EMAIL_SUPPORT="devops@ocelot.social"
|
||||
|
||||
CATEGORIES_ACTIVE=false
|
||||
|
||||
@ -91,6 +91,7 @@ FROM base as production
|
||||
|
||||
# Copy "binary"-files from build image
|
||||
COPY --from=build ${DOCKER_WORKDIR}/dist ./dist
|
||||
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/
|
||||
|
||||
@ -7,8 +7,9 @@ Run the following command to install everything through docker.
|
||||
The installation takes a bit longer on the first pass or on rebuild ...
|
||||
|
||||
```bash
|
||||
# in main folder
|
||||
$ docker-compose up
|
||||
|
||||
# or
|
||||
# rebuild the containers for a cleanup
|
||||
$ docker-compose up --build
|
||||
```
|
||||
@ -28,6 +29,7 @@ between different local node versions.
|
||||
Install node dependencies with [yarn](https://yarnpkg.com/en/):
|
||||
|
||||
```bash
|
||||
# in main folder
|
||||
$ cd backend
|
||||
$ yarn install
|
||||
```
|
||||
@ -45,12 +47,14 @@ a [local Neo4J](http://localhost:7474) instance is up and running.
|
||||
Start the backend for development with:
|
||||
|
||||
```bash
|
||||
# in backend/
|
||||
$ yarn run dev
|
||||
```
|
||||
|
||||
or start the backend in production environment with:
|
||||
|
||||
```bash
|
||||
# in backend/
|
||||
$ yarn run start
|
||||
```
|
||||
|
||||
@ -60,6 +64,7 @@ your `.env` configuration file.
|
||||
Your backend is up and running at [http://localhost:4000/](http://localhost:4000/)
|
||||
This will start the GraphQL service \(by default on localhost:4000\) where you
|
||||
can issue GraphQL requests or access GraphQL Playground in the browser.
|
||||
More details about our GraphQL playground and how to use it with ocelot.social can be found [here](./src/graphql/GraphQL-Playground.md).
|
||||
|
||||

|
||||
|
||||
@ -72,6 +77,7 @@ backend is running:
|
||||
{% tab title="Docker" %}
|
||||
|
||||
```bash
|
||||
# in main folder while docker-compose is running
|
||||
$ docker-compose exec backend yarn run db:migrate init
|
||||
```
|
||||
|
||||
@ -79,7 +85,7 @@ $ docker-compose exec backend yarn run db:migrate init
|
||||
{% tab title="Without Docker" %}
|
||||
|
||||
```bash
|
||||
# in folder backend/
|
||||
# in folder backend/ while database is running
|
||||
# make sure your database is running on http://localhost:7474/browser/
|
||||
yarn run db:migrate init
|
||||
```
|
||||
@ -98,12 +104,14 @@ need to seed your database:
|
||||
In another terminal run:
|
||||
|
||||
```bash
|
||||
# in main folder while docker-compose is running
|
||||
$ docker-compose exec backend yarn run db:seed
|
||||
```
|
||||
|
||||
To reset the database run:
|
||||
|
||||
```bash
|
||||
# in main folder while docker-compose is running
|
||||
$ docker-compose exec backend yarn run db:reset
|
||||
# you could also wipe out your neo4j database and delete all volumes with:
|
||||
$ docker-compose down -v
|
||||
@ -117,12 +125,14 @@ $ docker-compose exec backend yarn run db:migrate init
|
||||
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
|
||||
```
|
||||
|
||||
@ -140,6 +150,7 @@ you have to migrate your data e.g. because your data modeling has changed.
|
||||
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/
|
||||
```
|
||||
@ -147,6 +158,7 @@ $ docker-compose exec backend yarn run db:migrate:create your_data_migration
|
||||
To run the migration:
|
||||
|
||||
```bash
|
||||
# in main folder while docker-compose is running
|
||||
$ docker-compose exec backend yarn run db:migrate up
|
||||
```
|
||||
|
||||
@ -156,6 +168,7 @@ $ docker-compose exec backend yarn run db:migrate up
|
||||
Generate a data migration file:
|
||||
|
||||
```bash
|
||||
# in backend/
|
||||
$ yarn run db:migrate:create your_data_migration
|
||||
# Edit the file in ./src/db/migrations/
|
||||
```
|
||||
@ -163,6 +176,7 @@ $ yarn run db:migrate:create your_data_migration
|
||||
To run the migration:
|
||||
|
||||
```bash
|
||||
# in backend/ while database is running
|
||||
$ yarn run db:migrate up
|
||||
```
|
||||
|
||||
@ -180,6 +194,7 @@ database after each test, running the tests will wipe out all your data!
|
||||
Run the unit tests:
|
||||
|
||||
```bash
|
||||
# in main folder while docker-compose is running
|
||||
$ docker-compose exec backend yarn run test
|
||||
```
|
||||
|
||||
@ -190,6 +205,7 @@ $ docker-compose exec backend yarn run test
|
||||
Run the unit tests:
|
||||
|
||||
```bash
|
||||
# in backend/ while database is running
|
||||
$ yarn run test
|
||||
```
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ocelot-social-backend",
|
||||
"version": "1.0.7",
|
||||
"version": "2.2.0",
|
||||
"description": "GraphQL Backend for ocelot.social",
|
||||
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
|
||||
"author": "ocelot.social Community",
|
||||
@ -15,7 +15,7 @@
|
||||
"dev": "nodemon --exec babel-node src/ -e js,gql",
|
||||
"dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/ -e js,gql",
|
||||
"lint": "eslint src --config .eslintrc.js",
|
||||
"test": "cross-env NODE_ENV=test jest --forceExit --detectOpenHandles --runInBand --coverage",
|
||||
"test": "cross-env NODE_ENV=test NODE_OPTIONS=--max-old-space-size=8192 jest --forceExit --detectOpenHandles --runInBand --coverage --logHeapUsage",
|
||||
"db:clean": "babel-node src/db/clean.js",
|
||||
"db:reset": "yarn run db:clean",
|
||||
"db:seed": "babel-node src/db/seed.js",
|
||||
@ -103,14 +103,14 @@
|
||||
"mustache": "^4.2.0",
|
||||
"neo4j-driver": "^4.0.2",
|
||||
"neo4j-graphql-js": "^2.11.5",
|
||||
"neode": "^0.4.7",
|
||||
"neode": "^0.4.8",
|
||||
"node-fetch": "~2.6.1",
|
||||
"nodemailer": "^6.4.4",
|
||||
"nodemailer-html-to-text": "^3.2.0",
|
||||
"npm-run-all": "~4.1.5",
|
||||
"request": "~2.88.2",
|
||||
"sanitize-html": "~1.22.0",
|
||||
"slug": "~4.0.2",
|
||||
"slug": "~6.0.0",
|
||||
"subscriptions-transport-ws": "^0.9.19",
|
||||
"trunc-html": "~1.1.2",
|
||||
"uuid": "~8.3.2",
|
||||
@ -129,7 +129,7 @@
|
||||
"eslint-plugin-import": "~2.20.2",
|
||||
"eslint-plugin-jest": "~23.8.2",
|
||||
"eslint-plugin-node": "~11.1.0",
|
||||
"eslint-plugin-prettier": "~3.1.2",
|
||||
"eslint-plugin-prettier": "~3.4.1",
|
||||
"eslint-plugin-promise": "~4.3.1",
|
||||
"eslint-plugin-standard": "~4.0.1",
|
||||
"jest": "~25.3.0",
|
||||
|
||||
@ -22,6 +22,8 @@ const environment = {
|
||||
DEBUG: env.NODE_ENV !== 'production' && env.DEBUG,
|
||||
TEST: env.NODE_ENV === 'test',
|
||||
PRODUCTION: env.NODE_ENV === 'production',
|
||||
// used for staging enviroments if 'PRODUCTION=true' and 'PRODUCTION_DB_CLEAN_ALLOW=true'
|
||||
PRODUCTION_DB_CLEAN_ALLOW: env.PRODUCTION_DB_CLEAN_ALLOW === 'true' || false, // default = false
|
||||
DISABLED_MIDDLEWARES: (env.NODE_ENV !== 'production' && env.DISABLED_MIDDLEWARES) || false,
|
||||
}
|
||||
|
||||
@ -84,6 +86,7 @@ const options = {
|
||||
ORGANIZATION_URL: emails.ORGANIZATION_LINK,
|
||||
PUBLIC_REGISTRATION: env.PUBLIC_REGISTRATION === 'true' || false,
|
||||
INVITE_REGISTRATION: env.INVITE_REGISTRATION !== 'false', // default = true
|
||||
CATEGORIES_ACTIVE: process.env.CATEGORIES_ACTIVE === 'true' || false,
|
||||
}
|
||||
|
||||
// Check if all required configs are present
|
||||
|
||||
102
backend/src/constants/categories.js
Normal file
102
backend/src/constants/categories.js
Normal file
@ -0,0 +1,102 @@
|
||||
// this file is duplicated in `backend/src/constants/metadata.js` and `webapp/constants/metadata.js`
|
||||
export const CATEGORIES_MIN = 1
|
||||
export const CATEGORIES_MAX = 3
|
||||
|
||||
export const categories = [
|
||||
{
|
||||
icon: 'networking',
|
||||
name: 'networking',
|
||||
description: 'Kooperation, Aktionsbündnisse, Solidarität, Hilfe',
|
||||
},
|
||||
{
|
||||
icon: 'home',
|
||||
name: 'home',
|
||||
description: 'Bauen, Lebensgemeinschaften, Tiny Houses, Gemüsegarten',
|
||||
},
|
||||
{
|
||||
icon: 'energy',
|
||||
name: 'energy',
|
||||
description: 'Öl, Gas, Kohle, Wind, Wasserkraft, Biogas, Atomenergie, ...',
|
||||
},
|
||||
{
|
||||
icon: 'psyche',
|
||||
name: 'psyche',
|
||||
description: 'Seele, Gefühle, Glück',
|
||||
},
|
||||
{
|
||||
icon: 'movement',
|
||||
name: 'body-and-excercise',
|
||||
description: 'Sport, Yoga, Massage, Tanzen, Entspannung',
|
||||
},
|
||||
{
|
||||
icon: 'balance-scale',
|
||||
name: 'law',
|
||||
description: 'Menschenrechte, Gesetze, Verordnungen',
|
||||
},
|
||||
{
|
||||
icon: 'finance',
|
||||
name: 'finance',
|
||||
description: 'Geld, Finanzsystem, Alternativwährungen, ...',
|
||||
},
|
||||
{
|
||||
icon: 'child',
|
||||
name: 'children',
|
||||
description: 'Familie, Pädagogik, Schule, Prägung',
|
||||
},
|
||||
{
|
||||
icon: 'mobility',
|
||||
name: 'mobility',
|
||||
description: 'Reise, Verkehr, Elektromobilität',
|
||||
},
|
||||
{
|
||||
icon: 'shopping-cart',
|
||||
name: 'economy',
|
||||
description: 'Handel, Konsum, Marketing, Lebensmittel, Lieferketten, ...',
|
||||
},
|
||||
{
|
||||
icon: 'peace',
|
||||
name: 'peace',
|
||||
description: 'Krieg, Militär, soziale Verteidigung, Waffen, Cyberattacken',
|
||||
},
|
||||
{
|
||||
icon: 'politics',
|
||||
name: 'politics',
|
||||
description: 'Demokratie, Mitbestimmung, Wahlen, Korruption, Parteien',
|
||||
},
|
||||
{
|
||||
icon: 'nature',
|
||||
name: 'nature',
|
||||
description: 'Tiere, Pflanzen, Landwirtschaft, Ökologie, Artenvielfalt',
|
||||
},
|
||||
{
|
||||
icon: 'science',
|
||||
name: 'science',
|
||||
description: 'Bildung, Hochschule, Publikationen, ...',
|
||||
},
|
||||
{
|
||||
icon: 'health',
|
||||
name: 'health',
|
||||
description: 'Medizin, Ernährung, WHO, Impfungen, Schadstoffe, ...',
|
||||
},
|
||||
{
|
||||
icon: 'media',
|
||||
name: 'it-and-media',
|
||||
description:
|
||||
'Nachrichten, Manipulation, Datenschutz, Überwachung, Datenkraken, AI, Software, Apps',
|
||||
},
|
||||
{
|
||||
icon: 'spirituality',
|
||||
name: 'spirituality',
|
||||
description: 'Religion, Werte, Ethik',
|
||||
},
|
||||
{
|
||||
icon: 'culture',
|
||||
name: 'culture',
|
||||
description: 'Kunst, Theater, Musik, Fotografie, Film',
|
||||
},
|
||||
{
|
||||
icon: 'miscellaneous',
|
||||
name: 'miscellaneous',
|
||||
description: '',
|
||||
},
|
||||
]
|
||||
3
backend/src/constants/groups.js
Normal file
3
backend/src/constants/groups.js
Normal file
@ -0,0 +1,3 @@
|
||||
// this file is duplicated in `backend/src/constants/group.js` and `webapp/constants/group.js`
|
||||
export const DESCRIPTION_WITHOUT_HTML_LENGTH_MIN = 100 // with removed HTML tags
|
||||
export const DESCRIPTION_EXCERPT_HTML_LENGTH = 250 // with removed HTML tags
|
||||
@ -1,8 +1,8 @@
|
||||
import { cleanDatabase } from '../db/factories'
|
||||
import CONFIG from '../config'
|
||||
import { cleanDatabase } from '../db/factories'
|
||||
|
||||
if (CONFIG.PRODUCTION) {
|
||||
throw new Error(`You cannot clean the database in production environment!`)
|
||||
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 () {
|
||||
|
||||
30
backend/src/db/graphql/authentications.js
Normal file
30
backend/src/db/graphql/authentications.js
Normal file
@ -0,0 +1,30 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
// ------ mutations
|
||||
|
||||
export const signupVerificationMutation = gql`
|
||||
mutation (
|
||||
$password: String!
|
||||
$email: String!
|
||||
$name: String!
|
||||
$slug: String
|
||||
$nonce: String!
|
||||
$termsAndConditionsAgreedVersion: String!
|
||||
) {
|
||||
SignupVerification(
|
||||
email: $email
|
||||
password: $password
|
||||
name: $name
|
||||
slug: $slug
|
||||
nonce: $nonce
|
||||
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
|
||||
) {
|
||||
id
|
||||
slug
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// ------ queries
|
||||
|
||||
// fill queries in here
|
||||
15
backend/src/db/graphql/comments.js
Normal file
15
backend/src/db/graphql/comments.js
Normal file
@ -0,0 +1,15 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
// ------ mutations
|
||||
|
||||
export const createCommentMutation = gql`
|
||||
mutation ($id: ID, $postId: ID!, $content: String!) {
|
||||
CreateComment(id: $id, postId: $postId, content: $content) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// ------ queries
|
||||
|
||||
// fill queries in here
|
||||
203
backend/src/db/graphql/groups.js
Normal file
203
backend/src/db/graphql/groups.js
Normal file
@ -0,0 +1,203 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
// ------ mutations
|
||||
|
||||
export const createGroupMutation = () => {
|
||||
return gql`
|
||||
mutation (
|
||||
$id: ID
|
||||
$name: String!
|
||||
$slug: String
|
||||
$about: String
|
||||
$description: String!
|
||||
$groupType: GroupType!
|
||||
$actionRadius: GroupActionRadius!
|
||||
$categoryIds: [ID]
|
||||
$locationName: String # empty string '' sets it to null
|
||||
) {
|
||||
CreateGroup(
|
||||
id: $id
|
||||
name: $name
|
||||
slug: $slug
|
||||
about: $about
|
||||
description: $description
|
||||
groupType: $groupType
|
||||
actionRadius: $actionRadius
|
||||
categoryIds: $categoryIds
|
||||
locationName: $locationName
|
||||
) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
createdAt
|
||||
updatedAt
|
||||
disabled
|
||||
deleted
|
||||
about
|
||||
description
|
||||
descriptionExcerpt
|
||||
groupType
|
||||
actionRadius
|
||||
categories {
|
||||
id
|
||||
slug
|
||||
name
|
||||
icon
|
||||
}
|
||||
locationName
|
||||
location {
|
||||
name
|
||||
nameDE
|
||||
nameEN
|
||||
}
|
||||
myRole
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const updateGroupMutation = () => {
|
||||
return gql`
|
||||
mutation (
|
||||
$id: ID!
|
||||
$name: String
|
||||
$slug: String
|
||||
$about: String
|
||||
$description: String
|
||||
$actionRadius: GroupActionRadius
|
||||
$categoryIds: [ID]
|
||||
$avatar: ImageInput
|
||||
$locationName: String # empty string '' sets it to null
|
||||
) {
|
||||
UpdateGroup(
|
||||
id: $id
|
||||
name: $name
|
||||
slug: $slug
|
||||
about: $about
|
||||
description: $description
|
||||
actionRadius: $actionRadius
|
||||
categoryIds: $categoryIds
|
||||
avatar: $avatar
|
||||
locationName: $locationName
|
||||
) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
createdAt
|
||||
updatedAt
|
||||
disabled
|
||||
deleted
|
||||
about
|
||||
description
|
||||
descriptionExcerpt
|
||||
groupType
|
||||
actionRadius
|
||||
categories {
|
||||
id
|
||||
slug
|
||||
name
|
||||
icon
|
||||
}
|
||||
# avatar # test this as result
|
||||
locationName
|
||||
location {
|
||||
name
|
||||
nameDE
|
||||
nameEN
|
||||
}
|
||||
myRole
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const joinGroupMutation = () => {
|
||||
return gql`
|
||||
mutation ($groupId: ID!, $userId: ID!) {
|
||||
JoinGroup(groupId: $groupId, userId: $userId) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
myRoleInGroup
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const leaveGroupMutation = () => {
|
||||
return gql`
|
||||
mutation ($groupId: ID!, $userId: ID!) {
|
||||
LeaveGroup(groupId: $groupId, userId: $userId) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
myRoleInGroup
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const changeGroupMemberRoleMutation = () => {
|
||||
return gql`
|
||||
mutation ($groupId: ID!, $userId: ID!, $roleInGroup: GroupMemberRole!) {
|
||||
ChangeGroupMemberRole(groupId: $groupId, userId: $userId, roleInGroup: $roleInGroup) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
myRoleInGroup
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
// ------ queries
|
||||
|
||||
export const groupQuery = () => {
|
||||
return gql`
|
||||
query ($isMember: Boolean, $id: ID, $slug: String) {
|
||||
Group(isMember: $isMember, id: $id, slug: $slug) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
createdAt
|
||||
updatedAt
|
||||
disabled
|
||||
deleted
|
||||
about
|
||||
description
|
||||
descriptionExcerpt
|
||||
groupType
|
||||
actionRadius
|
||||
categories {
|
||||
id
|
||||
slug
|
||||
name
|
||||
icon
|
||||
}
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
locationName
|
||||
location {
|
||||
name
|
||||
nameDE
|
||||
nameEN
|
||||
}
|
||||
myRole
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const groupMembersQuery = () => {
|
||||
return gql`
|
||||
query ($id: ID!) {
|
||||
GroupMembers(id: $id) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
myRoleInGroup
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
88
backend/src/db/graphql/posts.js
Normal file
88
backend/src/db/graphql/posts.js
Normal file
@ -0,0 +1,88 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
// ------ mutations
|
||||
|
||||
export const createPostMutation = () => {
|
||||
return gql`
|
||||
mutation (
|
||||
$id: ID
|
||||
$title: String!
|
||||
$slug: String
|
||||
$content: String!
|
||||
$categoryIds: [ID]
|
||||
$groupId: ID
|
||||
) {
|
||||
CreatePost(
|
||||
id: $id
|
||||
title: $title
|
||||
slug: $slug
|
||||
content: $content
|
||||
categoryIds: $categoryIds
|
||||
groupId: $groupId
|
||||
) {
|
||||
id
|
||||
slug
|
||||
title
|
||||
content
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
// ------ queries
|
||||
|
||||
export const postQuery = () => {
|
||||
return gql`
|
||||
query Post($id: ID!) {
|
||||
Post(id: $id) {
|
||||
id
|
||||
title
|
||||
content
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const filterPosts = () => {
|
||||
return gql`
|
||||
query Post($filter: _PostFilter, $first: Int, $offset: Int, $orderBy: [_PostOrdering]) {
|
||||
Post(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) {
|
||||
id
|
||||
title
|
||||
content
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const profilePagePosts = () => {
|
||||
return gql`
|
||||
query profilePagePosts(
|
||||
$filter: _PostFilter
|
||||
$first: Int
|
||||
$offset: Int
|
||||
$orderBy: [_PostOrdering]
|
||||
) {
|
||||
profilePagePosts(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) {
|
||||
id
|
||||
title
|
||||
content
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const searchPosts = () => {
|
||||
return gql`
|
||||
query ($query: String!, $firstPosts: Int, $postsOffset: Int) {
|
||||
searchPosts(query: $query, firstPosts: $firstPosts, postsOffset: $postsOffset) {
|
||||
postCount
|
||||
posts {
|
||||
id
|
||||
title
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
13
backend/src/db/graphql/userManagement.js
Normal file
13
backend/src/db/graphql/userManagement.js
Normal file
@ -0,0 +1,13 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
// ------ mutations
|
||||
|
||||
export const loginMutation = gql`
|
||||
mutation ($email: String!, $password: String!) {
|
||||
login(email: $email, password: $password)
|
||||
}
|
||||
`
|
||||
|
||||
// ------ queries
|
||||
|
||||
// fill queries in here
|
||||
@ -1,6 +1,8 @@
|
||||
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',
|
||||
@ -10,6 +12,29 @@ const defaultAdmin = {
|
||||
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')
|
||||
@ -45,7 +70,7 @@ const createDefaultAdminUser = async (session) => {
|
||||
})
|
||||
try {
|
||||
await createAdminTxResultPromise
|
||||
console.log('Successfully created default admin user') // eslint-disable-line no-console
|
||||
console.log('Successfully created default admin user!') // eslint-disable-line no-console
|
||||
} catch (error) {
|
||||
console.log(error) // eslint-disable-line no-console
|
||||
}
|
||||
@ -58,12 +83,13 @@ class Store {
|
||||
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
|
||||
await txc.run('CALL apoc.schema.assert({},{},true)') // drop all indices and contraints
|
||||
return Promise.all(
|
||||
[
|
||||
'CALL db.index.fulltext.createNodeIndex("post_fulltext_search",["Post"],["title", "content"])',
|
||||
'CALL db.index.fulltext.createNodeIndex("user_fulltext_search",["User"],["name", "slug"])',
|
||||
'CALL db.index.fulltext.createNodeIndex("post_fulltext_search",["Post"],["title", "content"])',
|
||||
'CALL db.index.fulltext.createNodeIndex("tag_fulltext_search",["Tag"],["id"])',
|
||||
].map((statement) => txc.run(statement)),
|
||||
)
|
||||
|
||||
@ -0,0 +1,66 @@
|
||||
import { getDriver } from '../../db/neo4j'
|
||||
|
||||
export const description = `
|
||||
We introduced a new node label 'Group' and we need two primary keys 'id' and 'slug' for it.
|
||||
Additional we like to have fulltext indices the keys 'name', 'slug', 'about', and 'description'.
|
||||
`
|
||||
|
||||
export async function up(next) {
|
||||
const driver = getDriver()
|
||||
const session = driver.session()
|
||||
const transaction = session.beginTransaction()
|
||||
|
||||
try {
|
||||
// Implement your migration here.
|
||||
await transaction.run(`
|
||||
CREATE CONSTRAINT ON ( group:Group ) ASSERT group.id IS UNIQUE
|
||||
`)
|
||||
await transaction.run(`
|
||||
CREATE CONSTRAINT ON ( group:Group ) ASSERT group.slug IS UNIQUE
|
||||
`)
|
||||
await transaction.run(`
|
||||
CALL db.index.fulltext.createNodeIndex("group_fulltext_search",["Group"],["name", "slug", "about", "description"])
|
||||
`)
|
||||
await transaction.commit()
|
||||
next()
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error)
|
||||
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(`
|
||||
DROP CONSTRAINT ON ( group:Group ) ASSERT group.id IS UNIQUE
|
||||
`)
|
||||
await transaction.run(`
|
||||
DROP CONSTRAINT ON ( group:Group ) ASSERT group.slug IS UNIQUE
|
||||
`)
|
||||
await transaction.run(`
|
||||
CALL db.index.fulltext.drop("group_fulltext_search")
|
||||
`)
|
||||
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()
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,22 @@
|
||||
import sample from 'lodash/sample'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import CONFIG from '../config'
|
||||
import createServer from '../server'
|
||||
import faker from '@faker-js/faker'
|
||||
import Factory from '../db/factories'
|
||||
import { getNeode, getDriver } from '../db/neo4j'
|
||||
import { gql } from '../helpers/jest'
|
||||
import {
|
||||
createGroupMutation,
|
||||
joinGroupMutation,
|
||||
changeGroupMemberRoleMutation,
|
||||
} from './graphql/groups'
|
||||
import { createPostMutation } from './graphql/posts'
|
||||
import { createCommentMutation } from './graphql/comments'
|
||||
import { categories } from '../constants/categories'
|
||||
|
||||
if (CONFIG.PRODUCTION && !CONFIG.PRODUCTION_DB_CLEAN_ALLOW) {
|
||||
throw new Error(`You cannot seed the database in a non-staging and real production environment!`)
|
||||
}
|
||||
|
||||
const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
|
||||
@ -262,104 +274,16 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
dagobert.relateTo(louie, 'blocked'),
|
||||
])
|
||||
|
||||
await Promise.all([
|
||||
Factory.build('category', {
|
||||
id: 'cat1',
|
||||
name: 'Just For Fun',
|
||||
slug: 'just-for-fun',
|
||||
icon: 'smile',
|
||||
await Promise.all(
|
||||
categories.map(({ icon, name }, index) => {
|
||||
Factory.build('category', {
|
||||
id: `cat${index + 1}`,
|
||||
slug: name,
|
||||
name,
|
||||
icon,
|
||||
})
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat2',
|
||||
name: 'Happiness & Values',
|
||||
slug: 'happiness-values',
|
||||
icon: 'heart-o',
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat3',
|
||||
name: 'Health & Wellbeing',
|
||||
slug: 'health-wellbeing',
|
||||
icon: 'medkit',
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat4',
|
||||
name: 'Environment & Nature',
|
||||
slug: 'environment-nature',
|
||||
icon: 'tree',
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat5',
|
||||
name: 'Animal Protection',
|
||||
slug: 'animal-protection',
|
||||
icon: 'paw',
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat6',
|
||||
name: 'Human Rights & Justice',
|
||||
slug: 'human-rights-justice',
|
||||
icon: 'balance-scale',
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat7',
|
||||
name: 'Education & Sciences',
|
||||
slug: 'education-sciences',
|
||||
icon: 'graduation-cap',
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat8',
|
||||
name: 'Cooperation & Development',
|
||||
slug: 'cooperation-development',
|
||||
icon: 'users',
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat9',
|
||||
name: 'Democracy & Politics',
|
||||
slug: 'democracy-politics',
|
||||
icon: 'university',
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat10',
|
||||
name: 'Economy & Finances',
|
||||
slug: 'economy-finances',
|
||||
icon: 'money',
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat11',
|
||||
name: 'Energy & Technology',
|
||||
slug: 'energy-technology',
|
||||
icon: 'flash',
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat12',
|
||||
name: 'IT, Internet & Data Privacy',
|
||||
slug: 'it-internet-data-privacy',
|
||||
icon: 'mouse-pointer',
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat13',
|
||||
name: 'Art, Culture & Sport',
|
||||
slug: 'art-culture-sport',
|
||||
icon: 'paint-brush',
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat14',
|
||||
name: 'Freedom of Speech',
|
||||
slug: 'freedom-of-speech',
|
||||
icon: 'bullhorn',
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat15',
|
||||
name: 'Consumption & Sustainability',
|
||||
slug: 'consumption-sustainability',
|
||||
icon: 'shopping-cart',
|
||||
}),
|
||||
Factory.build('category', {
|
||||
id: 'cat16',
|
||||
name: 'Global Peace & Nonviolence',
|
||||
slug: 'global-peace-nonviolence',
|
||||
icon: 'angellist',
|
||||
}),
|
||||
])
|
||||
)
|
||||
|
||||
const [environment, nature, democracy, freedom] = await Promise.all([
|
||||
Factory.build('tag', {
|
||||
@ -376,6 +300,302 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
}),
|
||||
])
|
||||
|
||||
// Create Groups
|
||||
|
||||
authenticatedUser = await peterLustig.toJson()
|
||||
await Promise.all([
|
||||
mutate({
|
||||
mutation: createGroupMutation(),
|
||||
variables: {
|
||||
id: 'g0',
|
||||
name: 'Investigative Journalism',
|
||||
about: 'Investigative journalists share ideas and insights and can collaborate.',
|
||||
description: `<p class=""><em>English:</em></p><p class="">This group is hidden.</p><h3>What is our group for?</h3><p>This group was created to allow investigative journalists to share and collaborate.</p><h3>How does it work?</h3><p>Here you can internally share posts and comments about them.</p><p><br></p><p><em>Deutsch:</em></p><p class="">Diese Gruppe ist verborgen.</p><h3>Wofür ist unsere Gruppe?</h3><p class="">Diese Gruppe wurde geschaffen, um investigativen Journalisten den Austausch und die Zusammenarbeit zu ermöglichen.</p><h3>Wie funktioniert das?</h3><p class="">Hier könnt ihr euch intern über Beiträge und Kommentare zu ihnen austauschen.</p>`,
|
||||
groupType: 'hidden',
|
||||
actionRadius: 'global',
|
||||
categoryIds: ['cat6', 'cat12', 'cat16'],
|
||||
locationName: 'Hamburg, Germany',
|
||||
},
|
||||
}),
|
||||
])
|
||||
await Promise.all([
|
||||
mutate({
|
||||
mutation: joinGroupMutation(),
|
||||
variables: {
|
||||
groupId: 'g0',
|
||||
userId: 'u2',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: joinGroupMutation(),
|
||||
variables: {
|
||||
groupId: 'g0',
|
||||
userId: 'u4',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: joinGroupMutation(),
|
||||
variables: {
|
||||
groupId: 'g0',
|
||||
userId: 'u6',
|
||||
},
|
||||
}),
|
||||
])
|
||||
await Promise.all([
|
||||
mutate({
|
||||
mutation: changeGroupMemberRoleMutation(),
|
||||
variables: {
|
||||
groupId: 'g0',
|
||||
userId: 'u2',
|
||||
roleInGroup: 'usual',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: changeGroupMemberRoleMutation(),
|
||||
variables: {
|
||||
groupId: 'g0',
|
||||
userId: 'u4',
|
||||
roleInGroup: 'admin',
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
// post into group
|
||||
await Promise.all([
|
||||
mutate({
|
||||
mutation: createPostMutation(),
|
||||
variables: {
|
||||
id: 'p0-g0',
|
||||
groupId: 'g0',
|
||||
title: `What happend in Shanghai?`,
|
||||
content: 'A sack of rise dropped in Shanghai. Should we further investigate?',
|
||||
categoryIds: ['cat6'],
|
||||
},
|
||||
}),
|
||||
])
|
||||
authenticatedUser = await bobDerBaumeister.toJson()
|
||||
await Promise.all([
|
||||
mutate({
|
||||
mutation: createPostMutation(),
|
||||
variables: {
|
||||
id: 'p1-g0',
|
||||
groupId: 'g0',
|
||||
title: `The man on the moon`,
|
||||
content: 'We have to further investigate about the stories of a man living on the moon.',
|
||||
categoryIds: ['cat12', 'cat16'],
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
authenticatedUser = await jennyRostock.toJson()
|
||||
await Promise.all([
|
||||
mutate({
|
||||
mutation: createGroupMutation(),
|
||||
variables: {
|
||||
id: 'g1',
|
||||
name: 'School For Citizens',
|
||||
about: 'Our children shall receive education for life.',
|
||||
description: `<p class=""><em>English</em></p><h3>Our goal</h3><p>Only those who enjoy learning and do not lose their curiosity can obtain a good education for life and continue to learn with joy throughout their lives.</p><h3>Curiosity</h3><p>For this we need a school that takes up the curiosity of the children, the people, and satisfies it through a lot of experience.</p><p><br></p><p><em>Deutsch</em></p><h3>Unser Ziel</h3><p class="">Nur wer Spaß am Lernen hat und seine Neugier nicht verliert, kann gute Bildung für's Leben erlangen und sein ganzes Leben mit Freude weiter lernen.</p><h3>Neugier</h3><p class="">Dazu benötigen wir eine Schule, die die Neugier der Kinder, der Menschen, aufnimmt und durch viel Erfahrung befriedigt.</p>`,
|
||||
groupType: 'closed',
|
||||
actionRadius: 'national',
|
||||
categoryIds: ['cat8', 'cat14'],
|
||||
locationName: 'France',
|
||||
},
|
||||
}),
|
||||
])
|
||||
await Promise.all([
|
||||
mutate({
|
||||
mutation: joinGroupMutation(),
|
||||
variables: {
|
||||
groupId: 'g1',
|
||||
userId: 'u1',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: joinGroupMutation(),
|
||||
variables: {
|
||||
groupId: 'g1',
|
||||
userId: 'u2',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: joinGroupMutation(),
|
||||
variables: {
|
||||
groupId: 'g1',
|
||||
userId: 'u5',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: joinGroupMutation(),
|
||||
variables: {
|
||||
groupId: 'g1',
|
||||
userId: 'u6',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: joinGroupMutation(),
|
||||
variables: {
|
||||
groupId: 'g1',
|
||||
userId: 'u7',
|
||||
},
|
||||
}),
|
||||
])
|
||||
await Promise.all([
|
||||
mutate({
|
||||
mutation: changeGroupMemberRoleMutation(),
|
||||
variables: {
|
||||
groupId: 'g1',
|
||||
userId: 'u1',
|
||||
roleInGroup: 'usual',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: changeGroupMemberRoleMutation(),
|
||||
variables: {
|
||||
groupId: 'g1',
|
||||
userId: 'u5',
|
||||
roleInGroup: 'admin',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: changeGroupMemberRoleMutation(),
|
||||
variables: {
|
||||
groupId: 'g1',
|
||||
userId: 'u6',
|
||||
roleInGroup: 'owner',
|
||||
},
|
||||
}),
|
||||
])
|
||||
// post into group
|
||||
await Promise.all([
|
||||
mutate({
|
||||
mutation: createPostMutation(),
|
||||
variables: {
|
||||
id: 'p0-g1',
|
||||
groupId: 'g1',
|
||||
title: `Can we use ocelot for education?`,
|
||||
content: 'I like the concept of this school. Can we use our software in this?',
|
||||
categoryIds: ['cat8'],
|
||||
},
|
||||
}),
|
||||
])
|
||||
authenticatedUser = await peterLustig.toJson()
|
||||
await Promise.all([
|
||||
mutate({
|
||||
mutation: createPostMutation(),
|
||||
variables: {
|
||||
id: 'p1-g1',
|
||||
groupId: 'g1',
|
||||
title: `Can we push this idea out of France?`,
|
||||
content: 'This idea is too inportant to have the scope only on France.',
|
||||
categoryIds: ['cat14'],
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
authenticatedUser = await bobDerBaumeister.toJson()
|
||||
await Promise.all([
|
||||
mutate({
|
||||
mutation: createGroupMutation(),
|
||||
variables: {
|
||||
id: 'g2',
|
||||
name: 'Yoga Practice',
|
||||
about: 'We do yoga around the clock.',
|
||||
description: `<h3>What Is yoga?</h3><p>Yoga is not just about practicing asanas. It's about how we do it.</p><p class="">And practicing asanas doesn't have to be yoga, it can be more athletic than yogic.</p><h3>What makes practicing asanas yogic?</h3><p class="">The important thing is:</p><ul><li><p>Use the exercises (consciously) for your personal development.</p></li></ul>`,
|
||||
groupType: 'public',
|
||||
actionRadius: 'interplanetary',
|
||||
categoryIds: ['cat4', 'cat5', 'cat17'],
|
||||
},
|
||||
}),
|
||||
])
|
||||
await Promise.all([
|
||||
mutate({
|
||||
mutation: joinGroupMutation(),
|
||||
variables: {
|
||||
groupId: 'g2',
|
||||
userId: 'u3',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: joinGroupMutation(),
|
||||
variables: {
|
||||
groupId: 'g2',
|
||||
userId: 'u4',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: joinGroupMutation(),
|
||||
variables: {
|
||||
groupId: 'g2',
|
||||
userId: 'u5',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: joinGroupMutation(),
|
||||
variables: {
|
||||
groupId: 'g2',
|
||||
userId: 'u6',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: joinGroupMutation(),
|
||||
variables: {
|
||||
groupId: 'g2',
|
||||
userId: 'u7',
|
||||
},
|
||||
}),
|
||||
])
|
||||
await Promise.all([
|
||||
mutate({
|
||||
mutation: changeGroupMemberRoleMutation(),
|
||||
variables: {
|
||||
groupId: 'g2',
|
||||
userId: 'u3',
|
||||
roleInGroup: 'usual',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: changeGroupMemberRoleMutation(),
|
||||
variables: {
|
||||
groupId: 'g2',
|
||||
userId: 'u4',
|
||||
roleInGroup: 'pending',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: changeGroupMemberRoleMutation(),
|
||||
variables: {
|
||||
groupId: 'g2',
|
||||
userId: 'u5',
|
||||
roleInGroup: 'admin',
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: changeGroupMemberRoleMutation(),
|
||||
variables: {
|
||||
groupId: 'g2',
|
||||
userId: 'u6',
|
||||
roleInGroup: 'usual',
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
authenticatedUser = await louie.toJson()
|
||||
await Promise.all([
|
||||
mutate({
|
||||
mutation: createPostMutation(),
|
||||
variables: {
|
||||
id: 'p0-g2',
|
||||
groupId: 'g2',
|
||||
title: `I am a Noob`,
|
||||
content: 'I am new to Yoga and did not join this group so far.',
|
||||
categoryIds: ['cat4'],
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
// Create Posts
|
||||
|
||||
const [p0, p1, p3, p4, p5, p6, p9, p10, p11, p13, p14, p15] = await Promise.all([
|
||||
Factory.build(
|
||||
'post',
|
||||
@ -553,17 +773,10 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
'See <a class="hashtag" data-hashtag-id="NaturphilosophieYoga" href="/?hashtag=NaturphilosophieYoga">#NaturphilosophieYoga</a>, it can really help you!'
|
||||
const hashtagAndMention1 =
|
||||
'The new physics of <a class="hashtag" data-hashtag-id="QuantenFlussTheorie" href="/?hashtag=QuantenFlussTheorie">#QuantenFlussTheorie</a> can explain <a class="hashtag" data-hashtag-id="QuantumGravity" href="/?hashtag=QuantumGravity">#QuantumGravity</a>! <a class="mention" data-mention-id="u1" href="/profile/u1">@peter-lustig</a> got that already. ;-)'
|
||||
const createPostMutation = gql`
|
||||
mutation ($id: ID, $title: String!, $content: String!, $categoryIds: [ID]) {
|
||||
CreatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
await Promise.all([
|
||||
mutate({
|
||||
mutation: createPostMutation,
|
||||
mutation: createPostMutation(),
|
||||
variables: {
|
||||
id: 'p2',
|
||||
title: `Nature Philosophy Yoga`,
|
||||
@ -572,7 +785,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: createPostMutation,
|
||||
mutation: createPostMutation(),
|
||||
variables: {
|
||||
id: 'p7',
|
||||
title: 'This is post #7',
|
||||
@ -581,7 +794,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: createPostMutation,
|
||||
mutation: createPostMutation(),
|
||||
variables: {
|
||||
id: 'p8',
|
||||
image: faker.image.unsplash.nature(),
|
||||
@ -591,7 +804,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: createPostMutation,
|
||||
mutation: createPostMutation(),
|
||||
variables: {
|
||||
id: 'p12',
|
||||
title: 'This is post #12',
|
||||
@ -610,13 +823,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
'I heard <a class="mention" data-mention-id="u3" href="/profile/u3">@jenny-rostock</a> has practiced it for 3 years now.'
|
||||
const mentionInComment2 =
|
||||
'Did <a class="mention" data-mention-id="u1" href="/profile/u1">@peter-lustig</a> tell you?'
|
||||
const createCommentMutation = gql`
|
||||
mutation ($id: ID, $postId: ID!, $content: String!) {
|
||||
CreateComment(id: $id, postId: $postId, content: $content) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
await Promise.all([
|
||||
mutate({
|
||||
mutation: createCommentMutation,
|
||||
|
||||
108
backend/src/graphql/GraphQL-Playground.md
Normal file
108
backend/src/graphql/GraphQL-Playground.md
Normal file
@ -0,0 +1,108 @@
|
||||
# GraphQL Playground
|
||||
|
||||
To use GraphQL Playground, we need to know some basics:
|
||||
|
||||
## How To Login?
|
||||
|
||||
First, we need to have a user from ocelot.social to log in as.
|
||||
The user can be created by seeding the Neo4j database from the backend or by multiple GraphQL mutations.
|
||||
|
||||
### Seed The Neo4j Database
|
||||
|
||||
In your browser you can reach the GraphQL Playground under `http://localhost:4000/`, if the database and the backend are running, see [backend](../../README.md).
|
||||
There you will also find instructions on how to seed the database.
|
||||
|
||||
### Use GraphQL Mutations To Create A User
|
||||
|
||||
TODO: Describe how to create a user using GraphQL mutations!
|
||||
|
||||
### Login Via GraphQL
|
||||
|
||||
You can register a user by sending the query:
|
||||
|
||||
```gql
|
||||
mutation {
|
||||
login(email: "user@example.org", password: "1234")
|
||||
}
|
||||
```
|
||||
|
||||
Or use `"moderator@example.org"` or `"admin@example.org"` for the roll you need.
|
||||
|
||||
If all goes well, you will receive a QGL response like:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"login": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InUzIiwibmFtZSI6Ikplbm55IFJvc3RvY2siLCJzbHVnIjoiamVubnktcm9zdG9jayIsImlhdCI6MTY2MjAyMzMwNSwiZXhwIjoxNzI1MTM4NTA1LCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjQwMDAiLCJzdWIiOiJ1MyJ9.atBS-SOeS784HPeFl_5s8sRWehEAU1BkgcOZFD8d4bU"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can use this response to set an HTTP header when you click `HTTP HEADERS` in the footer.
|
||||
Just set it with the login token you received in response:
|
||||
|
||||
```json
|
||||
{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InUzIiwibmFtZSI6Ikplbm55IFJvc3RvY2siLCJzbHVnIjoiamVubnktcm9zdG9jayIsImlhdCI6MTY2MjAyMzMwNSwiZXhwIjoxNzI1MTM4NTA1LCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjQwMDAiLCJzdWIiOiJ1MyJ9.atBS-SOeS784HPeFl_5s8sRWehEAU1BkgcOZFD8d4bU"
|
||||
}
|
||||
```
|
||||
|
||||
This token is used for all other queries and mutations you send to the backend.
|
||||
|
||||
## Query And Mutate
|
||||
|
||||
When you are logged in and open a new playground tab by clicking "+", you can create a new group by sending the following mutation:
|
||||
|
||||
```gql
|
||||
mutation {
|
||||
CreateGroup(
|
||||
# id: ""
|
||||
name: "My Group"
|
||||
# slug: ""
|
||||
about: "We will save the world"
|
||||
description: "<p class=\"\"><em>English:</em></p><p class=\"\">This group is hidden.</p><h3>What is our group for?</h3><p>This group was created to allow investigative journalists to share and collaborate.</p><h3>How does it work?</h3><p>Here you can internally share posts and comments about them.</p><p><br></p><p><em>Deutsch:</em></p><p class=\"\">Diese Gruppe ist verborgen.</p><h3>Wofür ist unsere Gruppe?</h3><p class=\"\">Diese Gruppe wurde geschaffen, um investigativen Journalisten den Austausch und die Zusammenarbeit zu ermöglichen.</p><h3>Wie funktioniert das?</h3><p class=\"\">Hier könnt ihr euch intern über Beiträge und Kommentare zu ihnen austauschen.</p>"
|
||||
groupType: hidden
|
||||
actionRadius: interplanetary
|
||||
categoryIds: ["cat12"]
|
||||
) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
createdAt
|
||||
updatedAt
|
||||
disabled
|
||||
deleted
|
||||
about
|
||||
description
|
||||
groupType
|
||||
actionRadius
|
||||
myRole
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You will receive the answer:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"CreateGroup": {
|
||||
"id": "2e3bbadb-804b-4ebc-a673-2d7c7f05e827",
|
||||
"name": "My Group",
|
||||
"slug": "my-group",
|
||||
"createdAt": "2022-09-01T09:44:47.969Z",
|
||||
"updatedAt": "2022-09-01T09:44:47.969Z",
|
||||
"disabled": false,
|
||||
"deleted": false,
|
||||
"about": "We will save the world",
|
||||
"description": "<p class=\"\"><em>English:</em></p><p class=\"\">This group is hidden.</p><h3>What is our group for?</h3><p>This group was created to allow investigative journalists to share and collaborate.</p><h3>How does it work?</h3><p>Here you can internally share posts and comments about them.</p><p><br></p><p><em>Deutsch:</em></p><p class=\"\">Diese Gruppe ist verborgen.</p><h3>Wofür ist unsere Gruppe?</h3><p class=\"\">Diese Gruppe wurde geschaffen, um investigativen Journalisten den Austausch und die Zusammenarbeit zu ermöglichen.</p><h3>Wie funktioniert das?</h3><p class=\"\">Hier könnt ihr euch intern über Beiträge und Kommentare zu ihnen austauschen.</p>",
|
||||
"groupType": "hidden",
|
||||
"actionRadius": "interplanetary",
|
||||
"myRole": "owner"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you look into the Neo4j database with your browser and search the groups, you will now also find your new group.
|
||||
For more details about our Neo4j database read [here](../../../neo4j/README.md).
|
||||
@ -1,5 +1,18 @@
|
||||
// TODO: can be replaced with: (which is no a fake)
|
||||
// import gql from 'graphql-tag'
|
||||
// See issue: https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/5152
|
||||
|
||||
//* This is a fake ES2015 template string, just to benefit of syntax
|
||||
// highlighting of `gql` template strings in certain editors.
|
||||
export function gql(strings) {
|
||||
return strings.join('')
|
||||
}
|
||||
|
||||
// sometime we have to wait to check a db state by having a look into the db in a certain moment
|
||||
// or we wait a bit to check if we missed to set an await somewhere
|
||||
// see: https://www.sitepoint.com/delay-sleep-pause-wait/
|
||||
export function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
// usage – 4 seconds for example
|
||||
// await sleep(4 * 1000)
|
||||
|
||||
@ -1,26 +1,32 @@
|
||||
import trunc from 'trunc-html'
|
||||
import { DESCRIPTION_EXCERPT_HTML_LENGTH } from '../constants/groups'
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreateGroup: async (resolve, root, args, context, info) => {
|
||||
args.descriptionExcerpt = trunc(args.description, DESCRIPTION_EXCERPT_HTML_LENGTH).html
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
UpdateGroup: async (resolve, root, args, context, info) => {
|
||||
if (args.description)
|
||||
args.descriptionExcerpt = trunc(args.description, DESCRIPTION_EXCERPT_HTML_LENGTH).html
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
CreatePost: async (resolve, root, args, context, info) => {
|
||||
args.contentExcerpt = trunc(args.content, 120).html
|
||||
const result = await resolve(root, args, context, info)
|
||||
return result
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
UpdatePost: async (resolve, root, args, context, info) => {
|
||||
args.contentExcerpt = trunc(args.content, 120).html
|
||||
const result = await resolve(root, args, context, info)
|
||||
return result
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
CreateComment: async (resolve, root, args, context, info) => {
|
||||
args.contentExcerpt = trunc(args.content, 180).html
|
||||
const result = await resolve(root, args, context, info)
|
||||
return result
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
UpdateComment: async (resolve, root, args, context, info) => {
|
||||
args.contentExcerpt = trunc(args.content, 180).html
|
||||
const result = await resolve(root, args, context, info)
|
||||
return result
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,6 +1,13 @@
|
||||
import sanitizeHtml from 'sanitize-html'
|
||||
import linkifyHtml from 'linkifyjs/html'
|
||||
|
||||
export const removeHtmlTags = (input) => {
|
||||
return sanitizeHtml(input, {
|
||||
allowedTags: [],
|
||||
allowedAttributes: {},
|
||||
})
|
||||
}
|
||||
|
||||
const standardSanitizeHtmlOptions = {
|
||||
allowedTags: [
|
||||
'img',
|
||||
|
||||
@ -1,12 +1,5 @@
|
||||
import LanguageDetect from 'languagedetect'
|
||||
import sanitizeHtml from 'sanitize-html'
|
||||
|
||||
const removeHtmlTags = (input) => {
|
||||
return sanitizeHtml(input, {
|
||||
allowedTags: [],
|
||||
allowedAttributes: {},
|
||||
})
|
||||
}
|
||||
import { removeHtmlTags } from '../helpers/cleanHtml.js'
|
||||
|
||||
const setPostLanguage = (text) => {
|
||||
const lngDetector = new LanguageDetect()
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { rule, shield, deny, allow, or } from 'graphql-shield'
|
||||
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'
|
||||
@ -52,6 +52,237 @@ const isMySocialMedia = rule({
|
||||
return socialMedia.ownedBy.node.id === user.id
|
||||
})
|
||||
|
||||
const isAllowedToChangeGroupSettings = rule({
|
||||
cache: 'no_cache',
|
||||
})(async (_parent, args, { user, driver }) => {
|
||||
if (!(user && user.id)) return false
|
||||
const ownerId = user.id
|
||||
const { id: groupId } = args
|
||||
const session = driver.session()
|
||||
const readTxPromise = session.readTransaction(async (transaction) => {
|
||||
const transactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (owner:User {id: $ownerId})-[membership:MEMBER_OF]->(group:Group {id: $groupId})
|
||||
RETURN group {.*}, owner {.*, myRoleInGroup: membership.role}
|
||||
`,
|
||||
{ groupId, ownerId },
|
||||
)
|
||||
return {
|
||||
owner: transactionResponse.records.map((record) => record.get('owner'))[0],
|
||||
group: transactionResponse.records.map((record) => record.get('group'))[0],
|
||||
}
|
||||
})
|
||||
try {
|
||||
const { owner, group } = await readTxPromise
|
||||
return !!group && !!owner && ['owner'].includes(owner.myRoleInGroup)
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
})
|
||||
|
||||
const isAllowedSeeingGroupMembers = rule({
|
||||
cache: 'no_cache',
|
||||
})(async (_parent, args, { user, driver }) => {
|
||||
if (!(user && user.id)) return false
|
||||
const { id: groupId } = args
|
||||
const session = driver.session()
|
||||
const readTxPromise = session.readTransaction(async (transaction) => {
|
||||
const transactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (group:Group {id: $groupId})
|
||||
OPTIONAL MATCH (member:User {id: $userId})-[membership:MEMBER_OF]->(group)
|
||||
RETURN group {.*}, member {.*, myRoleInGroup: membership.role}
|
||||
`,
|
||||
{ groupId, userId: user.id },
|
||||
)
|
||||
return {
|
||||
member: transactionResponse.records.map((record) => record.get('member'))[0],
|
||||
group: transactionResponse.records.map((record) => record.get('group'))[0],
|
||||
}
|
||||
})
|
||||
try {
|
||||
const { member, group } = await readTxPromise
|
||||
return (
|
||||
!!group &&
|
||||
(group.groupType === 'public' ||
|
||||
(['closed', 'hidden'].includes(group.groupType) &&
|
||||
!!member &&
|
||||
['usual', 'admin', 'owner'].includes(member.myRoleInGroup)))
|
||||
)
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
})
|
||||
|
||||
const isAllowedToChangeGroupMemberRole = rule({
|
||||
cache: 'no_cache',
|
||||
})(async (_parent, args, { user, driver }) => {
|
||||
if (!(user && user.id)) return false
|
||||
const adminId = user.id
|
||||
const { groupId, userId, roleInGroup } = args
|
||||
if (adminId === userId) return false
|
||||
const session = driver.session()
|
||||
const readTxPromise = session.readTransaction(async (transaction) => {
|
||||
const transactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (admin:User {id: $adminId})-[adminMembership:MEMBER_OF]->(group:Group {id: $groupId})
|
||||
OPTIONAL MATCH (group)<-[userMembership:MEMBER_OF]-(member:User {id: $userId})
|
||||
RETURN group {.*}, admin {.*, myRoleInGroup: adminMembership.role}, member {.*, myRoleInGroup: userMembership.role}
|
||||
`,
|
||||
{ groupId, adminId, userId },
|
||||
)
|
||||
return {
|
||||
admin: transactionResponse.records.map((record) => record.get('admin'))[0],
|
||||
group: transactionResponse.records.map((record) => record.get('group'))[0],
|
||||
member: transactionResponse.records.map((record) => record.get('member'))[0],
|
||||
}
|
||||
})
|
||||
try {
|
||||
const { admin, group, member } = await readTxPromise
|
||||
return (
|
||||
!!group &&
|
||||
!!admin &&
|
||||
(!member ||
|
||||
(!!member &&
|
||||
(member.myRoleInGroup === roleInGroup || !['owner'].includes(member.myRoleInGroup)))) &&
|
||||
((['admin'].includes(admin.myRoleInGroup) &&
|
||||
['pending', 'usual', 'admin'].includes(roleInGroup)) ||
|
||||
(['owner'].includes(admin.myRoleInGroup) &&
|
||||
['pending', 'usual', 'admin', 'owner'].includes(roleInGroup)))
|
||||
)
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
})
|
||||
|
||||
const isAllowedToJoinGroup = rule({
|
||||
cache: 'no_cache',
|
||||
})(async (_parent, args, { user, driver }) => {
|
||||
if (!(user && user.id)) return false
|
||||
const { groupId, userId } = args
|
||||
const session = driver.session()
|
||||
const readTxPromise = session.readTransaction(async (transaction) => {
|
||||
const transactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (group:Group {id: $groupId})
|
||||
OPTIONAL MATCH (group)<-[membership:MEMBER_OF]-(member:User {id: $userId})
|
||||
RETURN group {.*}, member {.*, myRoleInGroup: membership.role}
|
||||
`,
|
||||
{ groupId, userId },
|
||||
)
|
||||
return {
|
||||
group: transactionResponse.records.map((record) => record.get('group'))[0],
|
||||
member: transactionResponse.records.map((record) => record.get('member'))[0],
|
||||
}
|
||||
})
|
||||
try {
|
||||
const { group, member } = await readTxPromise
|
||||
return !!group && (group.groupType !== 'hidden' || (!!member && !!member.myRoleInGroup))
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
})
|
||||
|
||||
const isAllowedToLeaveGroup = rule({
|
||||
cache: 'no_cache',
|
||||
})(async (_parent, args, { user, driver }) => {
|
||||
if (!(user && user.id)) return false
|
||||
const { groupId, userId } = args
|
||||
if (user.id !== userId) return false
|
||||
const session = driver.session()
|
||||
const readTxPromise = session.readTransaction(async (transaction) => {
|
||||
const transactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (member:User {id: $userId})-[membership:MEMBER_OF]->(group:Group {id: $groupId})
|
||||
RETURN group {.*}, member {.*, myRoleInGroup: membership.role}
|
||||
`,
|
||||
{ groupId, userId },
|
||||
)
|
||||
return {
|
||||
group: transactionResponse.records.map((record) => record.get('group'))[0],
|
||||
member: transactionResponse.records.map((record) => record.get('member'))[0],
|
||||
}
|
||||
})
|
||||
try {
|
||||
const { group, member } = await readTxPromise
|
||||
return !!group && !!member && !!member.myRoleInGroup && member.myRoleInGroup !== 'owner'
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
})
|
||||
|
||||
const isMemberOfGroup = rule({
|
||||
cache: 'no_cache',
|
||||
})(async (_parent, args, { user, driver }) => {
|
||||
if (!(user && user.id)) return false
|
||||
const { groupId } = args
|
||||
if (!groupId) return true
|
||||
const userId = user.id
|
||||
const session = driver.session()
|
||||
const readTxPromise = session.readTransaction(async (transaction) => {
|
||||
const transactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (User {id: $userId})-[membership:MEMBER_OF]->(Group {id: $groupId})
|
||||
RETURN membership.role AS role
|
||||
`,
|
||||
{ groupId, userId },
|
||||
)
|
||||
return transactionResponse.records.map((record) => record.get('role'))[0]
|
||||
})
|
||||
try {
|
||||
const role = await readTxPromise
|
||||
return ['usual', 'admin', 'owner'].includes(role)
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
})
|
||||
|
||||
const canCommentPost = rule({
|
||||
cache: 'no_cache',
|
||||
})(async (_parent, args, { user, driver }) => {
|
||||
if (!(user && user.id)) return false
|
||||
const { postId } = args
|
||||
const userId = user.id
|
||||
const session = driver.session()
|
||||
const readTxPromise = session.readTransaction(async (transaction) => {
|
||||
const transactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (post:Post { id: $postId })
|
||||
OPTIONAL MATCH (post)-[:IN]->(group:Group)
|
||||
OPTIONAL MATCH (user:User { id: $userId })-[membership:MEMBER_OF]->(group)
|
||||
RETURN group AS group, membership AS membership
|
||||
`,
|
||||
{ postId, userId },
|
||||
)
|
||||
return {
|
||||
group: transactionResponse.records.map((record) => record.get('group'))[0],
|
||||
membership: transactionResponse.records.map((record) => record.get('membership'))[0],
|
||||
}
|
||||
})
|
||||
try {
|
||||
const { group, membership } = await readTxPromise
|
||||
return (
|
||||
!group || (membership && ['usual', 'admin', 'owner'].includes(membership.properties.role))
|
||||
)
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
})
|
||||
|
||||
const isAuthor = rule({
|
||||
cache: 'no_cache',
|
||||
})(async (_parent, args, { user, driver }) => {
|
||||
@ -78,7 +309,7 @@ const isAuthor = rule({
|
||||
|
||||
const isDeletingOwnAccount = rule({
|
||||
cache: 'no_cache',
|
||||
})(async (parent, args, context, info) => {
|
||||
})(async (parent, args, context, _info) => {
|
||||
return context.user.id === args.id
|
||||
})
|
||||
|
||||
@ -102,11 +333,10 @@ export default shield(
|
||||
{
|
||||
Query: {
|
||||
'*': deny,
|
||||
findPosts: allow,
|
||||
findUsers: allow,
|
||||
searchResults: allow,
|
||||
searchPosts: allow,
|
||||
searchUsers: allow,
|
||||
searchGroups: allow,
|
||||
searchHashtags: allow,
|
||||
embed: allow,
|
||||
Category: allow,
|
||||
@ -114,6 +344,9 @@ export default shield(
|
||||
reports: isModerator,
|
||||
statistics: allow,
|
||||
currentUser: allow,
|
||||
Group: isAuthenticated,
|
||||
GroupMembers: isAllowedSeeingGroupMembers,
|
||||
GroupCount: isAuthenticated,
|
||||
Post: allow,
|
||||
profilePagePosts: allow,
|
||||
Comment: allow,
|
||||
@ -140,7 +373,12 @@ export default shield(
|
||||
Signup: or(publicRegistration, inviteRegistration, isAdmin),
|
||||
SignupVerification: allow,
|
||||
UpdateUser: onlyYourself,
|
||||
CreatePost: isAuthenticated,
|
||||
CreateGroup: isAuthenticated,
|
||||
UpdateGroup: isAllowedToChangeGroupSettings,
|
||||
JoinGroup: isAllowedToJoinGroup,
|
||||
LeaveGroup: isAllowedToLeaveGroup,
|
||||
ChangeGroupMemberRole: isAllowedToChangeGroupMemberRole,
|
||||
CreatePost: and(isAuthenticated, isMemberOfGroup),
|
||||
UpdatePost: isAuthor,
|
||||
DeletePost: isAuthor,
|
||||
fileReport: isAuthenticated,
|
||||
@ -157,7 +395,7 @@ export default shield(
|
||||
unshout: isAuthenticated,
|
||||
changePassword: isAuthenticated,
|
||||
review: isModerator,
|
||||
CreateComment: isAuthenticated,
|
||||
CreateComment: and(isAuthenticated, canCommentPost),
|
||||
UpdateComment: isAuthor,
|
||||
DeleteComment: isAuthor,
|
||||
DeleteUser: or(isDeletingOwnAccount, isAdmin),
|
||||
@ -178,6 +416,7 @@ export default shield(
|
||||
GenerateInviteCode: isAuthenticated,
|
||||
switchUserRole: isAdmin,
|
||||
markTeaserAsViewed: allow,
|
||||
saveCategorySettings: isAuthenticated,
|
||||
},
|
||||
User: {
|
||||
email: or(isMyOwn, isAdmin),
|
||||
@ -188,5 +427,6 @@ export default shield(
|
||||
debug,
|
||||
allowExternalErrors,
|
||||
fallbackRule: allow,
|
||||
fallbackError: Error('Not Authorized!'),
|
||||
},
|
||||
)
|
||||
|
||||
@ -102,7 +102,7 @@ describe('authorization', () => {
|
||||
await expect(
|
||||
query({ query: userQuery, variables: { name: 'Owner' } }),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
data: { User: [null] },
|
||||
})
|
||||
})
|
||||
@ -132,7 +132,7 @@ describe('authorization', () => {
|
||||
await expect(
|
||||
query({ query: userQuery, variables: { name: 'Owner' } }),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
data: { User: [null] },
|
||||
})
|
||||
})
|
||||
@ -147,7 +147,7 @@ describe('authorization', () => {
|
||||
await expect(
|
||||
query({ query: userQuery, variables: { name: 'Owner' } }),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
data: { User: [null] },
|
||||
})
|
||||
})
|
||||
@ -198,7 +198,7 @@ describe('authorization', () => {
|
||||
|
||||
it('denies permission', async () => {
|
||||
await expect(mutate({ mutation: signupMutation, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
data: { Signup: null },
|
||||
})
|
||||
})
|
||||
@ -288,7 +288,7 @@ describe('authorization', () => {
|
||||
|
||||
it('denies permission', async () => {
|
||||
await expect(mutate({ mutation: signupMutation, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
data: { Signup: null },
|
||||
})
|
||||
})
|
||||
|
||||
@ -26,11 +26,16 @@ export default {
|
||||
args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'User')))
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
CreateGroup: async (resolve, root, args, context, info) => {
|
||||
args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Group')))
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
CreatePost: async (resolve, root, args, context, info) => {
|
||||
args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post')))
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
UpdatePost: async (resolve, root, args, context, info) => {
|
||||
// TODO: is this absolutely correct? what happens if "args.title" is not defined? may it works accidentally, because "args.title" or "args.slug" is always send?
|
||||
args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post')))
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import slugify from 'slug'
|
||||
|
||||
export default async function uniqueSlug(string, isUnique) {
|
||||
const slug = slugify(string || 'anonymous', {
|
||||
lower: true,
|
||||
|
||||
@ -1,29 +1,34 @@
|
||||
import Factory, { cleanDatabase } from '../db/factories'
|
||||
import { gql } from '../helpers/jest'
|
||||
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 '../db/graphql/groups'
|
||||
import { createPostMutation } from '../db/graphql/posts'
|
||||
import { signupVerificationMutation } from '../db/graphql/authentications'
|
||||
|
||||
let mutate
|
||||
let authenticatedUser
|
||||
let variables
|
||||
const categoryIds = ['cat9']
|
||||
|
||||
const driver = getDriver()
|
||||
const neode = getNeode()
|
||||
const descriptionAdditional100 =
|
||||
' 123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789'
|
||||
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
driver,
|
||||
neode,
|
||||
user: authenticatedUser,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const { mutate } = createTestClient(server)
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
driver,
|
||||
neode,
|
||||
user: authenticatedUser,
|
||||
}
|
||||
},
|
||||
})
|
||||
mutate = createTestClient(server).mutate
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@ -46,6 +51,7 @@ beforeEach(async () => {
|
||||
await Factory.build('category', {
|
||||
id: 'cat9',
|
||||
name: 'Democracy & Politics',
|
||||
slug: 'democracy-politics',
|
||||
icon: 'university',
|
||||
})
|
||||
authenticatedUser = await admin.toJson()
|
||||
@ -57,16 +63,242 @@ afterEach(async () => {
|
||||
})
|
||||
|
||||
describe('slugifyMiddleware', () => {
|
||||
describe('CreatePost', () => {
|
||||
const categoryIds = ['cat9']
|
||||
const createPostMutation = gql`
|
||||
mutation ($title: String!, $content: String!, $categoryIds: [ID]!, $slug: String) {
|
||||
CreatePost(title: $title, content: $content, categoryIds: $categoryIds, slug: $slug) {
|
||||
slug
|
||||
}
|
||||
describe('CreateGroup', () => {
|
||||
beforeEach(() => {
|
||||
variables = {
|
||||
...variables,
|
||||
name: 'The Best Group',
|
||||
about: 'Some about',
|
||||
description: 'Some description' + descriptionAdditional100,
|
||||
groupType: 'closed',
|
||||
actionRadius: 'national',
|
||||
categoryIds,
|
||||
}
|
||||
`
|
||||
})
|
||||
|
||||
describe('if slug not exists', () => {
|
||||
it('generates a slug based on name', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createGroupMutation(),
|
||||
variables,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
CreateGroup: {
|
||||
name: 'The Best Group',
|
||||
slug: 'the-best-group',
|
||||
about: 'Some about',
|
||||
description: 'Some description' + descriptionAdditional100,
|
||||
groupType: 'closed',
|
||||
actionRadius: 'national',
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('generates a slug based on given slug', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createGroupMutation(),
|
||||
variables: {
|
||||
...variables,
|
||||
slug: 'the-group',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
CreateGroup: {
|
||||
slug: 'the-group',
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('if slug exists', () => {
|
||||
beforeEach(async () => {
|
||||
await mutate({
|
||||
mutation: createGroupMutation(),
|
||||
variables: {
|
||||
...variables,
|
||||
name: 'Pre-Existing Group',
|
||||
slug: 'pre-existing-group',
|
||||
about: 'As an about',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('chooses another slug', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createGroupMutation(),
|
||||
variables: {
|
||||
...variables,
|
||||
name: 'Pre-Existing Group',
|
||||
about: 'As an about',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
CreateGroup: {
|
||||
slug: 'pre-existing-group-1',
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
describe('but if the client specifies a slug', () => {
|
||||
it('rejects CreateGroup', async (done) => {
|
||||
try {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createGroupMutation(),
|
||||
variables: {
|
||||
...variables,
|
||||
name: 'Pre-Existing Group',
|
||||
about: 'As an about',
|
||||
slug: 'pre-existing-group',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [
|
||||
{
|
||||
message: 'Group with this slug already exists!',
|
||||
},
|
||||
],
|
||||
})
|
||||
done()
|
||||
} catch (error) {
|
||||
throw new Error(`
|
||||
${error}
|
||||
|
||||
Probably your database has no unique constraints!
|
||||
|
||||
To see all constraints go to http://localhost:7474/browser/ and
|
||||
paste the following:
|
||||
\`\`\`
|
||||
CALL db.constraints();
|
||||
\`\`\`
|
||||
|
||||
Learn how to setup the database here:
|
||||
https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/master/backend/README.md#database-indices-and-constraints
|
||||
`)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('UpdateGroup', () => {
|
||||
let createGroupResult
|
||||
|
||||
beforeEach(async () => {
|
||||
createGroupResult = await mutate({
|
||||
mutation: createGroupMutation(),
|
||||
variables: {
|
||||
name: 'The Best Group',
|
||||
slug: 'the-best-group',
|
||||
about: 'Some about',
|
||||
description: 'Some description' + descriptionAdditional100,
|
||||
groupType: 'closed',
|
||||
actionRadius: 'national',
|
||||
categoryIds,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('if group exists', () => {
|
||||
describe('if new slug not(!) exists', () => {
|
||||
describe('setting slug explicitly', () => {
|
||||
it('has the new slug', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: updateGroupMutation(),
|
||||
variables: {
|
||||
id: createGroupResult.data.CreateGroup.id,
|
||||
slug: 'my-best-group',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
UpdateGroup: {
|
||||
name: 'The Best Group',
|
||||
slug: 'my-best-group',
|
||||
about: 'Some about',
|
||||
description: 'Some description' + descriptionAdditional100,
|
||||
groupType: 'closed',
|
||||
actionRadius: 'national',
|
||||
myRole: 'owner',
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('if new slug exists in another group', () => {
|
||||
beforeEach(async () => {
|
||||
await mutate({
|
||||
mutation: createGroupMutation(),
|
||||
variables: {
|
||||
name: 'Pre-Existing Group',
|
||||
slug: 'pre-existing-group',
|
||||
about: 'Some about',
|
||||
description: 'Some description' + descriptionAdditional100,
|
||||
groupType: 'closed',
|
||||
actionRadius: 'national',
|
||||
categoryIds,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('setting slug explicitly', () => {
|
||||
it('rejects UpdateGroup', async (done) => {
|
||||
try {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: updateGroupMutation(),
|
||||
variables: {
|
||||
id: createGroupResult.data.CreateGroup.id,
|
||||
slug: 'pre-existing-group',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [
|
||||
{
|
||||
message: 'Group with this slug already exists!',
|
||||
},
|
||||
],
|
||||
})
|
||||
done()
|
||||
} catch (error) {
|
||||
throw new Error(`
|
||||
${error}
|
||||
|
||||
Probably your database has no unique constraints!
|
||||
|
||||
To see all constraints go to http://localhost:7474/browser/ and
|
||||
paste the following:
|
||||
\`\`\`
|
||||
CALL db.constraints();
|
||||
\`\`\`
|
||||
|
||||
Learn how to setup the database here:
|
||||
https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/master/backend/README.md#database-indices-and-constraints
|
||||
`)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('CreatePost', () => {
|
||||
beforeEach(() => {
|
||||
variables = {
|
||||
...variables,
|
||||
@ -76,18 +308,40 @@ describe('slugifyMiddleware', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('generates a slug based on title', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createPostMutation,
|
||||
variables,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
CreatePost: {
|
||||
slug: 'i-am-a-brand-new-post',
|
||||
describe('if slug not exists', () => {
|
||||
it('generates a slug based on title', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createPostMutation(),
|
||||
variables,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
CreatePost: {
|
||||
slug: 'i-am-a-brand-new-post',
|
||||
},
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('generates a slug based on given slug', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createPostMutation(),
|
||||
variables: {
|
||||
...variables,
|
||||
slug: 'the-post',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
CreatePost: {
|
||||
slug: 'the-post',
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -107,16 +361,15 @@ describe('slugifyMiddleware', () => {
|
||||
})
|
||||
|
||||
it('chooses another slug', async () => {
|
||||
variables = {
|
||||
...variables,
|
||||
title: 'Pre-existing post',
|
||||
content: 'Some content',
|
||||
categoryIds,
|
||||
}
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createPostMutation,
|
||||
variables,
|
||||
mutation: createPostMutation(),
|
||||
variables: {
|
||||
...variables,
|
||||
title: 'Pre-existing post',
|
||||
content: 'Some content',
|
||||
categoryIds,
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
@ -124,21 +377,24 @@ describe('slugifyMiddleware', () => {
|
||||
slug: 'pre-existing-post-1',
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
describe('but if the client specifies a slug', () => {
|
||||
it('rejects CreatePost', async (done) => {
|
||||
variables = {
|
||||
...variables,
|
||||
title: 'Pre-existing post',
|
||||
content: 'Some content',
|
||||
slug: 'pre-existing-post',
|
||||
categoryIds,
|
||||
}
|
||||
try {
|
||||
await expect(
|
||||
mutate({ mutation: createPostMutation, variables }),
|
||||
mutate({
|
||||
mutation: createPostMutation(),
|
||||
variables: {
|
||||
...variables,
|
||||
title: 'Pre-existing post',
|
||||
content: 'Some content',
|
||||
slug: 'pre-existing-post',
|
||||
categoryIds,
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [
|
||||
{
|
||||
@ -160,7 +416,7 @@ describe('slugifyMiddleware', () => {
|
||||
\`\`\`
|
||||
|
||||
Learn how to setup the database here:
|
||||
https://docs.human-connection.org/human-connection/backend#database-indices-and-constraints
|
||||
https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/master/backend/README.md#database-indices-and-constraints
|
||||
`)
|
||||
}
|
||||
})
|
||||
@ -168,29 +424,9 @@ describe('slugifyMiddleware', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('SignupVerification', () => {
|
||||
const mutation = gql`
|
||||
mutation (
|
||||
$password: String!
|
||||
$email: String!
|
||||
$name: String!
|
||||
$slug: String
|
||||
$nonce: String!
|
||||
$termsAndConditionsAgreedVersion: String!
|
||||
) {
|
||||
SignupVerification(
|
||||
email: $email
|
||||
password: $password
|
||||
name: $name
|
||||
slug: $slug
|
||||
nonce: $nonce
|
||||
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
|
||||
) {
|
||||
slug
|
||||
}
|
||||
}
|
||||
`
|
||||
it.todo('UpdatePost')
|
||||
|
||||
describe('SignupVerification', () => {
|
||||
beforeEach(() => {
|
||||
variables = {
|
||||
...variables,
|
||||
@ -211,18 +447,40 @@ describe('slugifyMiddleware', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('generates a slug based on name', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation,
|
||||
variables,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
SignupVerification: {
|
||||
slug: 'i-am-a-user',
|
||||
describe('if slug not exists', () => {
|
||||
it('generates a slug based on name', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: signupVerificationMutation,
|
||||
variables,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
SignupVerification: {
|
||||
slug: 'i-am-a-user',
|
||||
},
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('generates a slug based on given slug', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: signupVerificationMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
slug: 'the-user',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
SignupVerification: {
|
||||
slug: 'the-user',
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -237,7 +495,7 @@ describe('slugifyMiddleware', () => {
|
||||
it('chooses another slug', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation,
|
||||
mutation: signupVerificationMutation,
|
||||
variables,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
@ -246,6 +504,7 @@ describe('slugifyMiddleware', () => {
|
||||
slug: 'i-am-a-user-1',
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
@ -260,7 +519,7 @@ describe('slugifyMiddleware', () => {
|
||||
it('rejects SignupVerification (on FAIL Neo4j constraints may not defined in database)', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation,
|
||||
mutation: signupVerificationMutation,
|
||||
variables,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
|
||||
@ -31,7 +31,7 @@ const setPostCounter = async (postId, relation, context) => {
|
||||
}
|
||||
|
||||
const userClickedPost = async (resolve, root, args, context, info) => {
|
||||
if (args.id) {
|
||||
if (args.id && context.user) {
|
||||
await setPostCounter(args.id, 'CLICKED', context)
|
||||
}
|
||||
return resolve(root, args, context, info)
|
||||
|
||||
@ -9,8 +9,7 @@ export default {
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
isoDate: true,
|
||||
required: true,
|
||||
default: () => new Date().toISOString(),
|
||||
required: false,
|
||||
},
|
||||
post: {
|
||||
type: 'relationship',
|
||||
|
||||
46
backend/src/models/Group.js
Normal file
46
backend/src/models/Group.js
Normal file
@ -0,0 +1,46 @@
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
export default {
|
||||
id: { type: 'string', primary: true, default: uuid }, // TODO: should be type: 'uuid' but simplified for our tests
|
||||
name: { type: 'string', disallow: [null], min: 3 },
|
||||
slug: { type: 'string', unique: 'true', regex: /^[a-z0-9_-]+$/, lowercase: true },
|
||||
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
isoDate: true,
|
||||
required: true,
|
||||
default: () => new Date().toISOString(),
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
isoDate: true,
|
||||
required: true,
|
||||
default: () => new Date().toISOString(),
|
||||
},
|
||||
deleted: { type: 'boolean', default: false },
|
||||
disabled: { type: 'boolean', default: false },
|
||||
|
||||
avatar: {
|
||||
type: 'relationship',
|
||||
relationship: 'AVATAR_IMAGE',
|
||||
target: 'Image',
|
||||
direction: 'out',
|
||||
},
|
||||
|
||||
about: { type: 'string', allow: [null, ''] },
|
||||
description: { type: 'string', disallow: [null], min: 100 },
|
||||
descriptionExcerpt: { type: 'string', allow: [null] },
|
||||
groupType: { type: 'string', default: 'public' },
|
||||
actionRadius: { type: 'string', default: 'regional' },
|
||||
|
||||
myRole: { type: 'string', default: 'pending' },
|
||||
|
||||
locationName: { type: 'string', allow: [null] },
|
||||
|
||||
isIn: {
|
||||
type: 'relationship',
|
||||
relationship: 'IS_IN',
|
||||
target: 'Location',
|
||||
direction: 'out',
|
||||
},
|
||||
}
|
||||
@ -55,7 +55,7 @@ describe('slug', () => {
|
||||
\`\`\`
|
||||
|
||||
Learn how to setup the database here:
|
||||
https://docs.human-connection.org/human-connection/backend#database-indices-and-constraints
|
||||
https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/master/backend/README.md#database-indices-and-constraints
|
||||
`)
|
||||
}
|
||||
})
|
||||
|
||||
@ -4,6 +4,7 @@ export default {
|
||||
Image: require('./Image.js').default,
|
||||
Badge: require('./Badge.js').default,
|
||||
User: require('./User.js').default,
|
||||
Group: require('./Group.js').default,
|
||||
EmailAddress: require('./EmailAddress.js').default,
|
||||
UnverifiedEmailAddress: require('./UnverifiedEmailAddress.js').default,
|
||||
SocialMedia: require('./SocialMedia.js').default,
|
||||
|
||||
@ -20,6 +20,7 @@ export default makeAugmentedSchema({
|
||||
'FILED',
|
||||
'REVIEWED',
|
||||
'Report',
|
||||
'Group',
|
||||
],
|
||||
},
|
||||
mutation: false,
|
||||
|
||||
@ -88,10 +88,10 @@ describe('CreateComment', () => {
|
||||
variables = {
|
||||
...variables,
|
||||
postId: 'p1',
|
||||
content: "I'm not authorised to comment",
|
||||
content: "I'm not authorized to comment",
|
||||
}
|
||||
const { errors } = await mutate({ mutation: createCommentMutation, variables })
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
|
||||
@ -107,14 +107,14 @@ describe('CreateComment', () => {
|
||||
variables = {
|
||||
...variables,
|
||||
postId: 'p1',
|
||||
content: "I'm authorised to comment",
|
||||
content: "I'm authorized to comment",
|
||||
}
|
||||
})
|
||||
|
||||
it('creates a comment', async () => {
|
||||
await expect(mutate({ mutation: createCommentMutation, variables })).resolves.toMatchObject(
|
||||
{
|
||||
data: { CreateComment: { content: "I'm authorised to comment" } },
|
||||
data: { CreateComment: { content: "I'm authorized to comment" } },
|
||||
errors: undefined,
|
||||
},
|
||||
)
|
||||
@ -150,7 +150,7 @@ describe('UpdateComment', () => {
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
const { errors } = await mutate({ mutation: updateCommentMutation, variables })
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
|
||||
@ -162,7 +162,7 @@ describe('UpdateComment', () => {
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
const { errors } = await mutate({ mutation: updateCommentMutation, variables })
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
|
||||
@ -217,7 +217,7 @@ describe('UpdateComment', () => {
|
||||
it('returns null', async () => {
|
||||
const { data, errors } = await mutate({ mutation: updateCommentMutation, variables })
|
||||
expect(data).toMatchObject({ UpdateComment: null })
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -242,7 +242,7 @@ describe('DeleteComment', () => {
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
const result = await mutate({ mutation: deleteCommentMutation, variables })
|
||||
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
|
||||
@ -254,7 +254,7 @@ describe('DeleteComment', () => {
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
const { errors } = await mutate({ mutation: deleteCommentMutation, variables })
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -72,7 +72,7 @@ describe('donations', () => {
|
||||
it('throws authorization error', async () => {
|
||||
authenticatedUser = undefined
|
||||
await expect(query({ query: donationsQuery, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -106,7 +106,7 @@ describe('donations', () => {
|
||||
await expect(
|
||||
mutate({ mutation: updateDonationsMutation, variables }),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -126,7 +126,7 @@ describe('donations', () => {
|
||||
mutate({ mutation: updateDonationsMutation, variables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { UpdateDonations: null },
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -145,7 +145,7 @@ describe('donations', () => {
|
||||
mutate({ mutation: updateDonationsMutation, variables }),
|
||||
).resolves.toMatchObject({
|
||||
data: { UpdateDonations: null },
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -63,7 +63,7 @@ describe('AddEmailAddress', () => {
|
||||
it('throws AuthorizationError', async () => {
|
||||
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
|
||||
data: { AddEmailAddress: null },
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -169,7 +169,7 @@ describe('VerifyEmailAddress', () => {
|
||||
it('throws AuthorizationError', async () => {
|
||||
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
|
||||
data: { VerifyEmailAddress: null },
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -117,7 +117,7 @@ describe('follow', () => {
|
||||
variables,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
data: { followUser: null },
|
||||
})
|
||||
})
|
||||
@ -191,7 +191,7 @@ describe('follow', () => {
|
||||
authenticatedUser = null
|
||||
await expect(mutate({ mutation: mutationUnfollowUser, variables })).resolves.toMatchObject({
|
||||
data: { unfollowUser: null },
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
385
backend/src/schema/resolvers/groups.js
Normal file
385
backend/src/schema/resolvers/groups.js
Normal file
@ -0,0 +1,385 @@
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import { UserInputError } from 'apollo-server'
|
||||
import CONFIG from '../../config'
|
||||
import { CATEGORIES_MIN, CATEGORIES_MAX } from '../../constants/categories'
|
||||
import { DESCRIPTION_WITHOUT_HTML_LENGTH_MIN } from '../../constants/groups'
|
||||
import { removeHtmlTags } from '../../middleware/helpers/cleanHtml.js'
|
||||
import Resolver, {
|
||||
removeUndefinedNullValuesFromObject,
|
||||
convertObjectToCypherMapLiteral,
|
||||
} from './helpers/Resolver'
|
||||
import { mergeImage } from './images/images'
|
||||
import { createOrUpdateLocations } from './users/location'
|
||||
|
||||
export default {
|
||||
Query: {
|
||||
Group: async (_object, params, context, _resolveInfo) => {
|
||||
const { isMember, id, slug, first, offset } = params
|
||||
let pagination = ''
|
||||
if (first !== undefined && offset !== undefined) pagination = `SKIP ${offset} LIMIT ${first}`
|
||||
const matchParams = { id, slug }
|
||||
removeUndefinedNullValuesFromObject(matchParams)
|
||||
const session = context.driver.session()
|
||||
const readTxResultPromise = session.readTransaction(async (txc) => {
|
||||
const groupMatchParamsCypher = convertObjectToCypherMapLiteral(matchParams, true)
|
||||
let groupCypher
|
||||
if (isMember === true) {
|
||||
groupCypher = `
|
||||
MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group:Group${groupMatchParamsCypher})
|
||||
WITH group, membership
|
||||
WHERE (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner'])
|
||||
RETURN group {.*, myRole: membership.role}
|
||||
${pagination}
|
||||
`
|
||||
} else {
|
||||
if (isMember === false) {
|
||||
groupCypher = `
|
||||
MATCH (group:Group${groupMatchParamsCypher})
|
||||
WHERE (NOT (:User {id: $userId})-[:MEMBER_OF]->(group))
|
||||
WITH group
|
||||
WHERE group.groupType IN ['public', 'closed']
|
||||
RETURN group {.*, myRole: NULL}
|
||||
${pagination}
|
||||
`
|
||||
} else {
|
||||
groupCypher = `
|
||||
MATCH (group:Group${groupMatchParamsCypher})
|
||||
OPTIONAL MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group)
|
||||
WITH group, membership
|
||||
WHERE (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner'])
|
||||
RETURN group {.*, myRole: membership.role}
|
||||
${pagination}
|
||||
`
|
||||
}
|
||||
}
|
||||
const transactionResponse = await txc.run(groupCypher, {
|
||||
userId: context.user.id,
|
||||
})
|
||||
return transactionResponse.records.map((record) => record.get('group'))
|
||||
})
|
||||
try {
|
||||
return await readTxResultPromise
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
GroupMembers: async (_object, params, context, _resolveInfo) => {
|
||||
const { id: groupId } = params
|
||||
const session = context.driver.session()
|
||||
const readTxResultPromise = session.readTransaction(async (txc) => {
|
||||
const groupMemberCypher = `
|
||||
MATCH (user:User)-[membership:MEMBER_OF]->(:Group {id: $groupId})
|
||||
RETURN user {.*, myRoleInGroup: membership.role}
|
||||
`
|
||||
const transactionResponse = await txc.run(groupMemberCypher, {
|
||||
groupId,
|
||||
})
|
||||
return transactionResponse.records.map((record) => record.get('user'))
|
||||
})
|
||||
try {
|
||||
return await readTxResultPromise
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
GroupCount: async (_object, params, context, _resolveInfo) => {
|
||||
const { isMember } = params
|
||||
const {
|
||||
user: { id: userId },
|
||||
} = context
|
||||
const session = context.driver.session()
|
||||
const readTxResultPromise = session.readTransaction(async (txc) => {
|
||||
let cypher
|
||||
if (isMember) {
|
||||
cypher = `MATCH (user:User)-[membership:MEMBER_OF]->(group:Group)
|
||||
WHERE user.id = $userId
|
||||
AND membership.role IN ['usual', 'admin', 'owner']
|
||||
RETURN toString(count(group)) AS count`
|
||||
} else {
|
||||
cypher = `MATCH (group:Group)
|
||||
OPTIONAL MATCH (user:User)-[membership:MEMBER_OF]->(group)
|
||||
WHERE user.id = $userId
|
||||
WITH group, membership
|
||||
WHERE group.groupType IN ['public', 'closed']
|
||||
OR membership.role IN ['usual', 'admin', 'owner']
|
||||
RETURN toString(count(group)) AS count`
|
||||
}
|
||||
const transactionResponse = await txc.run(cypher, { userId })
|
||||
return transactionResponse.records.map((record) => record.get('count'))
|
||||
})
|
||||
try {
|
||||
return parseInt(await readTxResultPromise)
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
CreateGroup: async (_parent, params, context, _resolveInfo) => {
|
||||
const { categoryIds } = params
|
||||
delete params.categoryIds
|
||||
params.locationName = params.locationName === '' ? null : params.locationName
|
||||
if (CONFIG.CATEGORIES_ACTIVE && (!categoryIds || categoryIds.length < CATEGORIES_MIN)) {
|
||||
throw new UserInputError('Too view categories!')
|
||||
}
|
||||
if (CONFIG.CATEGORIES_ACTIVE && categoryIds && categoryIds.length > CATEGORIES_MAX) {
|
||||
throw new UserInputError('Too many categories!')
|
||||
}
|
||||
if (
|
||||
params.description === undefined ||
|
||||
params.description === null ||
|
||||
removeHtmlTags(params.description).length < DESCRIPTION_WITHOUT_HTML_LENGTH_MIN
|
||||
) {
|
||||
throw new UserInputError('Description too short!')
|
||||
}
|
||||
params.id = params.id || uuid()
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
const categoriesCypher =
|
||||
CONFIG.CATEGORIES_ACTIVE && categoryIds
|
||||
? `
|
||||
WITH group, membership
|
||||
UNWIND $categoryIds AS categoryId
|
||||
MATCH (category:Category {id: categoryId})
|
||||
MERGE (group)-[:CATEGORIZED]->(category)
|
||||
`
|
||||
: ''
|
||||
const ownerCreateGroupTransactionResponse = await transaction.run(
|
||||
`
|
||||
CREATE (group:Group)
|
||||
SET group += $params
|
||||
SET group.createdAt = toString(datetime())
|
||||
SET group.updatedAt = toString(datetime())
|
||||
WITH group
|
||||
MATCH (owner:User {id: $userId})
|
||||
MERGE (owner)-[:CREATED]->(group)
|
||||
MERGE (owner)-[membership:MEMBER_OF]->(group)
|
||||
SET
|
||||
membership.createdAt = toString(datetime()),
|
||||
membership.updatedAt = null,
|
||||
membership.role = 'owner'
|
||||
${categoriesCypher}
|
||||
RETURN group {.*, myRole: membership.role}
|
||||
`,
|
||||
{ userId: context.user.id, categoryIds, params },
|
||||
)
|
||||
const [group] = await ownerCreateGroupTransactionResponse.records.map((record) =>
|
||||
record.get('group'),
|
||||
)
|
||||
return group
|
||||
})
|
||||
try {
|
||||
const group = await writeTxResultPromise
|
||||
// TODO: put in a middleware, see "UpdateGroup", "UpdateUser"
|
||||
await createOrUpdateLocations('Group', params.id, params.locationName, session)
|
||||
return group
|
||||
} catch (error) {
|
||||
if (error.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
|
||||
throw new UserInputError('Group with this slug already exists!')
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
UpdateGroup: async (_parent, params, context, _resolveInfo) => {
|
||||
const { categoryIds } = params
|
||||
delete params.categoryIds
|
||||
const { id: groupId, avatar: avatarInput } = params
|
||||
delete params.avatar
|
||||
params.locationName = params.locationName === '' ? null : params.locationName
|
||||
|
||||
if (CONFIG.CATEGORIES_ACTIVE && categoryIds) {
|
||||
if (categoryIds.length < CATEGORIES_MIN) {
|
||||
throw new UserInputError('Too view categories!')
|
||||
}
|
||||
if (categoryIds.length > CATEGORIES_MAX) {
|
||||
throw new UserInputError('Too many categories!')
|
||||
}
|
||||
}
|
||||
if (
|
||||
params.description &&
|
||||
removeHtmlTags(params.description).length < DESCRIPTION_WITHOUT_HTML_LENGTH_MIN
|
||||
) {
|
||||
throw new UserInputError('Description too short!')
|
||||
}
|
||||
const session = context.driver.session()
|
||||
if (CONFIG.CATEGORIES_ACTIVE && categoryIds && categoryIds.length) {
|
||||
const cypherDeletePreviousRelations = `
|
||||
MATCH (group:Group {id: $groupId})-[previousRelations:CATEGORIZED]->(category:Category)
|
||||
DELETE previousRelations
|
||||
RETURN group, category
|
||||
`
|
||||
await session.writeTransaction((transaction) => {
|
||||
return transaction.run(cypherDeletePreviousRelations, { groupId })
|
||||
})
|
||||
}
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
let updateGroupCypher = `
|
||||
MATCH (group:Group {id: $groupId})
|
||||
SET group += $params
|
||||
SET group.updatedAt = toString(datetime())
|
||||
WITH group
|
||||
`
|
||||
if (CONFIG.CATEGORIES_ACTIVE && categoryIds && categoryIds.length) {
|
||||
updateGroupCypher += `
|
||||
UNWIND $categoryIds AS categoryId
|
||||
MATCH (category:Category {id: categoryId})
|
||||
MERGE (group)-[:CATEGORIZED]->(category)
|
||||
WITH group
|
||||
`
|
||||
}
|
||||
updateGroupCypher += `
|
||||
OPTIONAL MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group)
|
||||
RETURN group {.*, myRole: membership.role}
|
||||
`
|
||||
const transactionResponse = await transaction.run(updateGroupCypher, {
|
||||
groupId,
|
||||
userId: context.user.id,
|
||||
categoryIds,
|
||||
params,
|
||||
})
|
||||
const [group] = await transactionResponse.records.map((record) => record.get('group'))
|
||||
if (avatarInput) {
|
||||
await mergeImage(group, 'AVATAR_IMAGE', avatarInput, { transaction })
|
||||
}
|
||||
return group
|
||||
})
|
||||
try {
|
||||
const group = await writeTxResultPromise
|
||||
// TODO: put in a middleware, see "CreateGroup", "UpdateUser"
|
||||
await createOrUpdateLocations('Group', params.id, params.locationName, session)
|
||||
return group
|
||||
} catch (error) {
|
||||
if (error.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
|
||||
throw new UserInputError('Group with this slug already exists!')
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
JoinGroup: async (_parent, params, context, _resolveInfo) => {
|
||||
const { groupId, userId } = params
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
const joinGroupCypher = `
|
||||
MATCH (member:User {id: $userId}), (group:Group {id: $groupId})
|
||||
MERGE (member)-[membership:MEMBER_OF]->(group)
|
||||
ON CREATE SET
|
||||
membership.createdAt = toString(datetime()),
|
||||
membership.updatedAt = null,
|
||||
membership.role =
|
||||
CASE WHEN group.groupType = 'public'
|
||||
THEN 'usual'
|
||||
ELSE 'pending'
|
||||
END
|
||||
RETURN member {.*, myRoleInGroup: membership.role}
|
||||
`
|
||||
const transactionResponse = await transaction.run(joinGroupCypher, { groupId, userId })
|
||||
const [member] = await transactionResponse.records.map((record) => record.get('member'))
|
||||
return member
|
||||
})
|
||||
try {
|
||||
return await writeTxResultPromise
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
LeaveGroup: async (_parent, params, context, _resolveInfo) => {
|
||||
const { groupId, userId } = params
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
const leaveGroupCypher = `
|
||||
MATCH (member:User {id: $userId})-[membership:MEMBER_OF]->(group:Group {id: $groupId})
|
||||
DELETE membership
|
||||
WITH member, group
|
||||
OPTIONAL MATCH (p:Post)-[:IN]->(group)
|
||||
WHERE NOT group.groupType = 'public'
|
||||
WITH member, group, collect(p) AS posts
|
||||
FOREACH (post IN posts |
|
||||
MERGE (member)-[:CANNOT_SEE]->(post))
|
||||
RETURN member {.*, myRoleInGroup: NULL}
|
||||
`
|
||||
|
||||
const transactionResponse = await transaction.run(leaveGroupCypher, { groupId, userId })
|
||||
const [member] = await transactionResponse.records.map((record) => record.get('member'))
|
||||
return member
|
||||
})
|
||||
try {
|
||||
return await writeTxResultPromise
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
ChangeGroupMemberRole: async (_parent, params, context, _resolveInfo) => {
|
||||
const { groupId, userId, roleInGroup } = params
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
let postRestrictionCypher = ''
|
||||
if (['usual', 'admin', 'owner'].includes(roleInGroup)) {
|
||||
postRestrictionCypher = `
|
||||
WITH group, member, membership
|
||||
FOREACH (restriction IN [(member)-[r:CANNOT_SEE]->(:Post)-[:IN]->(group) | r] |
|
||||
DELETE restriction)`
|
||||
} else {
|
||||
postRestrictionCypher = `
|
||||
WITH group, member, membership
|
||||
FOREACH (post IN [(p:Post)-[:IN]->(group) | p] |
|
||||
MERGE (member)-[:CANNOT_SEE]->(post))`
|
||||
}
|
||||
|
||||
const joinGroupCypher = `
|
||||
MATCH (member:User {id: $userId})
|
||||
MATCH (group:Group {id: $groupId})
|
||||
MERGE (member)-[membership:MEMBER_OF]->(group)
|
||||
ON CREATE SET
|
||||
membership.createdAt = toString(datetime()),
|
||||
membership.updatedAt = null,
|
||||
membership.role = $roleInGroup
|
||||
ON MATCH SET
|
||||
membership.updatedAt = toString(datetime()),
|
||||
membership.role = $roleInGroup
|
||||
${postRestrictionCypher}
|
||||
RETURN member {.*, myRoleInGroup: membership.role}
|
||||
`
|
||||
|
||||
const transactionResponse = await transaction.run(joinGroupCypher, {
|
||||
groupId,
|
||||
userId,
|
||||
roleInGroup,
|
||||
})
|
||||
const [member] = await transactionResponse.records.map((record) => record.get('member'))
|
||||
return member
|
||||
})
|
||||
try {
|
||||
return await writeTxResultPromise
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
},
|
||||
Group: {
|
||||
...Resolver('Group', {
|
||||
undefinedToNull: ['deleted', 'disabled', 'locationName', 'about'],
|
||||
hasMany: {
|
||||
categories: '-[:CATEGORIZED]->(related:Category)',
|
||||
posts: '<-[:IN]-(related:Post)',
|
||||
},
|
||||
hasOne: {
|
||||
avatar: '-[:AVATAR_IMAGE]->(related:Image)',
|
||||
location: '-[:IS_IN]->(related:Location)',
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
2984
backend/src/schema/resolvers/groups.spec.js
Normal file
2984
backend/src/schema/resolvers/groups.spec.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -121,3 +121,25 @@ export default function Resolver(type, options = {}) {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export const removeUndefinedNullValuesFromObject = (obj) => {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
if ([undefined, null].includes(obj[key])) {
|
||||
delete obj[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const convertObjectToCypherMapLiteral = (params, addSpaceInfrontIfMapIsNotEmpty = false) => {
|
||||
// I have found no other way yet. maybe "apoc.convert.fromJsonMap(key)" can help, but couldn't get it how, see: https://stackoverflow.com/questions/43217823/neo4j-cypher-inline-conversion-of-string-to-a-map
|
||||
// result looks like: '{id: "g0", slug: "yoga"}'
|
||||
const paramsEntries = Object.entries(params)
|
||||
let mapLiteral = ''
|
||||
paramsEntries.forEach((ele, index) => {
|
||||
mapLiteral += index === 0 ? '{' : ''
|
||||
mapLiteral += `${ele[0]}: "${ele[1]}"`
|
||||
mapLiteral += index < paramsEntries.length - 1 ? ', ' : '}'
|
||||
})
|
||||
mapLiteral = (addSpaceInfrontIfMapIsNotEmpty && mapLiteral.length > 0 ? ' ' : '') + mapLiteral
|
||||
return mapLiteral
|
||||
}
|
||||
|
||||
47
backend/src/schema/resolvers/helpers/filterInvisiblePosts.js
Normal file
47
backend/src/schema/resolvers/helpers/filterInvisiblePosts.js
Normal file
@ -0,0 +1,47 @@
|
||||
import { mergeWith, isArray } from 'lodash'
|
||||
|
||||
const getInvisiblePosts = async (context) => {
|
||||
const session = context.driver.session()
|
||||
const readTxResultPromise = await session.readTransaction(async (transaction) => {
|
||||
let cypher = ''
|
||||
const { user } = context
|
||||
if (user && user.id) {
|
||||
cypher = `
|
||||
MATCH (post:Post)<-[:CANNOT_SEE]-(user:User { id: $userId })
|
||||
RETURN collect(post.id) AS invisiblePostIds`
|
||||
} else {
|
||||
cypher = `
|
||||
MATCH (post:Post)-[:IN]->(group:Group)
|
||||
WHERE NOT group.groupType = 'public'
|
||||
RETURN collect(post.id) AS invisiblePostIds`
|
||||
}
|
||||
const invisiblePostIdsResponse = await transaction.run(cypher, {
|
||||
userId: user ? user.id : null,
|
||||
})
|
||||
return invisiblePostIdsResponse.records.map((record) => record.get('invisiblePostIds'))
|
||||
})
|
||||
try {
|
||||
const [invisiblePostIds] = readTxResultPromise
|
||||
return invisiblePostIds
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
export const filterInvisiblePosts = async (params, context) => {
|
||||
const invisiblePostIds = await getInvisiblePosts(context)
|
||||
if (!invisiblePostIds.length) return params
|
||||
|
||||
params.filter = mergeWith(
|
||||
params.filter,
|
||||
{
|
||||
id_not_in: invisiblePostIds,
|
||||
},
|
||||
(objValue, srcValue) => {
|
||||
if (isArray(objValue)) {
|
||||
return objValue.concat(srcValue)
|
||||
}
|
||||
},
|
||||
)
|
||||
return params
|
||||
}
|
||||
@ -120,7 +120,7 @@ describe('moderate resources', () => {
|
||||
await expect(
|
||||
mutate({ mutation: reviewMutation, variables: disableVariables }),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -134,7 +134,7 @@ describe('moderate resources', () => {
|
||||
await expect(
|
||||
mutate({ mutation: reviewMutation, variables: disableVariables }),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -218,7 +218,7 @@ describe('moderate resources', () => {
|
||||
await expect(
|
||||
mutate({ mutation: reviewMutation, variables: disableVariables }),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -232,7 +232,7 @@ describe('moderate resources', () => {
|
||||
await expect(
|
||||
mutate({ mutation: reviewMutation, variables: disableVariables }),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -488,7 +488,7 @@ describe('moderate resources', () => {
|
||||
await expect(
|
||||
mutate({ mutation: reviewMutation, variables: enableVariables }),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -507,7 +507,7 @@ describe('moderate resources', () => {
|
||||
await expect(
|
||||
mutate({ mutation: reviewMutation, variables: enableVariables }),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -165,7 +165,7 @@ describe('given some notifications', () => {
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
const { errors } = await query({ query: notificationQuery })
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
|
||||
@ -313,7 +313,7 @@ describe('given some notifications', () => {
|
||||
mutation: markAsReadMutation,
|
||||
variables: { ...variables, id: 'p1' },
|
||||
})
|
||||
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -5,6 +5,8 @@ import { UserInputError } from 'apollo-server'
|
||||
import { mergeImage, deleteImage } from './images/images'
|
||||
import Resolver from './helpers/Resolver'
|
||||
import { filterForMutedUsers } from './helpers/filterForMutedUsers'
|
||||
import { filterInvisiblePosts } from './helpers/filterInvisiblePosts'
|
||||
import CONFIG from '../../config'
|
||||
|
||||
const maintainPinnedPosts = (params) => {
|
||||
const pinnedPostFilter = { pinned: true }
|
||||
@ -19,15 +21,13 @@ const maintainPinnedPosts = (params) => {
|
||||
export default {
|
||||
Query: {
|
||||
Post: async (object, params, context, resolveInfo) => {
|
||||
params = await filterInvisiblePosts(params, context)
|
||||
params = await filterForMutedUsers(params, context)
|
||||
params = await maintainPinnedPosts(params)
|
||||
return neo4jgraphql(object, params, context, resolveInfo)
|
||||
},
|
||||
findPosts: async (object, params, context, resolveInfo) => {
|
||||
params = await filterForMutedUsers(params, context)
|
||||
return neo4jgraphql(object, params, context, resolveInfo)
|
||||
},
|
||||
profilePagePosts: async (object, params, context, resolveInfo) => {
|
||||
params = await filterInvisiblePosts(params, context)
|
||||
params = await filterForMutedUsers(params, context)
|
||||
return neo4jgraphql(object, params, context, resolveInfo)
|
||||
},
|
||||
@ -76,12 +76,44 @@ export default {
|
||||
},
|
||||
Mutation: {
|
||||
CreatePost: async (_parent, params, context, _resolveInfo) => {
|
||||
const { categoryIds, groupId } = params
|
||||
const { image: imageInput } = params
|
||||
delete params.categoryIds
|
||||
delete params.image
|
||||
delete params.groupId
|
||||
params.id = params.id || uuid()
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
let groupCypher = ''
|
||||
if (groupId) {
|
||||
groupCypher = `
|
||||
WITH post MATCH (group:Group { id: $groupId })
|
||||
MERGE (post)-[:IN]->(group)`
|
||||
const groupTypeResponse = await transaction.run(
|
||||
`
|
||||
MATCH (group:Group { id: $groupId }) RETURN group.groupType AS groupType`,
|
||||
{ groupId },
|
||||
)
|
||||
const [groupType] = groupTypeResponse.records.map((record) => record.get('groupType'))
|
||||
if (groupType !== 'public')
|
||||
groupCypher += `
|
||||
WITH post, group
|
||||
MATCH (user:User)-[membership:MEMBER_OF]->(group)
|
||||
WHERE group.groupType IN ['closed', 'hidden']
|
||||
AND membership.role IN ['usual', 'admin', 'owner']
|
||||
WITH post, collect(user.id) AS userIds
|
||||
OPTIONAL MATCH path =(restricted:User) WHERE NOT restricted.id IN userIds
|
||||
FOREACH (user IN nodes(path) |
|
||||
MERGE (user)-[:CANNOT_SEE]->(post)
|
||||
)`
|
||||
}
|
||||
const categoriesCypher =
|
||||
CONFIG.CATEGORIES_ACTIVE && categoryIds
|
||||
? `WITH post
|
||||
UNWIND $categoryIds AS categoryId
|
||||
MATCH (category:Category {id: categoryId})
|
||||
MERGE (post)-[:CATEGORIZED]->(category)`
|
||||
: ''
|
||||
const createPostTransactionResponse = await transaction.run(
|
||||
`
|
||||
CREATE (post:Post)
|
||||
@ -93,9 +125,11 @@ export default {
|
||||
WITH post
|
||||
MATCH (author:User {id: $userId})
|
||||
MERGE (post)<-[:WROTE]-(author)
|
||||
${categoriesCypher}
|
||||
${groupCypher}
|
||||
RETURN post {.*}
|
||||
`,
|
||||
{ userId: context.user.id, params },
|
||||
{ userId: context.user.id, categoryIds, groupId, params },
|
||||
)
|
||||
const [post] = createPostTransactionResponse.records.map((record) => record.get('post'))
|
||||
if (imageInput) {
|
||||
@ -121,13 +155,13 @@ export default {
|
||||
delete params.image
|
||||
const session = context.driver.session()
|
||||
let updatePostCypher = `
|
||||
MATCH (post:Post {id: $params.id})
|
||||
SET post += $params
|
||||
SET post.updatedAt = toString(datetime())
|
||||
WITH post
|
||||
`
|
||||
MATCH (post:Post {id: $params.id})
|
||||
SET post += $params
|
||||
SET post.updatedAt = toString(datetime())
|
||||
WITH post
|
||||
`
|
||||
|
||||
if (categoryIds && categoryIds.length) {
|
||||
if (CONFIG.CATEGORIES_ACTIVE && categoryIds && categoryIds.length) {
|
||||
const cypherDeletePreviousRelations = `
|
||||
MATCH (post:Post { id: $params.id })-[previousRelations:CATEGORIZED]->(category:Category)
|
||||
DELETE previousRelations
|
||||
@ -348,7 +382,7 @@ export default {
|
||||
undefinedToNull: ['activityId', 'objectId', 'language', 'pinnedAt', 'pinned'],
|
||||
hasMany: {
|
||||
tags: '-[:TAGGED]->(related:Tag)',
|
||||
// categories: '-[:CATEGORIZED]->(related:Category)',
|
||||
categories: '-[:CATEGORIZED]->(related:Category)',
|
||||
comments: '<-[:COMMENTS]-(related:Comment)',
|
||||
shoutedBy: '<-[:SHOUTED]-(related:User)',
|
||||
emotions: '<-[related:EMOTED]',
|
||||
@ -357,6 +391,7 @@ export default {
|
||||
author: '<-[:WROTE]-(related:User)',
|
||||
pinnedBy: '<-[:PINNED]-(related:User)',
|
||||
image: '-[:HERO_IMAGE]->(related:Image)',
|
||||
group: '-[:IN]->(related:Group)',
|
||||
},
|
||||
count: {
|
||||
commentsCount:
|
||||
|
||||
@ -281,7 +281,7 @@ describe('CreatePost', () => {
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
const { errors } = await mutate({ mutation: createPostMutation, variables })
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
|
||||
@ -368,8 +368,8 @@ describe('UpdatePost', () => {
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
authenticatedUser = null
|
||||
expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
data: { UpdatePost: null },
|
||||
})
|
||||
})
|
||||
@ -382,7 +382,7 @@ describe('UpdatePost', () => {
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
const { errors } = await mutate({ mutation: updatePostMutation, variables })
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
|
||||
@ -547,7 +547,7 @@ describe('pin posts', () => {
|
||||
it('throws authorization error', async () => {
|
||||
authenticatedUser = null
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
data: { pinPost: null },
|
||||
})
|
||||
})
|
||||
@ -556,7 +556,7 @@ describe('pin posts', () => {
|
||||
describe('ordinary users', () => {
|
||||
it('throws authorization error', async () => {
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
data: { pinPost: null },
|
||||
})
|
||||
})
|
||||
@ -571,7 +571,7 @@ describe('pin posts', () => {
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
data: { pinPost: null },
|
||||
})
|
||||
})
|
||||
@ -854,7 +854,7 @@ describe('unpin posts', () => {
|
||||
it('throws authorization error', async () => {
|
||||
authenticatedUser = null
|
||||
await expect(mutate({ mutation: unpinPostMutation, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
data: { unpinPost: null },
|
||||
})
|
||||
})
|
||||
@ -863,7 +863,7 @@ describe('unpin posts', () => {
|
||||
describe('users cannot unpin posts', () => {
|
||||
it('throws authorization error', async () => {
|
||||
await expect(mutate({ mutation: unpinPostMutation, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
data: { unpinPost: null },
|
||||
})
|
||||
})
|
||||
@ -878,7 +878,7 @@ describe('unpin posts', () => {
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
await expect(mutate({ mutation: unpinPostMutation, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
data: { unpinPost: null },
|
||||
})
|
||||
})
|
||||
@ -975,7 +975,7 @@ describe('DeletePost', () => {
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
const { errors } = await mutate({ mutation: deletePostMutation, variables })
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
|
||||
@ -986,7 +986,7 @@ describe('DeletePost', () => {
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
const { errors } = await mutate({ mutation: deletePostMutation, variables })
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
|
||||
@ -1128,7 +1128,7 @@ describe('emotions', () => {
|
||||
variables,
|
||||
})
|
||||
|
||||
expect(addPostEmotions.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
expect(addPostEmotions.errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
|
||||
@ -1249,7 +1249,7 @@ describe('emotions', () => {
|
||||
mutation: removePostEmotionsMutation,
|
||||
variables: removePostEmotionsVariables,
|
||||
})
|
||||
expect(removePostEmotions.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
expect(removePostEmotions.errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
1681
backend/src/schema/resolvers/postsInGroups.spec.js
Normal file
1681
backend/src/schema/resolvers/postsInGroups.spec.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -72,19 +72,19 @@ const signupCypher = (inviteCode) => {
|
||||
(inviteCode:InviteCode {code: $inviteCode})<-[:GENERATED]-(host:User)
|
||||
`
|
||||
optionalMerge = `
|
||||
MERGE(user)-[:REDEEMED { createdAt: toString(datetime()) }]->(inviteCode)
|
||||
MERGE(host)-[:INVITED { createdAt: toString(datetime()) }]->(user)
|
||||
MERGE(user)-[:FOLLOWS { createdAt: toString(datetime()) }]->(host)
|
||||
MERGE(host)-[:FOLLOWS { createdAt: toString(datetime()) }]->(user)
|
||||
MERGE (user)-[:REDEEMED { createdAt: toString(datetime()) }]->(inviteCode)
|
||||
MERGE (host)-[:INVITED { createdAt: toString(datetime()) }]->(user)
|
||||
MERGE (user)-[:FOLLOWS { createdAt: toString(datetime()) }]->(host)
|
||||
MERGE (host)-[:FOLLOWS { createdAt: toString(datetime()) }]->(user)
|
||||
`
|
||||
}
|
||||
const cypher = `
|
||||
MATCH(email:EmailAddress {nonce: $nonce, email: $email})
|
||||
MATCH (email:EmailAddress {nonce: $nonce, email: $email})
|
||||
WHERE NOT (email)-[:BELONGS_TO]->()
|
||||
${optionalMatch}
|
||||
CREATE (user:User)
|
||||
MERGE(user)-[:PRIMARY_EMAIL]->(email)
|
||||
MERGE(user)<-[:BELONGS_TO]-(email)
|
||||
MERGE (user)-[:PRIMARY_EMAIL]->(email)
|
||||
MERGE (user)<-[:BELONGS_TO]-(email)
|
||||
${optionalMerge}
|
||||
SET user += $args
|
||||
SET user.id = randomUUID()
|
||||
@ -95,6 +95,13 @@ const signupCypher = (inviteCode) => {
|
||||
SET user.showShoutsPublicly = false
|
||||
SET user.sendNotificationEmails = true
|
||||
SET email.verifiedAt = toString(datetime())
|
||||
WITH user
|
||||
OPTIONAL MATCH (post:Post)-[:IN]->(group:Group)
|
||||
WHERE NOT group.groupType = 'public'
|
||||
WITH user, collect(post) AS invisiblePosts
|
||||
FOREACH (invisiblePost IN invisiblePosts |
|
||||
MERGE (user)-[:CANNOT_SEE]->(invisiblePost)
|
||||
)
|
||||
RETURN user {.*}
|
||||
`
|
||||
return cypher
|
||||
|
||||
@ -61,7 +61,7 @@ describe('Signup', () => {
|
||||
CONFIG.INVITE_REGISTRATION = false
|
||||
CONFIG.PUBLIC_REGISTRATION = false
|
||||
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -130,7 +130,7 @@ describe('file a report on a resource', () => {
|
||||
authenticatedUser = null
|
||||
await expect(mutate({ mutation: fileReportMutation, variables })).resolves.toMatchObject({
|
||||
data: { fileReport: null },
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -729,7 +729,7 @@ describe('file a report on a resource', () => {
|
||||
authenticatedUser = null
|
||||
expect(query({ query: reportsQuery })).resolves.toMatchObject({
|
||||
data: { reports: null },
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -739,7 +739,7 @@ describe('file a report on a resource', () => {
|
||||
authenticatedUser = await currentUser.toJson()
|
||||
expect(query({ query: reportsQuery })).resolves.toMatchObject({
|
||||
data: { reports: null },
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -97,7 +97,7 @@ describe('rewards', () => {
|
||||
authenticatedUser = null
|
||||
await expect(mutate({ mutation: rewardMutation, variables })).resolves.toMatchObject({
|
||||
data: { reward: null },
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -255,7 +255,7 @@ describe('rewards', () => {
|
||||
it('throws authorization error', async () => {
|
||||
await expect(mutate({ mutation: rewardMutation, variables })).resolves.toMatchObject({
|
||||
data: { reward: null },
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -308,7 +308,7 @@ describe('rewards', () => {
|
||||
authenticatedUser = null
|
||||
await expect(mutate({ mutation: unrewardMutation, variables })).resolves.toMatchObject({
|
||||
data: { unreward: null },
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -341,7 +341,7 @@ describe('rewards', () => {
|
||||
it('throws authorization error', async () => {
|
||||
await expect(mutate({ mutation: unrewardMutation, variables })).resolves.toMatchObject({
|
||||
data: { unreward: null },
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -23,12 +23,15 @@ const postWhereClause = `WHERE score >= 0.0
|
||||
AND NOT (
|
||||
author.deleted = true OR author.disabled = true
|
||||
OR resource.deleted = true OR resource.disabled = true
|
||||
OR (:User {id: $userId})-[:MUTED]->(author)
|
||||
)`
|
||||
) AND block IS NULL AND restriction IS NULL`
|
||||
|
||||
const searchPostsSetup = {
|
||||
fulltextIndex: 'post_fulltext_search',
|
||||
match: 'MATCH (resource:Post)<-[:WROTE]-(author:User)',
|
||||
match: `MATCH (resource:Post)<-[:WROTE]-(author:User)
|
||||
MATCH (user:User {id: $userId})
|
||||
OPTIONAL MATCH (user)-[block:MUTED]->(author)
|
||||
OPTIONAL MATCH (user)-[restriction:CANNOT_SEE]->(resource)
|
||||
WITH user, resource, author, block, restriction`,
|
||||
whereClause: postWhereClause,
|
||||
withClause: `WITH resource, author,
|
||||
[(resource)<-[:COMMENTS]-(comment:Comment) | comment] AS comments,
|
||||
@ -63,6 +66,21 @@ const searchHashtagsSetup = {
|
||||
limit: 'LIMIT $limit',
|
||||
}
|
||||
|
||||
const searchGroupsSetup = {
|
||||
fulltextIndex: 'group_fulltext_search',
|
||||
match: `MATCH (resource:Group)
|
||||
MATCH (user:User {id: $userId})
|
||||
OPTIONAL MATCH (user)-[membership:MEMBER_OF]->(resource)
|
||||
WITH user, resource, membership`,
|
||||
whereClause: `WHERE score >= 0.0
|
||||
AND NOT (resource.deleted = true OR resource.disabled = true)
|
||||
AND (resource.groupType IN ['public', 'closed']
|
||||
OR membership.role IN ['usual', 'admin', 'owner'])`,
|
||||
withClause: 'WITH resource, membership',
|
||||
returnClause: 'resource { .*, myRole: membership.role, __typename: labels(resource)[0] }',
|
||||
limit: 'LIMIT $limit',
|
||||
}
|
||||
|
||||
const countSetup = {
|
||||
returnClause: 'toString(size(collect(resource)))',
|
||||
limit: '',
|
||||
@ -80,6 +98,10 @@ const countHashtagsSetup = {
|
||||
...searchHashtagsSetup,
|
||||
...countSetup,
|
||||
}
|
||||
const countGroupsSetup = {
|
||||
...searchGroupsSetup,
|
||||
...countSetup,
|
||||
}
|
||||
|
||||
const searchResultPromise = async (session, setup, params) => {
|
||||
return session.readTransaction(async (transaction) => {
|
||||
@ -110,14 +132,15 @@ const multiSearchMap = [
|
||||
{ symbol: '!', setup: searchPostsSetup, resultName: 'posts' },
|
||||
{ symbol: '@', setup: searchUsersSetup, resultName: 'users' },
|
||||
{ symbol: '#', setup: searchHashtagsSetup, resultName: 'hashtags' },
|
||||
{ symbol: '&', setup: searchGroupsSetup, resultName: 'groups' },
|
||||
]
|
||||
|
||||
export default {
|
||||
Query: {
|
||||
searchPosts: async (_parent, args, context, _resolveInfo) => {
|
||||
const { query, postsOffset, firstPosts } = args
|
||||
const { id: userId } = context.user
|
||||
|
||||
let userId = null
|
||||
if (context.user) userId = context.user.id
|
||||
return {
|
||||
postCount: getSearchResults(
|
||||
context,
|
||||
@ -175,12 +198,36 @@ export default {
|
||||
}),
|
||||
}
|
||||
},
|
||||
searchGroups: async (_parent, args, context, _resolveInfo) => {
|
||||
const { query, groupsOffset, firstGroups } = args
|
||||
let userId = null
|
||||
if (context.user) userId = context.user.id
|
||||
return {
|
||||
groupCount: getSearchResults(
|
||||
context,
|
||||
countGroupsSetup,
|
||||
{
|
||||
query: queryString(query),
|
||||
skip: 0,
|
||||
userId,
|
||||
},
|
||||
countResultCallback,
|
||||
),
|
||||
groups: getSearchResults(context, searchGroupsSetup, {
|
||||
query: queryString(query),
|
||||
skip: groupsOffset,
|
||||
limit: firstGroups,
|
||||
userId,
|
||||
}),
|
||||
}
|
||||
},
|
||||
searchResults: async (_parent, args, context, _resolveInfo) => {
|
||||
const { query, limit } = args
|
||||
const { id: userId } = context.user
|
||||
let userId = null
|
||||
if (context.user) userId = context.user.id
|
||||
|
||||
const searchType = query.replace(/^([!@#]?).*$/, '$1')
|
||||
const searchString = query.replace(/^([!@#])/, '')
|
||||
const searchType = query.replace(/^([!@#&]?).*$/, '$1')
|
||||
const searchString = query.replace(/^([!@#&])/, '')
|
||||
|
||||
const params = {
|
||||
query: queryString(searchString),
|
||||
@ -193,6 +240,7 @@ export default {
|
||||
return [
|
||||
...(await getSearchResults(context, searchPostsSetup, params)),
|
||||
...(await getSearchResults(context, searchUsersSetup, params)),
|
||||
...(await getSearchResults(context, searchGroupsSetup, params)),
|
||||
...(await getSearchResults(context, searchHashtagsSetup, params)),
|
||||
]
|
||||
|
||||
|
||||
@ -90,7 +90,7 @@ describe('shout and unshout posts', () => {
|
||||
variables = { id: 'post-to-shout-id' }
|
||||
authenticatedUser = undefined
|
||||
await expect(mutate({ mutation: mutationShoutPost, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -165,7 +165,7 @@ describe('shout and unshout posts', () => {
|
||||
authenticatedUser = undefined
|
||||
variables = { id: 'post-to-shout-id' }
|
||||
await expect(mutate({ mutation: mutationUnshoutPost, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -94,7 +94,7 @@ describe('SocialMedia', () => {
|
||||
const user = null
|
||||
const result = await socialMediaAction(user, mutation, variables)
|
||||
|
||||
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
|
||||
@ -186,7 +186,7 @@ describe('SocialMedia', () => {
|
||||
const user = null
|
||||
const result = await socialMediaAction(user, mutation, variables)
|
||||
|
||||
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
|
||||
@ -195,7 +195,7 @@ describe('SocialMedia', () => {
|
||||
const user = someUser
|
||||
const result = await socialMediaAction(user, mutation, variables)
|
||||
|
||||
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
|
||||
@ -222,7 +222,7 @@ describe('SocialMedia', () => {
|
||||
variables.id = 'some-id'
|
||||
const result = await socialMediaAction(user, mutation, variables)
|
||||
|
||||
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -249,7 +249,7 @@ describe('SocialMedia', () => {
|
||||
const user = null
|
||||
const result = await socialMediaAction(user, mutation, variables)
|
||||
|
||||
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
|
||||
@ -258,7 +258,7 @@ describe('SocialMedia', () => {
|
||||
const user = someUser
|
||||
const result = await socialMediaAction(user, mutation, variables)
|
||||
|
||||
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -20,16 +20,22 @@ export default {
|
||||
const result = await transaction.run(
|
||||
`
|
||||
MATCH (user:User {id: $id})
|
||||
WITH user, [(user)<-[:OWNED_BY]-(medium:SocialMedia) | properties(medium) ] as media
|
||||
RETURN user {.*, socialMedia: media } as user
|
||||
OPTIONAL MATCH (category:Category) WHERE NOT ((user)-[:NOT_INTERESTED_IN]->(category))
|
||||
OPTIONAL MATCH (cats:Category)
|
||||
WITH user, [(user)<-[:OWNED_BY]-(medium:SocialMedia) | properties(medium) ] AS media, category, toString(COUNT(cats)) AS categoryCount
|
||||
RETURN user {.*, socialMedia: media, activeCategories: collect(category.id) } AS user, categoryCount
|
||||
`,
|
||||
{ id: user.id },
|
||||
)
|
||||
log(result)
|
||||
return result.records.map((record) => record.get('user'))
|
||||
const [categoryCount] = result.records.map((record) => record.get('categoryCount'))
|
||||
const [currentUser] = result.records.map((record) => record.get('user'))
|
||||
// frontend expects empty array when all categories are selected
|
||||
if (currentUser.activeCategories.length === parseInt(categoryCount))
|
||||
currentUser.activeCategories = []
|
||||
return currentUser
|
||||
})
|
||||
try {
|
||||
const [currentUser] = await currentUserTransactionPromise
|
||||
const currentUser = await currentUserTransactionPromise
|
||||
return currentUser
|
||||
} finally {
|
||||
session.close()
|
||||
|
||||
@ -2,10 +2,12 @@ import jwt from 'jsonwebtoken'
|
||||
import CONFIG from './../../config'
|
||||
import Factory, { cleanDatabase } from '../../db/factories'
|
||||
import { gql } from '../../helpers/jest'
|
||||
import { loginMutation } from '../../db/graphql/userManagement'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import createServer, { context } from '../../server'
|
||||
import encode from '../../jwt/encode'
|
||||
import { getNeode } from '../../db/neo4j'
|
||||
import { categories } from '../../constants/categories'
|
||||
|
||||
const neode = getNeode()
|
||||
let query, mutate, variables, req, user
|
||||
@ -118,6 +120,7 @@ describe('currentUser', () => {
|
||||
}
|
||||
email
|
||||
role
|
||||
activeCategories
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -172,17 +175,57 @@ describe('currentUser', () => {
|
||||
}
|
||||
await respondsWith(expected)
|
||||
})
|
||||
|
||||
describe('with categories in DB', () => {
|
||||
beforeEach(async () => {
|
||||
await Promise.all(
|
||||
categories.map(async ({ icon, name }, index) => {
|
||||
await Factory.build('category', {
|
||||
id: `cat${index + 1}`,
|
||||
slug: name,
|
||||
name,
|
||||
icon,
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('returns empty array for all categories', async () => {
|
||||
await respondsWith({
|
||||
data: {
|
||||
currentUser: expect.objectContaining({ activeCategories: [] }),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('with categories saved for current user', () => {
|
||||
const saveCategorySettings = gql`
|
||||
mutation ($activeCategories: [String]) {
|
||||
saveCategorySettings(activeCategories: $activeCategories)
|
||||
}
|
||||
`
|
||||
beforeEach(async () => {
|
||||
await mutate({
|
||||
mutation: saveCategorySettings,
|
||||
variables: { activeCategories: ['cat1', 'cat3', 'cat5', 'cat7'] },
|
||||
})
|
||||
})
|
||||
|
||||
it('returns only the saved active categories', async () => {
|
||||
const result = await query({ query: currentUserQuery, variables })
|
||||
expect(result.data.currentUser.activeCategories).toHaveLength(4)
|
||||
expect(result.data.currentUser.activeCategories).toContain('cat1')
|
||||
expect(result.data.currentUser.activeCategories).toContain('cat3')
|
||||
expect(result.data.currentUser.activeCategories).toContain('cat5')
|
||||
expect(result.data.currentUser.activeCategories).toContain('cat7')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('login', () => {
|
||||
const loginMutation = gql`
|
||||
mutation ($email: String!, $password: String!) {
|
||||
login(email: $email, password: $password)
|
||||
}
|
||||
`
|
||||
|
||||
const respondsWith = async (expected) => {
|
||||
await expect(mutate({ mutation: loginMutation, variables })).resolves.toMatchObject(expected)
|
||||
}
|
||||
@ -310,8 +353,8 @@ describe('change password', () => {
|
||||
})
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('throws "Not Authorised!"', async () => {
|
||||
await respondsWith({ errors: [{ message: 'Not Authorised!' }] })
|
||||
it('throws "Not Authorized!"', async () => {
|
||||
await respondsWith({ errors: [{ message: 'Not Authorized!' }] })
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import { UserInputError, ForbiddenError } from 'apollo-server'
|
||||
import { mergeImage, deleteImage } from './images/images'
|
||||
import Resolver from './helpers/Resolver'
|
||||
import log from './helpers/databaseLogger'
|
||||
import createOrUpdateLocations from './users/location'
|
||||
import { createOrUpdateLocations } from './users/location'
|
||||
|
||||
const neode = getNeode()
|
||||
|
||||
@ -139,9 +139,10 @@ export default {
|
||||
return blockedUser.toJson()
|
||||
},
|
||||
UpdateUser: async (_parent, params, context, _resolveInfo) => {
|
||||
const { termsAndConditionsAgreedVersion } = params
|
||||
const { avatar: avatarInput } = params
|
||||
delete params.avatar
|
||||
params.locationName = params.locationName === '' ? null : params.locationName
|
||||
const { termsAndConditionsAgreedVersion } = params
|
||||
if (termsAndConditionsAgreedVersion) {
|
||||
const regEx = new RegExp(/^[0-9]+\.[0-9]+\.[0-9]+$/g)
|
||||
if (!regEx.test(termsAndConditionsAgreedVersion)) {
|
||||
@ -169,7 +170,8 @@ export default {
|
||||
})
|
||||
try {
|
||||
const user = await writeTxResultPromise
|
||||
await createOrUpdateLocations(params.id, params.locationName, session)
|
||||
// TODO: put in a middleware, see "CreateGroup", "UpdateGroup"
|
||||
await createOrUpdateLocations('User', params.id, params.locationName, session)
|
||||
return user
|
||||
} catch (error) {
|
||||
throw new UserInputError(error.message)
|
||||
@ -269,6 +271,49 @@ export default {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
saveCategorySettings: async (object, args, context, resolveInfo) => {
|
||||
const { activeCategories } = args
|
||||
const {
|
||||
user: { id },
|
||||
} = context
|
||||
|
||||
const session = context.driver.session()
|
||||
await session.writeTransaction((transaction) => {
|
||||
return transaction.run(
|
||||
`
|
||||
MATCH (user:User { id: $id })-[previousCategories:NOT_INTERESTED_IN]->(category:Category)
|
||||
DELETE previousCategories
|
||||
RETURN user, category
|
||||
`,
|
||||
{ id },
|
||||
)
|
||||
})
|
||||
|
||||
// frontend gives [] when all categories are selected (default)
|
||||
if (activeCategories.length === 0) return true
|
||||
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
const saveCategorySettingsResponse = await transaction.run(
|
||||
`
|
||||
MATCH (category:Category) WHERE NOT category.id IN $activeCategories
|
||||
MATCH (user:User { id: $id })
|
||||
MERGE (user)-[r:NOT_INTERESTED_IN]->(category)
|
||||
RETURN user, r, category
|
||||
`,
|
||||
{ id, activeCategories },
|
||||
)
|
||||
const [user] = await saveCategorySettingsResponse.records.map((record) =>
|
||||
record.get('user'),
|
||||
)
|
||||
return user
|
||||
})
|
||||
try {
|
||||
await writeTxResultPromise
|
||||
return true
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
},
|
||||
User: {
|
||||
email: async (parent, params, context, resolveInfo) => {
|
||||
|
||||
@ -3,6 +3,7 @@ import { gql } from '../../helpers/jest'
|
||||
import { getNeode, getDriver } from '../../db/neo4j'
|
||||
import createServer from '../../server'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import { categories } from '../../constants/categories'
|
||||
|
||||
const categoryIds = ['cat9']
|
||||
let user
|
||||
@ -45,7 +46,7 @@ const deleteUserMutation = gql`
|
||||
}
|
||||
`
|
||||
const switchUserRoleMutation = gql`
|
||||
mutation ($role: UserGroup!, $id: ID!) {
|
||||
mutation ($role: UserRole!, $id: ID!) {
|
||||
switchUserRole(role: $role, id: $id) {
|
||||
name
|
||||
role
|
||||
@ -56,6 +57,12 @@ const switchUserRoleMutation = gql`
|
||||
}
|
||||
`
|
||||
|
||||
const saveCategorySettings = gql`
|
||||
mutation ($activeCategories: [String]) {
|
||||
saveCategorySettings(activeCategories: $activeCategories)
|
||||
}
|
||||
`
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
|
||||
@ -101,7 +108,7 @@ describe('User', () => {
|
||||
|
||||
it('is forbidden', async () => {
|
||||
await expect(query({ query: userQuery, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
|
||||
@ -154,7 +161,7 @@ describe('UpdateUser', () => {
|
||||
$id: ID!
|
||||
$name: String
|
||||
$termsAndConditionsAgreedVersion: String
|
||||
$locationName: String
|
||||
$locationName: String # empty string '' sets it to null
|
||||
) {
|
||||
UpdateUser(
|
||||
id: $id
|
||||
@ -167,6 +174,11 @@ describe('UpdateUser', () => {
|
||||
termsAndConditionsAgreedVersion
|
||||
termsAndConditionsAgreedAt
|
||||
locationName
|
||||
location {
|
||||
name
|
||||
nameDE
|
||||
nameEN
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -207,7 +219,7 @@ describe('UpdateUser', () => {
|
||||
|
||||
it('is not allowed to change other user accounts', async () => {
|
||||
const { errors } = await mutate({ mutation: updateUserMutation, variables })
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
|
||||
@ -282,11 +294,39 @@ describe('UpdateUser', () => {
|
||||
expect(errors[0]).toHaveProperty('message', 'Invalid version format!')
|
||||
})
|
||||
|
||||
it('supports updating location', async () => {
|
||||
variables = { ...variables, locationName: 'Hamburg, New Jersey, United States' }
|
||||
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject({
|
||||
data: { UpdateUser: { locationName: 'Hamburg, New Jersey, United States' } },
|
||||
errors: undefined,
|
||||
describe('supports updating location', () => {
|
||||
describe('change location to "Hamburg, New Jersey, United States"', () => {
|
||||
it('has updated location to "Hamburg, New Jersey, United States"', async () => {
|
||||
variables = { ...variables, locationName: 'Hamburg, New Jersey, United States' }
|
||||
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
UpdateUser: {
|
||||
locationName: 'Hamburg, New Jersey, United States',
|
||||
location: expect.objectContaining({
|
||||
name: 'Hamburg',
|
||||
nameDE: 'Hamburg',
|
||||
nameEN: 'Hamburg',
|
||||
}),
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('change location to unset location', () => {
|
||||
it('has updated location to unset location', async () => {
|
||||
variables = { ...variables, locationName: '' }
|
||||
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
UpdateUser: {
|
||||
locationName: null,
|
||||
location: null,
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -500,7 +540,7 @@ describe('switch user role', () => {
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
expect.objectContaining({
|
||||
message: 'Not Authorised!',
|
||||
message: 'Not Authorized!',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
@ -544,3 +584,140 @@ describe('switch user role', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('save category settings', () => {
|
||||
beforeEach(async () => {
|
||||
await Promise.all(
|
||||
categories.map(({ icon, name }, index) => {
|
||||
Factory.build('category', {
|
||||
id: `cat${index + 1}`,
|
||||
slug: name,
|
||||
name,
|
||||
icon,
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await Factory.build('user', {
|
||||
id: 'user',
|
||||
role: 'user',
|
||||
})
|
||||
variables = {
|
||||
activeCategories: ['cat1', 'cat3', 'cat5'],
|
||||
}
|
||||
})
|
||||
|
||||
describe('not authenticated', () => {
|
||||
beforeEach(async () => {
|
||||
authenticatedUser = undefined
|
||||
})
|
||||
|
||||
it('throws an error', async () => {
|
||||
await expect(mutate({ mutation: saveCategorySettings, variables })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
expect.objectContaining({
|
||||
message: 'Not Authorized!',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
beforeEach(async () => {
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
const userQuery = gql`
|
||||
query ($id: ID) {
|
||||
User(id: $id) {
|
||||
activeCategories
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
describe('no categories saved', () => {
|
||||
it('returns true for active categories mutation', async () => {
|
||||
await expect(mutate({ mutation: saveCategorySettings, variables })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: { saveCategorySettings: true },
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
describe('query for user', () => {
|
||||
beforeEach(async () => {
|
||||
await mutate({ mutation: saveCategorySettings, variables })
|
||||
})
|
||||
|
||||
it('returns the active categories when user is queried', async () => {
|
||||
await expect(
|
||||
query({ query: userQuery, variables: { id: authenticatedUser.id } }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
User: [
|
||||
{
|
||||
activeCategories: expect.arrayContaining(['cat1', 'cat3', 'cat5']),
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('categories already saved', () => {
|
||||
beforeEach(async () => {
|
||||
variables = {
|
||||
activeCategories: ['cat1', 'cat3', 'cat5'],
|
||||
}
|
||||
await mutate({ mutation: saveCategorySettings, variables })
|
||||
variables = {
|
||||
activeCategories: ['cat10', 'cat11', 'cat12', 'cat8', 'cat9'],
|
||||
}
|
||||
})
|
||||
|
||||
it('returns true', async () => {
|
||||
await expect(mutate({ mutation: saveCategorySettings, variables })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: { saveCategorySettings: true },
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
describe('query for user', () => {
|
||||
beforeEach(async () => {
|
||||
await mutate({ mutation: saveCategorySettings, variables })
|
||||
})
|
||||
|
||||
it('returns the new active categories when user is queried', async () => {
|
||||
await expect(
|
||||
query({ query: userQuery, variables: { id: authenticatedUser.id } }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
User: [
|
||||
{
|
||||
activeCategories: expect.arrayContaining([
|
||||
'cat10',
|
||||
'cat11',
|
||||
'cat12',
|
||||
'cat8',
|
||||
'cat9',
|
||||
]),
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import request from 'request'
|
||||
import { UserInputError } from 'apollo-server'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import Debug from 'debug'
|
||||
import asyncForEach from '../../../helpers/asyncForEach'
|
||||
import CONFIG from '../../../config'
|
||||
@ -62,77 +61,86 @@ const createLocation = async (session, mapboxData) => {
|
||||
})
|
||||
}
|
||||
|
||||
const createOrUpdateLocations = async (userId, locationName, session) => {
|
||||
if (isEmpty(locationName)) {
|
||||
return
|
||||
}
|
||||
const res = await fetch(
|
||||
`https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(
|
||||
locationName,
|
||||
)}.json?access_token=${CONFIG.MAPBOX_TOKEN}&types=region,place,country&language=${locales.join(
|
||||
',',
|
||||
)}`,
|
||||
)
|
||||
export const createOrUpdateLocations = async (nodeLabel, nodeId, locationName, session) => {
|
||||
if (locationName === undefined) return
|
||||
|
||||
debug(res)
|
||||
let locationId
|
||||
|
||||
if (!res || !res.features || !res.features[0]) {
|
||||
throw new UserInputError('locationName is invalid')
|
||||
}
|
||||
if (locationName !== null) {
|
||||
const res = await fetch(
|
||||
`https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(
|
||||
locationName,
|
||||
)}.json?access_token=${
|
||||
CONFIG.MAPBOX_TOKEN
|
||||
}&types=region,place,country&language=${locales.join(',')}`,
|
||||
)
|
||||
|
||||
let data
|
||||
debug(res)
|
||||
|
||||
res.features.forEach((item) => {
|
||||
if (item.matching_place_name === locationName) {
|
||||
data = item
|
||||
if (!res || !res.features || !res.features[0]) {
|
||||
throw new UserInputError('locationName is invalid')
|
||||
}
|
||||
})
|
||||
if (!data) {
|
||||
data = res.features[0]
|
||||
}
|
||||
|
||||
if (!data || !data.place_type || !data.place_type.length) {
|
||||
throw new UserInputError('locationName is invalid')
|
||||
}
|
||||
let data
|
||||
|
||||
if (data.place_type.length > 1) {
|
||||
data.id = 'region.' + data.id.split('.')[1]
|
||||
}
|
||||
await createLocation(session, data)
|
||||
|
||||
let parent = data
|
||||
|
||||
if (data.context) {
|
||||
await asyncForEach(data.context, async (ctx) => {
|
||||
await createLocation(session, ctx)
|
||||
await session.writeTransaction((transaction) => {
|
||||
return transaction.run(
|
||||
`
|
||||
MATCH (parent:Location {id: $parentId}), (child:Location {id: $childId})
|
||||
MERGE (child)<-[:IS_IN]-(parent)
|
||||
RETURN child.id, parent.id
|
||||
`,
|
||||
{
|
||||
parentId: parent.id,
|
||||
childId: ctx.id,
|
||||
},
|
||||
)
|
||||
})
|
||||
parent = ctx
|
||||
res.features.forEach((item) => {
|
||||
if (item.matching_place_name === locationName) {
|
||||
data = item
|
||||
}
|
||||
})
|
||||
if (!data) {
|
||||
data = res.features[0]
|
||||
}
|
||||
|
||||
if (!data || !data.place_type || !data.place_type.length) {
|
||||
throw new UserInputError('locationName is invalid')
|
||||
}
|
||||
|
||||
if (data.place_type.length > 1) {
|
||||
data.id = 'region.' + data.id.split('.')[1]
|
||||
}
|
||||
await createLocation(session, data)
|
||||
|
||||
let parent = data
|
||||
|
||||
if (data.context) {
|
||||
await asyncForEach(data.context, async (ctx) => {
|
||||
await createLocation(session, ctx)
|
||||
await session.writeTransaction((transaction) => {
|
||||
return transaction.run(
|
||||
`
|
||||
MATCH (parent:Location {id: $parentId}), (child:Location {id: $childId})
|
||||
MERGE (child)<-[:IS_IN]-(parent)
|
||||
RETURN child.id, parent.id
|
||||
`,
|
||||
{
|
||||
parentId: parent.id,
|
||||
childId: ctx.id,
|
||||
},
|
||||
)
|
||||
})
|
||||
parent = ctx
|
||||
})
|
||||
}
|
||||
|
||||
locationId = data.id
|
||||
} else {
|
||||
locationId = 'non-existent-id'
|
||||
}
|
||||
// delete all current locations from user and add new location
|
||||
|
||||
// delete all current locations from node and add new location
|
||||
await session.writeTransaction((transaction) => {
|
||||
return transaction.run(
|
||||
`
|
||||
MATCH (user:User {id: $userId})-[relationship:IS_IN]->(location:Location)
|
||||
DETACH DELETE relationship
|
||||
WITH user
|
||||
MATCH (location:Location {id: $locationId})
|
||||
MERGE (user)-[:IS_IN]->(location)
|
||||
RETURN location.id, user.id
|
||||
`,
|
||||
{ userId: userId, locationId: data.id },
|
||||
MATCH (node:${nodeLabel} {id: $nodeId})
|
||||
OPTIONAL MATCH (node)-[relationship:IS_IN]->(:Location)
|
||||
DELETE relationship
|
||||
WITH node
|
||||
MATCH (location:Location {id: $locationId})
|
||||
MERGE (node)-[:IS_IN]->(location)
|
||||
RETURN location.id, node.id
|
||||
`,
|
||||
{ nodeId, locationId },
|
||||
)
|
||||
})
|
||||
}
|
||||
@ -147,5 +155,3 @@ export const queryLocations = async ({ place, lang }) => {
|
||||
}
|
||||
return res.features
|
||||
}
|
||||
|
||||
export default createOrUpdateLocations
|
||||
|
||||
@ -121,7 +121,7 @@ describe('Location Service', () => {
|
||||
const result = await query({ query: queryLocations, variables })
|
||||
expect(result.data.queryLocations).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ id: 'place.14094307404564380', place_name: 'Berlin, Germany' },
|
||||
{ id: expect.stringMatching(/^place\.[0-9]+$/), place_name: 'Berlin, Germany' },
|
||||
{
|
||||
id: expect.stringMatching(/^place\.[0-9]+$/),
|
||||
place_name: 'Berlin, Maryland, United States',
|
||||
|
||||
@ -58,7 +58,7 @@ describe('mutedUsers', () => {
|
||||
it('throws permission error', async () => {
|
||||
const { query } = createTestClient(server)
|
||||
const result = await query({ query: mutedUserQuery })
|
||||
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
|
||||
describe('authenticated and given a muted user', () => {
|
||||
@ -116,7 +116,7 @@ describe('muteUser', () => {
|
||||
|
||||
it('throws permission error', async () => {
|
||||
const result = await muteAction({ id: 'u2' })
|
||||
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
@ -333,7 +333,7 @@ describe('unmuteUser', () => {
|
||||
|
||||
it('throws permission error', async () => {
|
||||
const result = await unmuteAction({ id: 'u2' })
|
||||
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
|
||||
7
backend/src/schema/types/enum/GroupActionRadius.gql
Normal file
7
backend/src/schema/types/enum/GroupActionRadius.gql
Normal file
@ -0,0 +1,7 @@
|
||||
enum GroupActionRadius {
|
||||
regional
|
||||
national
|
||||
continental
|
||||
global
|
||||
interplanetary
|
||||
}
|
||||
6
backend/src/schema/types/enum/GroupMemberRole.gql
Normal file
6
backend/src/schema/types/enum/GroupMemberRole.gql
Normal file
@ -0,0 +1,6 @@
|
||||
enum GroupMemberRole {
|
||||
pending
|
||||
usual
|
||||
admin
|
||||
owner
|
||||
}
|
||||
5
backend/src/schema/types/enum/GroupType.gql
Normal file
5
backend/src/schema/types/enum/GroupType.gql
Normal file
@ -0,0 +1,5 @@
|
||||
enum GroupType {
|
||||
public
|
||||
closed
|
||||
hidden
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
enum UserGroup {
|
||||
enum UserRole {
|
||||
admin
|
||||
moderator
|
||||
user
|
||||
135
backend/src/schema/types/type/Group.gql
Normal file
135
backend/src/schema/types/type/Group.gql
Normal file
@ -0,0 +1,135 @@
|
||||
enum _GroupOrdering {
|
||||
id_asc
|
||||
id_desc
|
||||
name_asc
|
||||
name_desc
|
||||
slug_asc
|
||||
slug_desc
|
||||
locationName_asc
|
||||
locationName_desc
|
||||
about_asc
|
||||
about_desc
|
||||
createdAt_asc
|
||||
createdAt_desc
|
||||
updatedAt_asc
|
||||
updatedAt_desc
|
||||
}
|
||||
|
||||
type Group {
|
||||
id: ID!
|
||||
name: String! # title
|
||||
slug: String!
|
||||
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
deleted: Boolean
|
||||
disabled: Boolean
|
||||
|
||||
avatar: Image @relation(name: "AVATAR_IMAGE", direction: "OUT")
|
||||
|
||||
about: String # goal
|
||||
description: String!
|
||||
descriptionExcerpt: String!
|
||||
groupType: GroupType!
|
||||
actionRadius: GroupActionRadius!
|
||||
|
||||
locationName: String
|
||||
location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
|
||||
|
||||
categories: [Category] @relation(name: "CATEGORIZED", direction: "OUT")
|
||||
|
||||
myRole: GroupMemberRole # if 'null' then the current user is no member
|
||||
|
||||
posts: [Post] @relation(name: "IN", direction: "IN")
|
||||
}
|
||||
|
||||
|
||||
input _GroupFilter {
|
||||
AND: [_GroupFilter!]
|
||||
OR: [_GroupFilter!]
|
||||
name_contains: String
|
||||
slug_contains: String
|
||||
about_contains: String
|
||||
description_contains: String
|
||||
groupType_in: [GroupType!]
|
||||
actionRadius_in: [GroupActionRadius!]
|
||||
myRole_in: [GroupMemberRole!]
|
||||
id: ID
|
||||
id_not: ID
|
||||
id_in: [ID!]
|
||||
id_not_in: [ID!]
|
||||
}
|
||||
|
||||
type Query {
|
||||
Group(
|
||||
isMember: Boolean # if 'undefined' or 'null' then get all groups
|
||||
id: ID
|
||||
slug: String
|
||||
first: Int
|
||||
offset: Int
|
||||
# orderBy: [_GroupOrdering] # not implemented yet
|
||||
# filter: _GroupFilter # not implemented yet
|
||||
): [Group]
|
||||
|
||||
GroupMembers(
|
||||
id: ID!
|
||||
# first: Int # not implemented yet
|
||||
# offset: Int # not implemented yet
|
||||
# orderBy: [_UserOrdering] # not implemented yet
|
||||
# filter: _UserFilter # not implemented yet
|
||||
): [User]
|
||||
|
||||
GroupCount(isMember: Boolean): Int
|
||||
|
||||
# AvailableGroupTypes: [GroupType]!
|
||||
|
||||
# AvailableGroupActionRadii: [GroupActionRadius]!
|
||||
|
||||
# AvailableGroupMemberRoles: [GroupMemberRole]!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
CreateGroup(
|
||||
id: ID
|
||||
name: String!
|
||||
slug: String
|
||||
about: String
|
||||
description: String!
|
||||
groupType: GroupType!
|
||||
actionRadius: GroupActionRadius!
|
||||
categoryIds: [ID]
|
||||
# avatar: ImageInput # a group can not be created with an avatar
|
||||
locationName: String # empty string '' sets it to null
|
||||
): Group
|
||||
|
||||
UpdateGroup(
|
||||
id: ID!
|
||||
name: String
|
||||
slug: String
|
||||
about: String
|
||||
description: String
|
||||
# groupType: GroupType # is not possible at the moment and has to be discussed. may be in the stronger direction: public → closed → hidden
|
||||
actionRadius: GroupActionRadius
|
||||
categoryIds: [ID]
|
||||
avatar: ImageInput # test this as result
|
||||
locationName: String # empty string '' sets it to null
|
||||
): Group
|
||||
|
||||
# DeleteGroup(id: ID!): Group
|
||||
|
||||
JoinGroup(
|
||||
groupId: ID!
|
||||
userId: ID!
|
||||
): User
|
||||
|
||||
LeaveGroup(
|
||||
groupId: ID!
|
||||
userId: ID!
|
||||
): User
|
||||
|
||||
ChangeGroupMemberRole(
|
||||
groupId: ID!
|
||||
userId: ID!
|
||||
roleInGroup: GroupMemberRole!
|
||||
): User
|
||||
}
|
||||
5
backend/src/schema/types/type/MEMBER_OF.gql
Normal file
5
backend/src/schema/types/type/MEMBER_OF.gql
Normal file
@ -0,0 +1,5 @@
|
||||
type MEMBER_OF {
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
role: GroupMemberRole!
|
||||
}
|
||||
@ -81,6 +81,7 @@ input _PostFilter {
|
||||
emotions_none: _PostEMOTEDFilter
|
||||
emotions_single: _PostEMOTEDFilter
|
||||
emotions_every: _PostEMOTEDFilter
|
||||
group: _GroupFilter
|
||||
}
|
||||
|
||||
enum _PostOrdering {
|
||||
@ -167,6 +168,8 @@ type Post {
|
||||
emotions: [EMOTED]
|
||||
emotionsCount: Int!
|
||||
@cypher(statement: "MATCH (this)<-[emoted:EMOTED]-(:User) RETURN COUNT(DISTINCT emoted)")
|
||||
|
||||
group: Group @relation(name: "IN", direction: "OUT")
|
||||
}
|
||||
|
||||
input _PostInput {
|
||||
@ -184,6 +187,7 @@ type Mutation {
|
||||
language: String
|
||||
categoryIds: [ID]
|
||||
contentExcerpt: String
|
||||
groupId: ID
|
||||
): Post
|
||||
UpdatePost(
|
||||
id: ID!
|
||||
@ -225,18 +229,4 @@ type Query {
|
||||
PostsEmotionsCountByEmotion(postId: ID!, data: _EMOTEDInput!): Int!
|
||||
PostsEmotionsByCurrentUser(postId: ID!): [String]
|
||||
profilePagePosts(filter: _PostFilter, first: Int, offset: Int, orderBy: [_PostOrdering]): [Post]
|
||||
findPosts(query: String!, limit: Int = 10, filter: _PostFilter): [Post]!
|
||||
@cypher(
|
||||
statement: """
|
||||
CALL db.index.fulltext.queryNodes('post_fulltext_search', $query)
|
||||
YIELD node as post, score
|
||||
MATCH (post)<-[:WROTE]-(user:User)
|
||||
WHERE score >= 0.2
|
||||
AND NOT user.deleted = true AND NOT user.disabled = true
|
||||
AND NOT post.deleted = true AND NOT post.disabled = true
|
||||
AND NOT user.id in COALESCE($filter.author_not.id_in, [])
|
||||
RETURN post
|
||||
LIMIT $limit
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
union SearchResult = Post | User | Tag
|
||||
union SearchResult = Post | User | Tag | Group
|
||||
|
||||
type postSearchResults {
|
||||
postCount: Int
|
||||
@ -15,9 +15,15 @@ type hashtagSearchResults {
|
||||
hashtags: [Tag]!
|
||||
}
|
||||
|
||||
type groupSearchResults {
|
||||
groupCount: Int
|
||||
groups: [Group]!
|
||||
}
|
||||
|
||||
type Query {
|
||||
searchPosts(query: String!, firstPosts: Int, postsOffset: Int): postSearchResults!
|
||||
searchUsers(query: String!, firstUsers: Int, usersOffset: Int): userSearchResults!
|
||||
searchGroups(query: String!, firstGroups: Int, groupsOffset: Int): groupSearchResults!
|
||||
searchHashtags(query: String!, firstHashtags: Int, hashtagsOffset: Int): hashtagSearchResults!
|
||||
searchResults(query: String!, limit: Int = 5): [SearchResult]!
|
||||
}
|
||||
|
||||
@ -28,13 +28,13 @@ type User {
|
||||
avatar: Image @relation(name: "AVATAR_IMAGE", direction: "OUT")
|
||||
deleted: Boolean
|
||||
disabled: Boolean
|
||||
role: UserGroup!
|
||||
role: UserRole!
|
||||
publicKey: String
|
||||
invitedBy: User @relation(name: "INVITED", direction: "IN")
|
||||
invited: [User] @relation(name: "INVITED", direction: "OUT")
|
||||
|
||||
location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
|
||||
locationName: String
|
||||
location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
|
||||
about: String
|
||||
socialMedia: [SocialMedia]! @relation(name: "OWNED_BY", direction: "IN")
|
||||
|
||||
@ -114,6 +114,16 @@ type User {
|
||||
badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)")
|
||||
|
||||
emotions: [EMOTED]
|
||||
|
||||
activeCategories: [String] @cypher(
|
||||
statement: """
|
||||
MATCH (category:Category)
|
||||
WHERE NOT ((this)-[:NOT_INTERESTED_IN]->(category))
|
||||
RETURN collect(category.id)
|
||||
"""
|
||||
)
|
||||
|
||||
myRoleInGroup: GroupMemberRole
|
||||
}
|
||||
|
||||
|
||||
@ -151,43 +161,31 @@ input _UserFilter {
|
||||
followedBy_none: _UserFilter
|
||||
followedBy_single: _UserFilter
|
||||
followedBy_every: _UserFilter
|
||||
role_in: [UserGroup!]
|
||||
role_in: [UserRole!]
|
||||
}
|
||||
|
||||
type Query {
|
||||
User(
|
||||
id: ID
|
||||
email: String # admins need to search for a user sometimes
|
||||
name: String
|
||||
slug: String
|
||||
role: UserGroup
|
||||
locationName: String
|
||||
about: String
|
||||
createdAt: String
|
||||
updatedAt: String
|
||||
first: Int
|
||||
offset: Int
|
||||
orderBy: [_UserOrdering]
|
||||
filter: _UserFilter
|
||||
id: ID
|
||||
email: String # admins need to search for a user sometimes
|
||||
name: String
|
||||
slug: String
|
||||
role: UserRole
|
||||
locationName: String
|
||||
about: String
|
||||
createdAt: String
|
||||
updatedAt: String
|
||||
first: Int
|
||||
offset: Int
|
||||
orderBy: [_UserOrdering]
|
||||
filter: _UserFilter
|
||||
): [User]
|
||||
|
||||
availableRoles: [UserGroup]!
|
||||
availableRoles: [UserRole]!
|
||||
mutedUsers: [User]
|
||||
blockedUsers: [User]
|
||||
isLoggedIn: Boolean!
|
||||
currentUser: User
|
||||
findUsers(query: String!,limit: Int = 10, filter: _UserFilter): [User]!
|
||||
@cypher(
|
||||
statement: """
|
||||
CALL db.index.fulltext.queryNodes('user_fulltext_search', $query)
|
||||
YIELD node as post, score
|
||||
MATCH (user)
|
||||
WHERE score >= 0.2
|
||||
AND NOT user.deleted = true AND NOT user.disabled = true
|
||||
RETURN user
|
||||
LIMIT $limit
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
enum Deletable {
|
||||
@ -197,19 +195,19 @@ enum Deletable {
|
||||
|
||||
type Mutation {
|
||||
UpdateUser (
|
||||
id: ID!
|
||||
name: String
|
||||
email: String
|
||||
slug: String
|
||||
avatar: ImageInput
|
||||
locationName: String
|
||||
about: String
|
||||
termsAndConditionsAgreedVersion: String
|
||||
termsAndConditionsAgreedAt: String
|
||||
allowEmbedIframes: Boolean
|
||||
showShoutsPublicly: Boolean
|
||||
sendNotificationEmails: Boolean
|
||||
locale: String
|
||||
id: ID!
|
||||
name: String
|
||||
email: String
|
||||
slug: String
|
||||
avatar: ImageInput
|
||||
locationName: String # empty string '' sets it to null
|
||||
about: String
|
||||
termsAndConditionsAgreedVersion: String
|
||||
termsAndConditionsAgreedAt: String
|
||||
allowEmbedIframes: Boolean
|
||||
showShoutsPublicly: Boolean
|
||||
sendNotificationEmails: Boolean
|
||||
locale: String
|
||||
): User
|
||||
|
||||
DeleteUser(id: ID!, resource: [Deletable]): User
|
||||
@ -219,5 +217,7 @@ type Mutation {
|
||||
blockUser(id: ID!): User
|
||||
unblockUser(id: ID!): User
|
||||
|
||||
switchUserRole(role: UserGroup!, id: ID!): User
|
||||
switchUserRole(role: UserRole!, id: ID!): User
|
||||
|
||||
saveCategorySettings(activeCategories: [String]): Boolean
|
||||
}
|
||||
|
||||
@ -997,9 +997,9 @@
|
||||
tslib "1.11.1"
|
||||
|
||||
"@hapi/address@2.x.x":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.2.tgz#1c794cd6dbf2354d1eb1ef10e0303f573e1c7222"
|
||||
integrity sha512-O4QDrx+JoGKZc6aN64L04vqa7e41tIiLU+OvKdcYaEMP97UttL0f9GIi9/0A4WAMx0uBd6SidDIhktZhgOcN8Q==
|
||||
version "2.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
|
||||
integrity sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ==
|
||||
|
||||
"@hapi/address@^4.0.1":
|
||||
version "4.0.1"
|
||||
@ -1018,10 +1018,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@hapi/formula/-/formula-2.0.0.tgz#edade0619ed58c8e4f164f233cda70211e787128"
|
||||
integrity sha512-V87P8fv7PI0LH7LiVi8Lkf3x+KCO7pQozXRssAHNXXL9L1K+uyu4XypLXwxqVDKgyQai6qj3/KteNlrqDx4W5A==
|
||||
|
||||
"@hapi/hoek@8.x.x":
|
||||
version "8.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-8.2.4.tgz#684a14f4ca35d46f44abc87dfc696e5e4fe8a020"
|
||||
integrity sha512-Ze5SDNt325yZvNO7s5C4fXDscjJ6dcqLFXJQ/M7dZRQCewuDj2iDUuBi6jLQt+APbW9RjjVEvLr35FXuOEqjow==
|
||||
"@hapi/hoek@8.x.x", "@hapi/hoek@^8.3.0":
|
||||
version "8.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-8.5.1.tgz#fde96064ca446dec8c55a8c2f130957b070c6e06"
|
||||
integrity sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow==
|
||||
|
||||
"@hapi/hoek@^9.0.0":
|
||||
version "9.0.0"
|
||||
@ -1055,11 +1055,11 @@
|
||||
integrity sha512-vzXR5MY7n4XeIvLpfl3HtE3coZYO4raKXW766R6DZw/6aLqR26iuZ109K7a0NtF2Db0jxqh7xz2AxkUwpUFybw==
|
||||
|
||||
"@hapi/topo@3.x.x":
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-3.1.3.tgz#c7a02e0d936596d29f184e6d7fdc07e8b5efce11"
|
||||
integrity sha512-JmS9/vQK6dcUYn7wc2YZTqzIKubAQcJKu2KCKAru6es482U5RT5fP1EXCPtlXpiK7PR0On/kpQKI4fRKkzpZBQ==
|
||||
version "3.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-3.1.6.tgz#68d935fa3eae7fdd5ab0d7f953f3205d8b2bfc29"
|
||||
integrity sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ==
|
||||
dependencies:
|
||||
"@hapi/hoek" "8.x.x"
|
||||
"@hapi/hoek" "^8.3.0"
|
||||
|
||||
"@hapi/topo@^5.0.0":
|
||||
version "5.0.0"
|
||||
@ -2681,6 +2681,11 @@ base64-js@^1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
|
||||
integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
|
||||
|
||||
base64-js@^1.3.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||
|
||||
base@^0.11.1:
|
||||
version "0.11.2"
|
||||
resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
|
||||
@ -2850,6 +2855,14 @@ buffer@4.9.1:
|
||||
ieee754 "^1.1.4"
|
||||
isarray "^1.0.0"
|
||||
|
||||
buffer@^6.0.3:
|
||||
version "6.0.3"
|
||||
resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
|
||||
integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
|
||||
dependencies:
|
||||
base64-js "^1.3.1"
|
||||
ieee754 "^1.2.1"
|
||||
|
||||
busboy@^0.3.1:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.3.1.tgz#170899274c5bf38aae27d5c62b71268cd585fd1b"
|
||||
@ -3929,7 +3942,7 @@ dot-prop@^4.1.0:
|
||||
dotenv@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d"
|
||||
integrity sha1-hk7xN5rO1Vzm+V3r7NzhefegzR0=
|
||||
integrity sha512-XcaMACOr3JMVcEv0Y/iUM2XaOsATRZ3U1In41/1jjK6vJZ2PZbQ1bzCG8uvaByfaBpl9gqc9QWJovpUGBXLLYQ==
|
||||
|
||||
dotenv@^6.1.0:
|
||||
version "6.2.0"
|
||||
@ -4253,10 +4266,10 @@ eslint-plugin-node@~11.1.0:
|
||||
resolve "^1.10.1"
|
||||
semver "^6.1.0"
|
||||
|
||||
eslint-plugin-prettier@~3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.2.tgz#432e5a667666ab84ce72f945c72f77d996a5c9ba"
|
||||
integrity sha512-GlolCC9y3XZfv3RQfwGew7NnuFDKsfI4lbvRK+PIIo23SFH+LemGs4cKwzAaRa+Mdb+lQO/STaIayno8T5sJJA==
|
||||
eslint-plugin-prettier@~3.4.1:
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.1.tgz#e9ddb200efb6f3d05ffe83b1665a716af4a387e5"
|
||||
integrity sha512-htg25EUYUeIhKHXjOinK4BgCcDwtLHjqaxCDsMy5nbnUMkKFvIhMVCp+5GFUXQ4Nr8lBsPqtGAqBenbpFqAA2g==
|
||||
dependencies:
|
||||
prettier-linter-helpers "^1.0.0"
|
||||
|
||||
@ -5516,6 +5529,11 @@ ieee754@1.1.13, ieee754@^1.1.4:
|
||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
|
||||
integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
|
||||
|
||||
ieee754@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
|
||||
|
||||
ienoopen@1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ienoopen/-/ienoopen-1.1.0.tgz#411e5d530c982287dbdc3bb31e7a9c9e32630974"
|
||||
@ -7528,18 +7546,19 @@ negotiator@0.6.2:
|
||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
|
||||
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
|
||||
|
||||
neo4j-driver-bolt-connection@^4.3.4:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.yarnpkg.com/neo4j-driver-bolt-connection/-/neo4j-driver-bolt-connection-4.3.4.tgz#de642bb6a62ffc6ae2e280dccf21395b4d1705a2"
|
||||
integrity sha512-yxbvwGav+N7EYjcEAINqL6D3CZV+ee2qLInpAhx+iNurwbl3zqtBGiVP79SZ+7tU++y3Q1fW5ofikH06yc+LqQ==
|
||||
neo4j-driver-bolt-connection@^4.4.7:
|
||||
version "4.4.7"
|
||||
resolved "https://registry.yarnpkg.com/neo4j-driver-bolt-connection/-/neo4j-driver-bolt-connection-4.4.7.tgz#0582d54de1f213e60c374209193d1f645ba523ea"
|
||||
integrity sha512-6Q4hCtvWE6gzN64N09UqZqf/3rDl7FUWZZXiVQL0ZRbaMkJpZNC2NmrDIgGXYE05XEEbRBexf2tVv5OTYZYrow==
|
||||
dependencies:
|
||||
neo4j-driver-core "^4.3.4"
|
||||
text-encoding-utf-8 "^1.0.2"
|
||||
buffer "^6.0.3"
|
||||
neo4j-driver-core "^4.4.7"
|
||||
string_decoder "^1.3.0"
|
||||
|
||||
neo4j-driver-core@^4.3.4:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.yarnpkg.com/neo4j-driver-core/-/neo4j-driver-core-4.3.4.tgz#b445a4fbf94dce8441075099bd6ac3133c1cf5ee"
|
||||
integrity sha512-3tn3j6IRUNlpXeehZ9Xv7dLTZPB4a7APaoJ+xhQyMmYQO3ujDM4RFHc0pZcG+GokmaltT5pUCIPTDYx6ODdhcA==
|
||||
neo4j-driver-core@^4.4.7:
|
||||
version "4.4.7"
|
||||
resolved "https://registry.yarnpkg.com/neo4j-driver-core/-/neo4j-driver-core-4.4.7.tgz#d2475e107b3fea2b9d1c36b0c273da5c5a291c37"
|
||||
integrity sha512-NhvVuQYgG7eO/vXxRaoJfkWUNkjvIpmCIS9UWU9Bbhb4V+wCOyX/MVOXqD0Yizhs4eyIkD7x90OXb79q+vi+oA==
|
||||
|
||||
neo4j-driver@^4.0.1, neo4j-driver@^4.0.2:
|
||||
version "4.0.2"
|
||||
@ -7552,13 +7571,13 @@ neo4j-driver@^4.0.1, neo4j-driver@^4.0.2:
|
||||
uri-js "^4.2.2"
|
||||
|
||||
neo4j-driver@^4.2.2:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-4.3.4.tgz#a54f0562f868ee94dff7509df74e3eb2c1f95a85"
|
||||
integrity sha512-AGrsFFqnoZv4KhJdmKt4mOBV5mnxmV3+/t8KJTOM68jQuEWoy+RlmAaRRaCSU4eY586OFN/R8lg9MrJpZdSFjw==
|
||||
version "4.4.7"
|
||||
resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-4.4.7.tgz#51b3fb48241e66eb3be94e90032cc494c44e59f3"
|
||||
integrity sha512-N7GddPhp12gVJe4eB84u5ik5SmrtRv8nH3rK47Qy7IUKnJkVEos/F1QjOJN6zt1jLnDXwDcGzCKK8XklYpzogw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.5.5"
|
||||
neo4j-driver-bolt-connection "^4.3.4"
|
||||
neo4j-driver-core "^4.3.4"
|
||||
neo4j-driver-bolt-connection "^4.4.7"
|
||||
neo4j-driver-core "^4.4.7"
|
||||
rxjs "^6.6.3"
|
||||
|
||||
neo4j-graphql-js@^2.11.5:
|
||||
@ -7574,10 +7593,10 @@ neo4j-graphql-js@^2.11.5:
|
||||
lodash "^4.17.15"
|
||||
neo4j-driver "^4.0.1"
|
||||
|
||||
neode@^0.4.7:
|
||||
version "0.4.7"
|
||||
resolved "https://registry.yarnpkg.com/neode/-/neode-0.4.7.tgz#033007b57a2ee167e9ee5537493086db08d005eb"
|
||||
integrity sha512-YXlc187JRpeKCBcUIkY6nimXXG+Tvlopfe71/FPno2THrwmYt5mm0RPHZ+mXF2O1Xg6zvjKvOpCpDz2vHBfroQ==
|
||||
neode@^0.4.8:
|
||||
version "0.4.8"
|
||||
resolved "https://registry.yarnpkg.com/neode/-/neode-0.4.8.tgz#0889b4fc7f1bf0b470b01fa5b8870373b5d47ad6"
|
||||
integrity sha512-pb91NfCOg4Fj5o+98H+S2XYC+ByQfbdhwcc1UVuzuUQ0Ezzj+jWz8NmKWU8ZfCH6l4plk71yDAPd2eTwpt+Xvg==
|
||||
dependencies:
|
||||
"@hapi/joi" "^15.1.1"
|
||||
dotenv "^4.0.0"
|
||||
@ -9262,10 +9281,10 @@ slug@^0.9.2:
|
||||
dependencies:
|
||||
unicode ">= 0.3.1"
|
||||
|
||||
slug@~4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/slug/-/slug-4.0.2.tgz#35a62b4e71582778ac08bb30a1bf439fd0a43ea7"
|
||||
integrity sha512-c5XbWkwxHU13gAdSvBHQgnGy2sxv/REMz0ugcM0SOSBCO/N4wfU0TDBC3pgdOwVGjZwGnLBTRljXzdVYE+KYNw==
|
||||
slug@~6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/slug/-/slug-6.0.0.tgz#39637b32e5a873bc692812a630842880499ed6c9"
|
||||
integrity sha512-0MpNLyCSUSf0G1nAZmp9gY1cvesPP35a1Live25vZ23gWQ5SAopF0N+0hk9KI4ytNuTebJrHGNrgTnxboofcSg==
|
||||
|
||||
smart-buffer@^4.1.0:
|
||||
version "4.1.0"
|
||||
@ -9603,7 +9622,7 @@ string.prototype.trimstart@^1.0.1:
|
||||
define-properties "^1.1.3"
|
||||
es-abstract "^1.17.5"
|
||||
|
||||
string_decoder@^1.1.1:
|
||||
string_decoder@^1.1.1, string_decoder@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
|
||||
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { When } from "cypress-cucumber-preprocessor/steps";
|
||||
|
||||
When('I click on the author', () => {
|
||||
cy.get('.user-teaser')
|
||||
cy.get('[data-test="avatarUserLink"]')
|
||||
.click()
|
||||
.url().should('include', '/profile/')
|
||||
})
|
||||
@ -5,7 +5,7 @@ Then("I should see my comment", () => {
|
||||
.should("contain", "Ocelot.social rocks")
|
||||
.get(".user-teaser span.slug")
|
||||
.should("contain", "@peter-pan") // specific enough
|
||||
.get(".user-avatar img")
|
||||
.get(".profile-avatar img")
|
||||
.should("have.attr", "src")
|
||||
.and("contain", 'https://') // some url
|
||||
.get(".user-teaser > .info > .text")
|
||||
|
||||
@ -4,5 +4,5 @@ Then("I cannot upload a picture", () => {
|
||||
cy.get(".base-card")
|
||||
.children()
|
||||
.should("not.have.id", "customdropzone")
|
||||
.should("have.class", "user-avatar");
|
||||
.should("have.class", "profile-avatar");
|
||||
});
|
||||
@ -9,7 +9,7 @@ Then("I should be able to change my profile picture", () => {
|
||||
{ subjectType: "drag-n-drop", force: true }
|
||||
);
|
||||
});
|
||||
cy.get(".profile-avatar img")
|
||||
cy.get(".profile-page-avatar img")
|
||||
.should("have.attr", "src")
|
||||
.and("contains", "onourjourney");
|
||||
cy.contains(".iziToast-message", "Upload successful")
|
||||
|
||||
34
docker-compose.apple-m1.override.yml
Normal file
34
docker-compose.apple-m1.override.yml
Normal file
@ -0,0 +1,34 @@
|
||||
version: "3.4"
|
||||
|
||||
services:
|
||||
|
||||
########################################################
|
||||
# WEBAPP ###############################################
|
||||
########################################################
|
||||
webapp:
|
||||
platform: linux/amd64
|
||||
|
||||
########################################################
|
||||
# BACKEND ##############################################
|
||||
########################################################
|
||||
backend:
|
||||
platform: linux/amd64
|
||||
|
||||
########################################################
|
||||
# MAINTENANCE ##########################################
|
||||
########################################################
|
||||
maintenance:
|
||||
platform: linux/amd64
|
||||
|
||||
########################################################
|
||||
# NEO4J ################################################
|
||||
########################################################
|
||||
neo4j:
|
||||
platform: linux/amd64
|
||||
|
||||
########################################################
|
||||
# MAILSERVER TO FAKE SMTP ##############################
|
||||
########################################################
|
||||
# commented out, because otherwise override of production would error. and it seems unnecessary
|
||||
# mailserver:
|
||||
# platform: linux/amd64
|
||||
@ -1,3 +1,5 @@
|
||||
# Todo: !!! This file seems related to our old maintenance worker for MongoDB and has to be refactored in case of using it !!!
|
||||
|
||||
version: "3.4"
|
||||
|
||||
services:
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
version: "3.4"
|
||||
|
||||
services:
|
||||
|
||||
########################################################
|
||||
# WEBAPP ###############################################
|
||||
########################################################
|
||||
webapp:
|
||||
image: ocelotsocialnetwork/webapp:development
|
||||
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
||||
image: ocelotsocialnetwork/webapp:local-development
|
||||
build:
|
||||
target: development
|
||||
environment:
|
||||
@ -18,11 +20,13 @@ services:
|
||||
- webapp_node_modules:/app/node_modules
|
||||
# bind the local folder to the docker to allow live reload
|
||||
- ./webapp:/app
|
||||
|
||||
########################################################
|
||||
# BACKEND ##############################################
|
||||
########################################################
|
||||
backend:
|
||||
image: ocelotsocialnetwork/backend:development
|
||||
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
||||
image: ocelotsocialnetwork/backend:local-development
|
||||
build:
|
||||
target: development
|
||||
environment:
|
||||
@ -34,22 +38,27 @@ services:
|
||||
- backend_node_modules:/app/node_modules
|
||||
# bind the local folder to the docker to allow live reload
|
||||
- ./backend:/app
|
||||
|
||||
########################################################
|
||||
# MAINTENANCE ##########################################
|
||||
########################################################
|
||||
maintenance:
|
||||
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
||||
image: ocelotsocialnetwork/maintenance:local-development
|
||||
|
||||
########################################################
|
||||
# NEO4J ################################################
|
||||
########################################################
|
||||
neo4j:
|
||||
image: ocelotsocialnetwork/neo4j:development
|
||||
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
||||
image: ocelotsocialnetwork/neo4j-community:local-development
|
||||
ports:
|
||||
# Also expose the neo4j query browser
|
||||
- 7474:7474
|
||||
networks:
|
||||
# So we can access the neo4j query browser from our host machine
|
||||
- external-net
|
||||
########################################################
|
||||
# MAINTENANCE ##########################################
|
||||
########################################################
|
||||
maintenance:
|
||||
image: ocelotsocialnetwork/maintenance:development
|
||||
|
||||
########################################################
|
||||
# MAILSERVER TO FAKE SMTP ##############################
|
||||
########################################################
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
version: "3.4"
|
||||
|
||||
services:
|
||||
|
||||
########################################################
|
||||
# WEBAPP ###############################################
|
||||
########################################################
|
||||
webapp:
|
||||
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
||||
image: ocelotsocialnetwork/webapp:test
|
||||
build:
|
||||
target: test
|
||||
@ -12,10 +14,12 @@ services:
|
||||
- NODE_ENV="test"
|
||||
volumes:
|
||||
- ./coverage:/app/coverage
|
||||
|
||||
########################################################
|
||||
# BACKEND ##############################################
|
||||
########################################################
|
||||
backend:
|
||||
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
||||
image: ocelotsocialnetwork/backend:test
|
||||
build:
|
||||
target: test
|
||||
@ -23,11 +27,20 @@ services:
|
||||
- NODE_ENV="test"
|
||||
volumes:
|
||||
- ./coverage:/app/coverage
|
||||
|
||||
########################################################
|
||||
# MAINTENANCE ##########################################
|
||||
########################################################
|
||||
maintenance:
|
||||
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
||||
image: ocelotsocialnetwork/maintenance:test
|
||||
|
||||
########################################################
|
||||
# NEO4J ################################################
|
||||
########################################################
|
||||
neo4j:
|
||||
image: ocelotsocialnetwork/neo4j:community
|
||||
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
||||
image: ocelotsocialnetwork/neo4j-community:test
|
||||
#environment:
|
||||
# - NEO4J_dbms_connector_bolt_enabled=true
|
||||
# - NEO4J_dbms_connector_bolt_tls__level=OPTIONAL
|
||||
@ -39,11 +52,7 @@ services:
|
||||
networks:
|
||||
# So we can access the neo4j query browser from our host machine
|
||||
- external-net
|
||||
########################################################
|
||||
# MAINTENANCE ##########################################
|
||||
########################################################
|
||||
maintenance:
|
||||
image: ocelotsocialnetwork/maintenance:test
|
||||
|
||||
########################################################
|
||||
# MAILSERVER TO FAKE SMTP ##############################
|
||||
########################################################
|
||||
|
||||
@ -6,11 +6,13 @@
|
||||
version: "3.4"
|
||||
|
||||
services:
|
||||
|
||||
########################################################
|
||||
# WEBAPP ###############################################
|
||||
########################################################
|
||||
webapp:
|
||||
image: ocelotsocialnetwork/webapp:latest
|
||||
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
||||
image: ocelotsocialnetwork/webapp:local-production
|
||||
build:
|
||||
context: ./webapp
|
||||
target: production
|
||||
@ -35,11 +37,13 @@ services:
|
||||
- GRAPHQL_URI=http://backend:4000
|
||||
env_file:
|
||||
- ./webapp/.env
|
||||
|
||||
########################################################
|
||||
# BACKEND ##############################################
|
||||
########################################################
|
||||
backend:
|
||||
image: ocelotsocialnetwork/backend:latest
|
||||
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
||||
image: ocelotsocialnetwork/backend:local-production
|
||||
build:
|
||||
context: ./backend
|
||||
target: production
|
||||
@ -67,11 +71,28 @@ services:
|
||||
- CLIENT_URI=http://webapp:3000
|
||||
env_file:
|
||||
- ./backend/.env
|
||||
|
||||
########################################################
|
||||
# MAINTENANCE ##########################################
|
||||
########################################################
|
||||
maintenance:
|
||||
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
||||
image: ocelotsocialnetwork/maintenance:local-production
|
||||
build:
|
||||
# TODO: Separate from webapp, this must be independent
|
||||
context: ./webapp
|
||||
dockerfile: Dockerfile.maintenance
|
||||
networks:
|
||||
- external-net
|
||||
ports:
|
||||
- 3001:80
|
||||
|
||||
########################################################
|
||||
# NEO4J ################################################
|
||||
########################################################
|
||||
neo4j:
|
||||
image: ocelotsocialnetwork/neo4j:latest
|
||||
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
||||
image: ocelotsocialnetwork/neo4j-community:local-production
|
||||
build:
|
||||
context: ./neo4j
|
||||
# community edition 👆🏼, because we have no enterprise licence 👇🏼 at the moment
|
||||
@ -90,19 +111,6 @@ services:
|
||||
# TODO: clarify if that is the only thing needed to unlock the Enterprise version
|
||||
# - NEO4J_ACCEPT_LICENSE_AGREEMENT=yes
|
||||
# TODO: Remove the playground from production
|
||||
########################################################
|
||||
# MAINTENANCE ##########################################
|
||||
########################################################
|
||||
maintenance:
|
||||
image: ocelotsocialnetwork/maintenance:latest
|
||||
build:
|
||||
# TODO: Separate from webapp, this must be independent
|
||||
context: ./webapp
|
||||
dockerfile: Dockerfile.maintenance
|
||||
networks:
|
||||
- external-net
|
||||
ports:
|
||||
- 5000:80
|
||||
|
||||
networks:
|
||||
external-net:
|
||||
|
||||
@ -7,7 +7,7 @@ database available. The community edition of Neo4J is Free and Open Source and
|
||||
we try our best to keep our application compatible with the community edition
|
||||
only.
|
||||
|
||||
## Installation with Docker
|
||||
## Installation With Docker
|
||||
|
||||
Run:
|
||||
|
||||
@ -19,7 +19,7 @@ You can access Neo4J through [http://localhost:7474/](http://localhost:7474/)
|
||||
for an interactive cypher shell and a visualization of the graph.
|
||||
|
||||
|
||||
## Installation without Docker
|
||||
## Installation Without Docker
|
||||
|
||||
Install the community edition of [Neo4j](https://neo4j.com/) along with the plugin
|
||||
[Apoc](https://github.com/neo4j-contrib/neo4j-apoc-procedures) on your system.
|
||||
@ -51,3 +51,81 @@ in `backend/.env`.
|
||||
|
||||
Start Neo4J and confirm the database is running at [http://localhost:7474](http://localhost:7474).
|
||||
|
||||
## Commands
|
||||
|
||||
Here we describe some rarely used Cypher commands for Neo4j that are needed from time to time:
|
||||
|
||||
### Index And Contraint Commands
|
||||
|
||||
If indexes or constraints are missing or not set correctly, the browser search will not work or the database seed for development will not work.
|
||||
|
||||
The indexes and constraints of our database are set in `backend/src/db/migrate/store.js`.
|
||||
This is where the magic happens.
|
||||
|
||||
It's called by our `prod:migrate init` command.
|
||||
This command initializes the Admin user and creates all necessary indexes and constraints in the Neo4j database.
|
||||
|
||||
***Calls in development***
|
||||
|
||||
Locally without Docker:
|
||||
|
||||
```bash
|
||||
# in backend folder
|
||||
$ yarn prod:migrate init
|
||||
```
|
||||
|
||||
Locally with Docker:
|
||||
|
||||
```bash
|
||||
# in main folder
|
||||
$ docker compose exec backend yarn prod:migrate init
|
||||
```
|
||||
|
||||
***Calls in production***
|
||||
|
||||
Locally with Docker:
|
||||
|
||||
```bash
|
||||
# in main folder
|
||||
$ docker compose exec backend /bin/sh -c "yarn prod:migrate init"
|
||||
```
|
||||
|
||||
On a server with Kubernetes cluster:
|
||||
|
||||
```bash
|
||||
# tested for one backend replica
|
||||
# !!! be aware of the kubectl context !!!
|
||||
$ kubectl -n default exec -it $(kubectl -n default get pods | grep ocelot-backend | awk '{ print $1 }') -- /bin/sh -c "yarn prod:migrate init"
|
||||
```
|
||||
|
||||
***Cypher commands to show indexes and contraints***
|
||||
|
||||
```bash
|
||||
# in browser command line or cypher shell
|
||||
|
||||
# show all indexes and contraints
|
||||
$ :schema
|
||||
|
||||
# show all indexes
|
||||
$ CALL db.indexes();
|
||||
|
||||
# show all contraints
|
||||
$ CALL db.constraints();
|
||||
```
|
||||
|
||||
***Cypher commands to create and drop indexes and contraints***
|
||||
|
||||
```bash
|
||||
# in browser command line or cypher shell
|
||||
|
||||
# create indexes
|
||||
$ CALL db.index.fulltext.createNodeIndex("post_fulltext_search",["Post"],["title", "content"]);
|
||||
$ CALL db.index.fulltext.createNodeIndex("user_fulltext_search",["User"],["name", "slug"]);
|
||||
$ CALL db.index.fulltext.createNodeIndex("tag_fulltext_search",["Tag"],["id"]);
|
||||
|
||||
# drop an index
|
||||
$ DROP CONSTRAINT ON ( image:Image ) ASSERT image.url IS UNIQUE
|
||||
|
||||
# drop all indexes and contraints
|
||||
$ CALL apoc.schema.assert({},{},true) YIELD label, key RETURN * ;
|
||||
```
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ocelot-social",
|
||||
"version": "1.0.7",
|
||||
"version": "2.2.0",
|
||||
"description": "Free and open source software program code available to run social networks.",
|
||||
"author": "ocelot.social Community",
|
||||
"license": "MIT",
|
||||
@ -25,6 +25,7 @@
|
||||
"@babel/core": "^7.9.0",
|
||||
"@babel/preset-env": "^7.12.7",
|
||||
"@babel/register": "^7.12.10",
|
||||
"@faker-js/faker": "5.1.0",
|
||||
"auto-changelog": "^2.3.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"codecov": "^3.8.2",
|
||||
@ -36,16 +37,15 @@
|
||||
"date-fns": "^2.25.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"expect": "^25.3.0",
|
||||
"@faker-js/faker": "5.1.0",
|
||||
"graphql-request": "^2.0.0",
|
||||
"import": "^0.0.6",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mock-socket": "^9.0.3",
|
||||
"neo4j-driver": "^4.3.4",
|
||||
"neode": "^0.4.7",
|
||||
"neode": "^0.4.8",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"rosie": "^2.1.0",
|
||||
"slug": "^5.1.0"
|
||||
"slug": "^6.0.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"set-value": "^2.0.1"
|
||||
|
||||
@ -4,3 +4,4 @@ PUBLIC_REGISTRATION=false
|
||||
INVITE_REGISTRATION=true
|
||||
WEBSOCKETS_URI=ws://localhost:3000/api/graphql
|
||||
GRAPHQL_URI=http://localhost:4000/
|
||||
CATEGORIES_ACTIVE=false
|
||||
|
||||
@ -94,6 +94,13 @@ FROM base as production
|
||||
COPY --from=build ${DOCKER_WORKDIR}/.nuxt ./.nuxt
|
||||
COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
|
||||
COPY --from=build ${DOCKER_WORKDIR}/nuxt.config.js ./nuxt.config.js
|
||||
# Copy static files
|
||||
# TODO - this seems not be needed anymore for the new rebranding
|
||||
# TODO - this should be one Folder containign all stuff needed to be copied
|
||||
COPY --from=build ${DOCKER_WORKDIR}/config/ ./config/
|
||||
COPY --from=build ${DOCKER_WORKDIR}/constants ./constants
|
||||
COPY --from=build ${DOCKER_WORKDIR}/static ./static
|
||||
COPY --from=build ${DOCKER_WORKDIR}/locales ./locales
|
||||
# Copy package.json for script definitions (lock file should not be needed)
|
||||
COPY --from=build ${DOCKER_WORKDIR}/package.json ./package.json
|
||||
|
||||
|
||||
5
webapp/assets/_new/icons/svgs/child.svg
Normal file
5
webapp/assets/_new/icons/svgs/child.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>child</title>
|
||||
<path d="M12 3c2.202 0 3.791 1.007 4.531 2.313 0.026-0.041 0.034-0.084 0.063-0.125 0.453-0.641 1.315-1.188 2.406-1.188v2c-0.453 0-0.588 0.111-0.719 0.281 3.845 0.921 6.812 4.105 7.563 8.063 1.193 0.397 2.156 1.337 2.156 2.656 0 1.365-1.024 2.33-2.281 2.688-0.816 4.701-4.82 8.313-9.719 8.313s-8.903-3.611-9.719-8.313c-1.257-0.357-2.281-1.323-2.281-2.688s1.024-2.33 2.281-2.688c0.741-4.271 4.122-7.637 8.406-8.219-0.39-0.574-1.192-1.094-2.688-1.094v-2zM16 8c-4.093 0-7.461 3.121-7.906 7.125l-0.094 0.875h-1c-0.555 0-1 0.445-1 1s0.445 1 1 1h1l0.094 0.875c0.445 4.004 3.813 7.125 7.906 7.125s7.461-3.121 7.906-7.125l0.094-0.875h1c0.555 0 1-0.445 1-1s-0.445-1-1-1h-0.875l-0.125-0.875c-0.536-4.019-3.907-7.125-8-7.125zM12.5 16c0.828 0 1.5 0.672 1.5 1.5s-0.672 1.5-1.5 1.5-1.5-0.672-1.5-1.5 0.672-1.5 1.5-1.5zM19.5 16c0.828 0 1.5 0.672 1.5 1.5s-0.672 1.5-1.5 1.5-1.5-0.672-1.5-1.5 0.672-1.5 1.5-1.5z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
20
webapp/assets/_new/icons/svgs/culture.svg
Normal file
20
webapp/assets/_new/icons/svgs/culture.svg
Normal file
@ -0,0 +1,20 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32px" height="32px" viewBox="0 0 32 32">
|
||||
<path d="M5.9,25.1c-0.3,0.3-0.3,0.8,0,1.1s0.8,0.3,1.1,0c1.7-1.7,4.5-1.7,6.3,0c0.1,0.1,0.3,0.2,0.5,0.2s0.4-0.1,0.5-0.2
|
||||
c0.3-0.3,0.3-0.8,0-1.1C12,22.8,8.2,22.8,5.9,25.1z"/>
|
||||
<path d="M24.4,8.7c0.7-0.7,2-0.7,2.7,0C27.2,8.9,27.4,9,27.6,9c0.2,0,0.4-0.1,0.5-0.2c0.3-0.3,0.3-0.8,0-1.1
|
||||
c-1.3-1.3-3.5-1.3-4.8,0C23,8,23,8.5,23.3,8.7C23.6,9,24.1,9,24.4,8.7z"/>
|
||||
<path d="M16.4,7.7c-0.3,0.3-0.3,0.8,0,1.1c0.3,0.3,0.8,0.3,1.1,0c0.7-0.7,1.9-0.7,2.7,0C20.3,8.9,20.5,9,20.7,9s0.4-0.1,0.5-0.2
|
||||
c0.3-0.3,0.3-0.8,0-1.1C19.9,6.4,17.7,6.4,16.4,7.7z"/>
|
||||
<path d="M31.4,0.8c-0.2-0.1-0.5-0.2-0.7-0.1c-2,0.8-5.1,1.2-8.4,1.2s-6.4-0.4-8.4-1.2c-0.2-0.1-0.5-0.1-0.7,0.1
|
||||
c-0.2,0.1-0.3,0.4-0.3,0.6v9.9c-1,0.1-1.9,0.1-3,0.1c-3.3,0-6.4-0.4-8.4-1.2c-0.2-0.1-0.5-0.1-0.7,0.1c-0.2,0.1-0.3,0.4-0.3,0.6
|
||||
l0,11.5c0,5.2,4.2,9.4,9.4,9.4s9.4-4.2,9.4-9.4V22c0.7,0.3,1.6,0.4,3,0.4c5.2,0,9.4-4.2,9.4-9.4V1.4C31.7,1.2,31.6,0.9,31.4,0.8z
|
||||
M9.9,30.4c-4.4,0-7.9-3.6-7.9-7.9V12c2.1,0.6,4.9,1,7.9,1c2.7,0,5.2-0.3,7.3-0.8l0.5-0.1c0,1.6,0,2.9,0,4c0,1.3,0,2.4,0.1,3.2v3.2
|
||||
C17.8,26.8,14.2,30.4,9.9,30.4z M30.2,13c0,4.4-3.6,7.9-7.9,7.9c-2.1,0-2.8,0-3-1.7v-2.8c0.9,0.5,1.9,0.8,3,0.8
|
||||
c1.6,0,3.1-0.6,4.2-1.7c0.3-0.3,0.3-0.8,0-1.1s-0.8-0.3-1.1,0c-0.8,0.8-2,1.3-3.1,1.3c-1.1,0-2.1-0.4-3-1.2v-3.6
|
||||
c0-0.2-0.1-0.5-0.3-0.6c-0.2-0.1-0.5-0.2-0.7-0.1c-0.4,0.2-0.9,0.3-1.4,0.4l-2.5,0.4V2.5c2.1,0.6,4.9,1,7.9,1c3,0,5.8-0.3,7.9-1V13
|
||||
z"/>
|
||||
<path d="M10.9,17.7c-0.3,0.3-0.3,0.8,0,1.1s0.8,0.3,1.1,0c0.7-0.7,2-0.7,2.7,0c0.1,0.1,0.3,0.2,0.5,0.2s0.4-0.1,0.5-0.2
|
||||
c0.3-0.3,0.3-0.8,0-1.1C14.4,16.4,12.2,16.4,10.9,17.7z"/>
|
||||
<path d="M7.7,18.7C7.9,18.9,8.1,19,8.3,19s0.4-0.1,0.5-0.2c0.3-0.3,0.3-0.8,0-1.1c-1.3-1.3-3.5-1.3-4.8,0c-0.3,0.3-0.3,0.8,0,1.1
|
||||
s0.8,0.3,1.1,0C5.8,18,7,18,7.7,18.7z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
5
webapp/assets/_new/icons/svgs/desktop.svg
Normal file
5
webapp/assets/_new/icons/svgs/desktop.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>desktop</title>
|
||||
<path d="M2 6h28v18h-13v2h5v2h-12v-2h5v-2h-13v-18zM4 8v14h24v-14h-24z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 240 B |
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user