Merge branch 'master' into 1188-Delete_teaser_image

This commit is contained in:
Alexander Friedland 2020-02-05 08:00:55 +01:00 committed by GitHub
commit 798e4d4f5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
225 changed files with 7464 additions and 5091 deletions

View File

@ -0,0 +1,20 @@
---
name: 🔧 Refactor ticket
about: Help us improve our code by refactoring it.
labels: refactor
title: 🔧 [Refactor]
---
## :zap: 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.-->
### Motive
<!-- What is the purpose of this refactoring? If it's removing depcrecated code, please link to the deprecation notice. -->
### Related issues
<!-- Are there any related issues to link to? Please paste them below for reference. -->
### Implementation
<!-- Please, document any ideas of how the code should be refactored. -->
### Additional context
<!-- Add other context or background about the feature request here.-->

View File

@ -6,7 +6,6 @@ addons:
- libgconf-2-4 - libgconf-2-4
snaps: snaps:
- docker - docker
- chromium
install: install:
- yarn global add wait-on - yarn global add wait-on
@ -19,7 +18,7 @@ before_script:
- docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml build # just tagging, just be quite fast - docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml build # just tagging, just be quite fast
- docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml up -d - docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml up -d
- wait-on http://localhost:7474 - wait-on http://localhost:7474
- docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml exec neo4j db_setup - docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml exec backend yarn run db:migrate init
script: script:
- export CYPRESS_RETRIES=1 - export CYPRESS_RETRIES=1
@ -70,7 +69,3 @@ deploy:
script: bash scripts/deploy.sh script: bash scripts/deploy.sh
on: on:
branch: master branch: master
- provider: script
script: bash scripts/github_release.sh
on:
branch: master

7
.versionrc.json Normal file
View File

@ -0,0 +1,7 @@
{
"bumpFiles": [
"package.json",
"backend/package.json",
"webapp/package.json"
]
}

View File

@ -4,6 +4,187 @@ 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). Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [v0.3.0](https://github.com/Human-Connection/Human-Connection/compare/v0.2.1...v0.3.0)
> 31 January 2020
- feat: 🍰 Direct Reply On Comment [`#2608`](https://github.com/Human-Connection/Human-Connection/pull/2608)
- build(deps-dev): bump @babel/core from 7.8.3 to 7.8.4 in /backend [`#2938`](https://github.com/Human-Connection/Human-Connection/pull/2938)
- fix: deploy script with new naming convention [`#2930`](https://github.com/Human-Connection/Human-Connection/pull/2930)
- build(deps-dev): bump @babel/preset-env from 7.8.3 to 7.8.4 in /backend [`#2940`](https://github.com/Human-Connection/Human-Connection/pull/2940)
- build(deps-dev): bump @babel/cli from 7.8.3 to 7.8.4 in /backend [`#2934`](https://github.com/Human-Connection/Human-Connection/pull/2934)
- build(deps-dev): bump @babel/core from 7.8.3 to 7.8.4 [`#2935`](https://github.com/Human-Connection/Human-Connection/pull/2935)
- build(deps-dev): bump @babel/preset-env from 7.8.3 to 7.8.4 in /webapp [`#2932`](https://github.com/Human-Connection/Human-Connection/pull/2932)
- build(deps): bump @nuxtjs/axios from 5.9.3 to 5.9.4 in /webapp [`#2937`](https://github.com/Human-Connection/Human-Connection/pull/2937)
- build(deps-dev): bump @babel/node from 7.8.3 to 7.8.4 in /backend [`#2936`](https://github.com/Human-Connection/Human-Connection/pull/2936)
- build(deps-dev): bump @babel/preset-env from 7.8.3 to 7.8.4 [`#2933`](https://github.com/Human-Connection/Human-Connection/pull/2933)
- feat: Blocked users cannot comment on posts [`#2714`](https://github.com/Human-Connection/Human-Connection/pull/2714)
- build(deps): bump @sentry/node from 5.11.1 to 5.11.2 in /backend [`#2927`](https://github.com/Human-Connection/Human-Connection/pull/2927)
- build(deps): bump cookie-universal-nuxt from 2.1.0 to 2.1.1 in /webapp [`#2925`](https://github.com/Human-Connection/Human-Connection/pull/2925)
- build(deps): bump @nuxtjs/sentry from 3.0.1 to 3.1.0 in /webapp [`#2928`](https://github.com/Human-Connection/Human-Connection/pull/2928)
- docs(CONTRIBUTING): Add open-source bounty program [`#2899`](https://github.com/Human-Connection/Human-Connection/pull/2899)
- build(deps-dev): bump eslint-config-prettier from 6.9.0 to 6.10.0 in /webapp [`#2926`](https://github.com/Human-Connection/Human-Connection/pull/2926)
- build(deps): bump graphql-shield from 7.0.8 to 7.0.9 in /backend [`#2924`](https://github.com/Human-Connection/Human-Connection/pull/2924)
- build(deps-dev): bump eslint-config-prettier from 6.9.0 to 6.10.0 in /backend [`#2923`](https://github.com/Human-Connection/Human-Connection/pull/2923)
- feat(editor): Underline markup for posts+comments [`#2898`](https://github.com/Human-Connection/Human-Connection/pull/2898)
- fix(backend): Add migration for muted relationship [`#2919`](https://github.com/Human-Connection/Human-Connection/pull/2919)
- build(deps-dev): bump @storybook/addon-a11y from 5.3.8 to 5.3.9 in /webapp [`#2914`](https://github.com/Human-Connection/Human-Connection/pull/2914)
- fix(webapp): 🐛 Adjust Avatar Sizes and 'z-index' [`#2871`](https://github.com/Human-Connection/Human-Connection/pull/2871)
- build(deps-dev): bump @storybook/vue from 5.3.8 to 5.3.9 in /webapp [`#2913`](https://github.com/Human-Connection/Human-Connection/pull/2913)
- build(deps): bump validator from 12.1.0 to 12.2.0 in /backend [`#2907`](https://github.com/Human-Connection/Human-Connection/pull/2907)
- build(deps): bump graphql from 14.5.8 to 14.6.0 in /webapp [`#2905`](https://github.com/Human-Connection/Human-Connection/pull/2905)
- build(deps): bump cross-env from 6.0.3 to 7.0.0 in /backend [`#2911`](https://github.com/Human-Connection/Human-Connection/pull/2911)
- build(deps-dev): bump @storybook/addon-notes from 5.3.8 to 5.3.9 in /webapp [`#2910`](https://github.com/Human-Connection/Human-Connection/pull/2910)
- build(deps): bump slug from 2.1.0 to 2.1.1 in /backend [`#2906`](https://github.com/Human-Connection/Human-Connection/pull/2906)
- build(deps): bump cross-env from 6.0.3 to 7.0.0 in /webapp [`#2916`](https://github.com/Human-Connection/Human-Connection/pull/2916)
- build(deps): bump graphql from 14.5.8 to 14.6.0 in /backend [`#2909`](https://github.com/Human-Connection/Human-Connection/pull/2909)
- build(deps-dev): bump storybook-design-token from 0.5.0 to 0.5.1 in /webapp [`#2915`](https://github.com/Human-Connection/Human-Connection/pull/2915)
- build(deps): bump validator from 12.1.0 to 12.2.0 in /webapp [`#2912`](https://github.com/Human-Connection/Human-Connection/pull/2912)
- build(deps-dev): bump @storybook/addon-actions from 5.3.8 to 5.3.9 in /webapp [`#2908`](https://github.com/Human-Connection/Human-Connection/pull/2908)
- build(deps-dev): bump slug from 2.1.0 to 2.1.1 [`#2904`](https://github.com/Human-Connection/Human-Connection/pull/2904)
- build(deps): bump graphql-shield from 7.0.7 to 7.0.8 in /backend [`#2903`](https://github.com/Human-Connection/Human-Connection/pull/2903)
- build(deps-dev): bump cypress from 3.8.2 to 3.8.3 [`#2902`](https://github.com/Human-Connection/Human-Connection/pull/2902)
- build(deps): bump metascraper-logo from 5.10.5 to 5.10.6 in /backend [`#2893`](https://github.com/Human-Connection/Human-Connection/pull/2893)
- build(deps): bump metascraper-video from 5.10.5 to 5.10.6 in /backend [`#2892`](https://github.com/Human-Connection/Human-Connection/pull/2892)
- build(deps): bump metascraper-image from 5.10.5 to 5.10.6 in /backend [`#2891`](https://github.com/Human-Connection/Human-Connection/pull/2891)
- build(deps): bump metascraper-publisher from 5.10.5 to 5.10.6 in /backend [`#2890`](https://github.com/Human-Connection/Human-Connection/pull/2890)
- build(deps-dev): bump codecov from 3.6.1 to 3.6.2 [`#2889`](https://github.com/Human-Connection/Human-Connection/pull/2889)
- feat(db): Setup neo4j data migrations [`#2828`](https://github.com/Human-Connection/Human-Connection/pull/2828)
- build(deps): bump metascraper from 5.10.5 to 5.10.6 in /backend [`#2877`](https://github.com/Human-Connection/Human-Connection/pull/2877)
- build(deps): bump metascraper-url from 5.10.5 to 5.10.6 in /backend [`#2879`](https://github.com/Human-Connection/Human-Connection/pull/2879)
- build(deps): bump metascraper-lang from 5.10.5 to 5.10.6 in /backend [`#2882`](https://github.com/Human-Connection/Human-Connection/pull/2882)
- build(deps): bump metascraper-audio from 5.10.5 to 5.10.6 in /backend [`#2883`](https://github.com/Human-Connection/Human-Connection/pull/2883)
- build(deps): bump metascraper-date from 5.10.5 to 5.10.6 in /backend [`#2878`](https://github.com/Human-Connection/Human-Connection/pull/2878)
- build(deps): bump metascraper-title from 5.10.5 to 5.10.6 in /backend [`#2880`](https://github.com/Human-Connection/Human-Connection/pull/2880)
- build(deps): bump metascraper-youtube from 5.10.5 to 5.10.6 in /backend [`#2881`](https://github.com/Human-Connection/Human-Connection/pull/2881)
- build(deps): bump metascraper-author from 5.10.5 to 5.10.6 in /backend [`#2876`](https://github.com/Human-Connection/Human-Connection/pull/2876)
- build(deps): bump metascraper-description from 5.10.5 to 5.10.6 in /backend [`#2875`](https://github.com/Human-Connection/Human-Connection/pull/2875)
- build(deps): bump metascraper-soundcloud from 5.10.5 to 5.10.6 in /backend [`#2874`](https://github.com/Human-Connection/Human-Connection/pull/2874)
- build(deps-dev): bump jest from 24.9.0 to 25.1.0 in /webapp [`#2868`](https://github.com/Human-Connection/Human-Connection/pull/2868)
- build(deps-dev): bump @storybook/vue from 5.3.7 to 5.3.8 in /webapp [`#2867`](https://github.com/Human-Connection/Human-Connection/pull/2867)
- build(deps-dev): bump babel-jest from 24.9.0 to 25.1.0 in /webapp [`#2869`](https://github.com/Human-Connection/Human-Connection/pull/2869)
- build(deps-dev): bump @storybook/addon-actions from 5.3.7 to 5.3.8 in /webapp [`#2865`](https://github.com/Human-Connection/Human-Connection/pull/2865)
- build(deps-dev): bump babel-jest from 24.9.0 to 25.1.0 in /backend [`#2863`](https://github.com/Human-Connection/Human-Connection/pull/2863)
- build(deps-dev): bump expect from 24.9.0 to 25.1.0 [`#2861`](https://github.com/Human-Connection/Human-Connection/pull/2861)
- build(deps-dev): bump @storybook/addon-a11y from 5.3.7 to 5.3.8 in /webapp [`#2866`](https://github.com/Human-Connection/Human-Connection/pull/2866)
- build(deps-dev): bump @storybook/addon-notes from 5.3.7 to 5.3.8 in /webapp [`#2864`](https://github.com/Human-Connection/Human-Connection/pull/2864)
- build(deps-dev): bump jest from 24.9.0 to 25.1.0 in /backend [`#2862`](https://github.com/Human-Connection/Human-Connection/pull/2862)
- docs(deployment): Explain how to setup metrics [`#2825`](https://github.com/Human-Connection/Human-Connection/pull/2825)
- refactor(styleguide): Migrate Avatar component to monorepo [`#2700`](https://github.com/Human-Connection/Human-Connection/pull/2700)
- feat: Convert block/unblock to mute/unmute [`#2686`](https://github.com/Human-Connection/Human-Connection/pull/2686)
- removed obsolete German keys [`#2845`](https://github.com/Human-Connection/Human-Connection/pull/2845)
- build(deps-dev): bump @vue/server-test-utils in /webapp [`#2852`](https://github.com/Human-Connection/Human-Connection/pull/2852)
- build(deps-dev): bump @storybook/vue from 5.3.6 to 5.3.7 in /webapp [`#2857`](https://github.com/Human-Connection/Human-Connection/pull/2857)
- build(deps-dev): bump @storybook/addon-a11y in /webapp [`#2858`](https://github.com/Human-Connection/Human-Connection/pull/2858)
- build(deps): bump metascraper-image from 5.10.3 to 5.10.5 in /backend [`#2849`](https://github.com/Human-Connection/Human-Connection/pull/2849)
- build(deps): bump wait-on from 3.3.0 to 4.0.0 in /backend [`#2848`](https://github.com/Human-Connection/Human-Connection/pull/2848)
- build(deps): bump v-tooltip from 2.0.2 to 2.0.3 in /webapp [`#2856`](https://github.com/Human-Connection/Human-Connection/pull/2856)
- build(deps-dev): bump @storybook/addon-notes from 5.3.6 to 5.3.7 in /webapp [`#2855`](https://github.com/Human-Connection/Human-Connection/pull/2855)
- build(deps): bump sanitize-html from 1.20.1 to 1.21.1 in /backend [`#2854`](https://github.com/Human-Connection/Human-Connection/pull/2854)
- build(deps): bump metascraper-video from 5.10.3 to 5.10.5 in /backend [`#2853`](https://github.com/Human-Connection/Human-Connection/pull/2853)
- build(deps): bump metascraper-date from 5.10.3 to 5.10.5 in /backend [`#2851`](https://github.com/Human-Connection/Human-Connection/pull/2851)
- build(deps-dev): bump @vue/test-utils from 1.0.0-beta.30 to 1.0.0-beta.31 in /webapp [`#2850`](https://github.com/Human-Connection/Human-Connection/pull/2850)
- build(deps): bump metascraper-logo from 5.10.3 to 5.10.5 in /backend [`#2847`](https://github.com/Human-Connection/Human-Connection/pull/2847)
- build(deps): bump @hapi/joi from 17.0.2 to 17.1.0 in /backend [`#2846`](https://github.com/Human-Connection/Human-Connection/pull/2846)
- Release 0.2.2 [`#2844`](https://github.com/Human-Connection/Human-Connection/pull/2844)
- build(deps-dev): bump @storybook/addon-actions in /webapp [`#2842`](https://github.com/Human-Connection/Human-Connection/pull/2842)
- build(deps): bump metascraper-title from 5.10.3 to 5.10.5 in /backend [`#2835`](https://github.com/Human-Connection/Human-Connection/pull/2835)
- build(deps): bump metascraper-publisher in /backend [`#2836`](https://github.com/Human-Connection/Human-Connection/pull/2836)
- build(deps): bump metascraper-audio from 5.10.3 to 5.10.5 in /backend [`#2840`](https://github.com/Human-Connection/Human-Connection/pull/2840)
- build(deps): bump metascraper-author from 5.10.3 to 5.10.5 in /backend [`#2838`](https://github.com/Human-Connection/Human-Connection/pull/2838)
- build(deps): bump metascraper-url from 5.10.3 to 5.10.5 in /backend [`#2832`](https://github.com/Human-Connection/Human-Connection/pull/2832)
- build(deps): bump metascraper-lang from 5.10.3 to 5.10.5 in /backend [`#2831`](https://github.com/Human-Connection/Human-Connection/pull/2831)
- refactor(modules): Various import fixes [`#2802`](https://github.com/Human-Connection/Human-Connection/pull/2802)
- build(deps): bump metascraper-description from 5.10.3 to 5.10.5 in /backend [`#2839`](https://github.com/Human-Connection/Human-Connection/pull/2839)
- build(deps-dev): bump @storybook/addon-notes from 5.3.5 to 5.3.6 in /webapp [`#2834`](https://github.com/Human-Connection/Human-Connection/pull/2834)
- build(deps): bump metascraper-youtube from 5.10.3 to 5.10.5 in /backend [`#2833`](https://github.com/Human-Connection/Human-Connection/pull/2833)
- build(deps): bump metascraper from 5.10.3 to 5.10.5 in /backend [`#2830`](https://github.com/Human-Connection/Human-Connection/pull/2830)
- build(deps): bump metascraper-soundcloud from 5.10.3 to 5.10.5 in /backend [`#2829`](https://github.com/Human-Connection/Human-Connection/pull/2829)
- fix(translations): Remove duplicate and mistranslated item from code of conduct [`#2725`](https://github.com/Human-Connection/Human-Connection/pull/2725)
- build(deps-dev): bump @storybook/addon-a11y from 5.3.3 to 5.3.6 in /webapp [`#2820`](https://github.com/Human-Connection/Human-Connection/pull/2820)
- build(deps): bump metascraper from 5.10.2 to 5.10.3 in /backend [`#2808`](https://github.com/Human-Connection/Human-Connection/pull/2808)
- build(deps-dev): bump @storybook/vue from 5.3.3 to 5.3.6 in /webapp [`#2819`](https://github.com/Human-Connection/Human-Connection/pull/2819)
- build(deps): bump faker from `9fd8d7d` to `3b2fa4a` in /backend [`#2803`](https://github.com/Human-Connection/Human-Connection/pull/2803)
- build(deps-dev): bump faker from `9fd8d7d` to `3b2fa4a` [`#2804`](https://github.com/Human-Connection/Human-Connection/pull/2804)
- build(deps-dev): bump @storybook/addon-a11y in /webapp [`#2809`](https://github.com/Human-Connection/Human-Connection/pull/2809)
- build(deps): bump uuid from 3.3.3 to 3.4.0 in /backend [`#2810`](https://github.com/Human-Connection/Human-Connection/pull/2810)
- build(deps): bump metascraper-image from 5.9.5 to 5.10.3 in /backend [`#2811`](https://github.com/Human-Connection/Human-Connection/pull/2811)
- build(deps-dev): bump node-sass from 4.13.0 to 4.13.1 in /webapp [`#2812`](https://github.com/Human-Connection/Human-Connection/pull/2812)
- build(deps): bump metascraper-audio from 5.9.5 to 5.10.3 in /backend [`#2813`](https://github.com/Human-Connection/Human-Connection/pull/2813)
- build(deps): bump metascraper-soundcloud in /backend [`#2815`](https://github.com/Human-Connection/Human-Connection/pull/2815)
- build(deps-dev): bump @storybook/addon-notes in /webapp [`#2816`](https://github.com/Human-Connection/Human-Connection/pull/2816)
- build(deps-dev): bump @storybook/addon-actions from 5.3.3 to 5.3.5 in /webapp [`#2807`](https://github.com/Human-Connection/Human-Connection/pull/2807)
- build(deps): bump metascraper-description from 5.9.5 to 5.10.3 in /backend [`#2806`](https://github.com/Human-Connection/Human-Connection/pull/2806)
- build(deps): bump mustache from 3.2.1 to 4.0.0 in /backend [`#2805`](https://github.com/Human-Connection/Human-Connection/pull/2805)
- 🍰 feat(webapp): Display deployed version in footer [`#2728`](https://github.com/Human-Connection/Human-Connection/pull/2728)
- fix: cypress breaks locally in login step [`#2776`](https://github.com/Human-Connection/Human-Connection/pull/2776)
- build(deps-dev): bump @vue/test-utils from 1.0.0-beta.29 to 1.0.0-beta.30 in /webapp [`#2378`](https://github.com/Human-Connection/Human-Connection/pull/2378)
- build(deps): bump metascraper-youtube from 5.9.5 to 5.10.3 in /backend [`#2794`](https://github.com/Human-Connection/Human-Connection/pull/2794)
- build(deps): bump metascraper-video from 5.9.5 to 5.10.3 in /backend [`#2795`](https://github.com/Human-Connection/Human-Connection/pull/2795)
- build(deps): bump metascraper-logo from 5.9.5 to 5.10.3 in /backend [`#2796`](https://github.com/Human-Connection/Human-Connection/pull/2796)
- refactor(styleguide): improve emotion buttons and header responsiveness [`#2582`](https://github.com/Human-Connection/Human-Connection/pull/2582)
- build(deps): bump metascraper-url from 5.9.5 to 5.10.3 in /backend [`#2793`](https://github.com/Human-Connection/Human-Connection/pull/2793)
- build(deps): bump metascraper-author from 5.9.5 to 5.10.3 in /backend [`#2789`](https://github.com/Human-Connection/Human-Connection/pull/2789)
- build(deps): bump metascraper-lang from 5.9.5 to 5.10.3 in /backend [`#2790`](https://github.com/Human-Connection/Human-Connection/pull/2790)
- build(deps): bump metascraper-publisher from 5.9.5 to 5.10.3 in /backend [`#2792`](https://github.com/Human-Connection/Human-Connection/pull/2792)
- build(deps): bump metascraper-title from 5.9.5 to 5.10.3 in /backend [`#2791`](https://github.com/Human-Connection/Human-Connection/pull/2791)
- build(deps): bump @sentry/node from 5.11.0 to 5.11.1 in /backend [`#2788`](https://github.com/Human-Connection/Human-Connection/pull/2788)
- build(deps): bump metascraper-date from 5.9.5 to 5.10.3 in /backend [`#2787`](https://github.com/Human-Connection/Human-Connection/pull/2787)
- build(deps-dev): bump @babel/node from 7.8.0 to 7.8.3 in /backend [`#2754`](https://github.com/Human-Connection/Human-Connection/pull/2754)
- refactor(styleguide): migrate and redesign buttons [`#2562`](https://github.com/Human-Connection/Human-Connection/pull/2562)
- build(deps-dev): bump @babel/core from 7.8.0 to 7.8.3 in /backend [`#2760`](https://github.com/Human-Connection/Human-Connection/pull/2760)
- build(deps-dev): bump @storybook/addon-actions from 5.3.2 to 5.3.3 in /webapp [`#2782`](https://github.com/Human-Connection/Human-Connection/pull/2782)
- build(deps-dev): bump sass-loader from 8.0.0 to 8.0.2 in /webapp [`#2781`](https://github.com/Human-Connection/Human-Connection/pull/2781)
- build(deps-dev): bump @babel/plugin-syntax-dynamic-import from 7.8.0 to 7.8.3 in /webapp [`#2780`](https://github.com/Human-Connection/Human-Connection/pull/2780)
- build(deps-dev): bump @storybook/addon-a11y from 5.3.2 to 5.3.3 in /webapp [`#2779`](https://github.com/Human-Connection/Human-Connection/pull/2779)
- build(deps): bump metascraper from 5.9.5 to 5.10.2 in /backend [`#2778`](https://github.com/Human-Connection/Human-Connection/pull/2778)
- build(deps-dev): bump @babel/preset-env from 7.7.7 to 7.8.3 in /webapp [`#2767`](https://github.com/Human-Connection/Human-Connection/pull/2767)
- build(deps-dev): bump @babel/plugin-proposal-throw-expressions from 7.8.0 to 7.8.3 in /backend [`#2757`](https://github.com/Human-Connection/Human-Connection/pull/2757)
- build(deps-dev): bump @storybook/vue from 5.3.1 to 5.3.3 in /webapp [`#2772`](https://github.com/Human-Connection/Human-Connection/pull/2772)
- build(deps-dev): bump @babel/preset-env from 7.8.2 to 7.8.3 [`#2758`](https://github.com/Human-Connection/Human-Connection/pull/2758)
- build(deps-dev): bump eslint-plugin-import from 2.19.1 to 2.20.0 in /webapp [`#2748`](https://github.com/Human-Connection/Human-Connection/pull/2748)
- build(deps-dev): bump @storybook/addon-notes from 5.3.1 to 5.3.3 in /webapp [`#2771`](https://github.com/Human-Connection/Human-Connection/pull/2771)
- build(deps-dev): bump @babel/core from 7.7.7 to 7.8.3 in /webapp [`#2769`](https://github.com/Human-Connection/Human-Connection/pull/2769)
- build(deps-dev): bump @babel/register from 7.8.0 to 7.8.3 [`#2764`](https://github.com/Human-Connection/Human-Connection/pull/2764)
- build(deps-dev): bump @babel/preset-env from 7.8.2 to 7.8.3 in /backend [`#2755`](https://github.com/Human-Connection/Human-Connection/pull/2755)
- build(deps-dev): bump eslint-plugin-jest from 23.3.0 to 23.6.0 in /webapp [`#2768`](https://github.com/Human-Connection/Human-Connection/pull/2768)
- build(deps-dev): bump @babel/cli from 7.8.0 to 7.8.3 in /backend [`#2763`](https://github.com/Human-Connection/Human-Connection/pull/2763)
- build(deps-dev): bump cypress-cucumber-preprocessor from 1.19.0 to 2.0.1 [`#2761`](https://github.com/Human-Connection/Human-Connection/pull/2761)
- build(deps-dev): bump @storybook/addon-a11y from 5.2.8 to 5.3.2 in /webapp [`#2759`](https://github.com/Human-Connection/Human-Connection/pull/2759)
- build(deps-dev): bump @babel/core from 7.8.0 to 7.8.3 [`#2756`](https://github.com/Human-Connection/Human-Connection/pull/2756)
- build(deps-dev): bump @babel/register from 7.8.0 to 7.8.3 in /backend [`#2753`](https://github.com/Human-Connection/Human-Connection/pull/2753)
- build(deps): [security] bump serialize-javascript from 2.1.0 to 2.1.2 in /webapp [`#2752`](https://github.com/Human-Connection/Human-Connection/pull/2752)
- build(deps-dev): bump @babel/core from 7.7.7 to 7.8.0 in /backend [`#2743`](https://github.com/Human-Connection/Human-Connection/pull/2743)
- build(deps-dev): bump @storybook/addon-actions from 5.2.8 to 5.3.2 in /webapp [`#2751`](https://github.com/Human-Connection/Human-Connection/pull/2751)
- build(deps-dev): bump @babel/register from 7.7.7 to 7.8.0 in /backend [`#2735`](https://github.com/Human-Connection/Human-Connection/pull/2735)
- build(deps-dev): bump @babel/plugin-syntax-dynamic-import from 7.7.4 to 7.8.0 in /webapp [`#2746`](https://github.com/Human-Connection/Human-Connection/pull/2746)
- build(deps-dev): bump @babel/preset-env from 7.7.7 to 7.8.2 in /backend [`#2739`](https://github.com/Human-Connection/Human-Connection/pull/2739)
- build(deps-dev): bump @babel/cli from 7.7.7 to 7.8.0 in /backend [`#2744`](https://github.com/Human-Connection/Human-Connection/pull/2744)
- Issues marked as bounty never become stale [`#2726`](https://github.com/Human-Connection/Human-Connection/pull/2726)
- build(deps-dev): bump css-loader from 3.4.1 to 3.4.2 in /webapp [`#2747`](https://github.com/Human-Connection/Human-Connection/pull/2747)
- build(deps-dev): bump @storybook/addon-notes from 5.2.8 to 5.3.1 in /webapp [`#2742`](https://github.com/Human-Connection/Human-Connection/pull/2742)
- build(deps-dev): bump @babel/plugin-proposal-throw-expressions from 7.7.4 to 7.8.0 in /backend [`#2741`](https://github.com/Human-Connection/Human-Connection/pull/2741)
- build(deps-dev): bump eslint-plugin-import from 2.19.1 to 2.20.0 in /backend [`#2737`](https://github.com/Human-Connection/Human-Connection/pull/2737)
- build(deps-dev): bump @babel/preset-env from 7.7.7 to 7.8.2 [`#2732`](https://github.com/Human-Connection/Human-Connection/pull/2732)
- build(deps): bump @nuxtjs/axios from 5.9.2 to 5.9.3 in /webapp [`#2740`](https://github.com/Human-Connection/Human-Connection/pull/2740)
- build(deps-dev): bump @storybook/vue from 5.2.8 to 5.3.1 in /webapp [`#2738`](https://github.com/Human-Connection/Human-Connection/pull/2738)
- build(deps-dev): bump cypress from 3.8.1 to 3.8.2 [`#2734`](https://github.com/Human-Connection/Human-Connection/pull/2734)
- build(deps-dev): bump @babel/node from 7.7.7 to 7.8.0 in /backend [`#2733`](https://github.com/Human-Connection/Human-Connection/pull/2733)
- build(deps-dev): bump eslint-plugin-jest from 23.3.0 to 23.6.0 in /backend [`#2731`](https://github.com/Human-Connection/Human-Connection/pull/2731)
- build(deps-dev): bump @babel/core from 7.7.7 to 7.8.0 [`#2730`](https://github.com/Human-Connection/Human-Connection/pull/2730)
- build(deps-dev): bump @babel/register from 7.7.7 to 7.8.0 [`#2729`](https://github.com/Human-Connection/Human-Connection/pull/2729)
- build(deps): bump nuxt from 2.10.2 to 2.11.0 in /webapp [`#2552`](https://github.com/Human-Connection/Human-Connection/pull/2552)
- Update yarn.lock after dependabot update [`#2724`](https://github.com/Human-Connection/Human-Connection/pull/2724)
- build(deps): bump @nuxtjs/axios from 5.8.0 to 5.9.2 in /webapp [`#2657`](https://github.com/Human-Connection/Human-Connection/pull/2657)
- Update to version 0.2.1 [`#2722`](https://github.com/Human-Connection/Human-Connection/pull/2722)
- docs(deployment): Explain how to setup metrics (#2825) [`#2411`](https://github.com/Human-Connection/Human-Connection/issues/2411) [`#2777`](https://github.com/Human-Connection/Human-Connection/issues/2777)
- refactor(modules): Various import fixes [`#2773`](https://github.com/Human-Connection/Human-Connection/issues/2773) [`#2774`](https://github.com/Human-Connection/Human-Connection/issues/2774)
- feat(webapp): Display deployed version in footer [`#1831`](https://github.com/Human-Connection/Human-Connection/issues/1831)
- fix #2229 [`#2229`](https://github.com/Human-Connection/Human-Connection/issues/2229)
- refactor: Make `db:setup` init stage of `migrate` [`b063847`](https://github.com/Human-Connection/Human-Connection/commit/b063847849a84db885337dc8e84e75ddaf87011f)
- Improve styling per @alina-beck review [`bcc1ab1`](https://github.com/Human-Connection/Human-Connection/commit/bcc1ab167e8b1dfdac1ec0a05a0c14e8234bcabc)
- test(cypress): Cover "Pinned post" feature [`d49afc2`](https://github.com/Human-Connection/Human-Connection/commit/d49afc25cfa1c1f98ed04f78dd3ff826cd85ae25)
#### [v0.2.1](https://github.com/Human-Connection/Human-Connection/compare/v0.2.0...v0.2.1) #### [v0.2.1](https://github.com/Human-Connection/Human-Connection/compare/v0.2.0...v0.2.1)
> 10 January 2020 > 10 January 2020
@ -109,9 +290,9 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Fix imageAspectRatio set to null UpdatePost [`#2588`](https://github.com/Human-Connection/Human-Connection/pull/2588) - Fix imageAspectRatio set to null UpdatePost [`#2588`](https://github.com/Human-Connection/Human-Connection/pull/2588)
- Update to version 0.2.0 [`#2584`](https://github.com/Human-Connection/Human-Connection/pull/2584) - Update to version 0.2.0 [`#2584`](https://github.com/Human-Connection/Human-Connection/pull/2584)
- fixes #2659 [`#2659`](https://github.com/Human-Connection/Human-Connection/issues/2659) - fixes #2659 [`#2659`](https://github.com/Human-Connection/Human-Connection/issues/2659)
- Convert block/unblock to blacklist/whitelist [`c297b83`](https://github.com/Human-Connection/Human-Connection/commit/c297b83f873edc61ddec370633b9b65896c56591)
- Rename blacklist/whitelist to mute/unmute [`ba3e9e1`](https://github.com/Human-Connection/Human-Connection/commit/ba3e9e1025bf432151c9bf1002045179b338ff7f)
- build(deps-dev): bump storybook-design-token in /webapp [`88d39c4`](https://github.com/Human-Connection/Human-Connection/commit/88d39c4a427cb86527b06201f3f5e96d53ac09a0) - build(deps-dev): bump storybook-design-token in /webapp [`88d39c4`](https://github.com/Human-Connection/Human-Connection/commit/88d39c4a427cb86527b06201f3f5e96d53ac09a0)
- Specs for Searches [`bc3aa51`](https://github.com/Human-Connection/Human-Connection/commit/bc3aa519d0e7a6e0242ecd37d611fd1a3df385d0)
- build(deps): bump apollo-server-express in /backend [`84df7b5`](https://github.com/Human-Connection/Human-Connection/commit/84df7b5a0a4845ab44d19946d877aef79691d38e)
#### [v0.2.0](https://github.com/Human-Connection/Human-Connection/compare/v0.1.13...v0.2.0) #### [v0.2.0](https://github.com/Human-Connection/Human-Connection/compare/v0.1.13...v0.2.0)
@ -497,7 +678,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- first implementation [`aeae72f`](https://github.com/Human-Connection/Human-Connection/commit/aeae72f6918861aa2a4c64d0b32c847d9e857e93) - first implementation [`aeae72f`](https://github.com/Human-Connection/Human-Connection/commit/aeae72f6918861aa2a4c64d0b32c847d9e857e93)
- build(deps-dev): bump eslint-plugin-jest in /backend [`6c1bd53`](https://github.com/Human-Connection/Human-Connection/commit/6c1bd535ac482eb0a05d21e227a476800717a19e) - build(deps-dev): bump eslint-plugin-jest in /backend [`6c1bd53`](https://github.com/Human-Connection/Human-Connection/commit/6c1bd535ac482eb0a05d21e227a476800717a19e)
#### [v0.1.8](https://github.com/Human-Connection/Human-Connection/compare/0.1.7...v0.1.8) #### [v0.1.8](https://github.com/Human-Connection/Human-Connection/compare/v0.1.7...v0.1.8)
> 25 October 2019 > 25 October 2019
@ -519,7 +700,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- build(deps): bump @nuxtjs/apollo in /webapp [`26c21b5`](https://github.com/Human-Connection/Human-Connection/commit/26c21b5b76c96206d98ff6bbfdbd1ca973ffcd4f) - build(deps): bump @nuxtjs/apollo in /webapp [`26c21b5`](https://github.com/Human-Connection/Human-Connection/commit/26c21b5b76c96206d98ff6bbfdbd1ca973ffcd4f)
- Finish redesign of moderators report list [`15d28aa`](https://github.com/Human-Connection/Human-Connection/commit/15d28aa8ef84788aa640aac67838380bfacf63b7) - Finish redesign of moderators report list [`15d28aa`](https://github.com/Human-Connection/Human-Connection/commit/15d28aa8ef84788aa640aac67838380bfacf63b7)
#### [0.1.7](https://github.com/Human-Connection/Human-Connection/compare/0.1.6...0.1.7) #### [v0.1.7](https://github.com/Human-Connection/Human-Connection/compare/v0.1.6...v0.1.7)
> 23 October 2019 > 23 October 2019
@ -535,7 +716,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- build(deps-dev): bump @vue/cli-shared-utils in /webapp [`a5d993c`](https://github.com/Human-Connection/Human-Connection/commit/a5d993c761b2f92c3f44b6f83592ea4c1d822606) - build(deps-dev): bump @vue/cli-shared-utils in /webapp [`a5d993c`](https://github.com/Human-Connection/Human-Connection/commit/a5d993c761b2f92c3f44b6f83592ea4c1d822606)
- Fix block user workflow [`44e5437`](https://github.com/Human-Connection/Human-Connection/commit/44e54372c4148fafae1095d172d1a52a87b3b1b2) - Fix block user workflow [`44e5437`](https://github.com/Human-Connection/Human-Connection/commit/44e54372c4148fafae1095d172d1a52a87b3b1b2)
#### [0.1.6](https://github.com/Human-Connection/Human-Connection/compare/0.1.5...0.1.6) #### [v0.1.6](https://github.com/Human-Connection/Human-Connection/compare/v0.1.5...v0.1.6)
> 22 October 2019 > 22 October 2019
@ -569,7 +750,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Refactor tests for querying reported resources [`4e42017`](https://github.com/Human-Connection/Human-Connection/commit/4e42017afaa97fa87ec726a5bbd1605cca911375) - Refactor tests for querying reported resources [`4e42017`](https://github.com/Human-Connection/Human-Connection/commit/4e42017afaa97fa87ec726a5bbd1605cca911375)
- Return pinnedAt date from pinPost resolver/clean up [`be0c804`](https://github.com/Human-Connection/Human-Connection/commit/be0c8044e87e211f2578df151d9d2d11795a135f) - Return pinnedAt date from pinPost resolver/clean up [`be0c804`](https://github.com/Human-Connection/Human-Connection/commit/be0c8044e87e211f2578df151d9d2d11795a135f)
#### [0.1.5](https://github.com/Human-Connection/Human-Connection/compare/0.1.4...0.1.5) #### [v0.1.5](https://github.com/Human-Connection/Human-Connection/compare/v0.1.4...v0.1.5)
> 17 October 2019 > 17 October 2019
@ -627,7 +808,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Start adding missing portuguese translation [`33eb000`](https://github.com/Human-Connection/Human-Connection/commit/33eb000ee33e5aa513083450f0a00abd7240efb0) - Start adding missing portuguese translation [`33eb000`](https://github.com/Human-Connection/Human-Connection/commit/33eb000ee33e5aa513083450f0a00abd7240efb0)
- refactor: restructure translations and components [`bb5d581`](https://github.com/Human-Connection/Human-Connection/commit/bb5d581906b5e6e723966c3dc687c7f309356841) - refactor: restructure translations and components [`bb5d581`](https://github.com/Human-Connection/Human-Connection/commit/bb5d581906b5e6e723966c3dc687c7f309356841)
#### [0.1.4](https://github.com/Human-Connection/Human-Connection/compare/0.1.3...0.1.4) #### [v0.1.4](https://github.com/Human-Connection/Human-Connection/compare/v0.1.3...v0.1.4)
> 10 October 2019 > 10 October 2019
@ -665,7 +846,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Fix lint, update tests [`bced698`](https://github.com/Human-Connection/Human-Connection/commit/bced6983ea1f51736e989eab6a41166723a6a6ca) - Fix lint, update tests [`bced698`](https://github.com/Human-Connection/Human-Connection/commit/bced6983ea1f51736e989eab6a41166723a6a6ca)
- add test embeds and links [`7cc139e`](https://github.com/Human-Connection/Human-Connection/commit/7cc139e879ac7ea912e82ea7eff14f7b67eddb4a) - add test embeds and links [`7cc139e`](https://github.com/Human-Connection/Human-Connection/commit/7cc139e879ac7ea912e82ea7eff14f7b67eddb4a)
#### [0.1.3](https://github.com/Human-Connection/Human-Connection/compare/0.1.2...0.1.3) #### [v0.1.3](https://github.com/Human-Connection/Human-Connection/compare/v0.1.2...v0.1.3)
> 4 October 2019 > 4 October 2019
@ -685,7 +866,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Set hasMore to false when returned Posts are equal to pageSize [`6f1c5e3`](https://github.com/Human-Connection/Human-Connection/commit/6f1c5e3efa3b77e72172592a0b5e4ea52158e642) - Set hasMore to false when returned Posts are equal to pageSize [`6f1c5e3`](https://github.com/Human-Connection/Human-Connection/commit/6f1c5e3efa3b77e72172592a0b5e4ea52158e642)
- refactor: use named slot for additional text [`3912b21`](https://github.com/Human-Connection/Human-Connection/commit/3912b21ea2f24e2e25682060b7166d1511442e6e) - refactor: use named slot for additional text [`3912b21`](https://github.com/Human-Connection/Human-Connection/commit/3912b21ea2f24e2e25682060b7166d1511442e6e)
#### [0.1.2](https://github.com/Human-Connection/Human-Connection/compare/0.1.1...0.1.2) #### [v0.1.2](https://github.com/Human-Connection/Human-Connection/compare/v0.1.1...v0.1.2)
> 2 October 2019 > 2 October 2019
@ -744,7 +925,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- build(deps): bump @nuxtjs/apollo in /webapp [`4648080`](https://github.com/Human-Connection/Human-Connection/commit/4648080a74fa6df60d6bb9b34d1db5030a9d4124) - build(deps): bump @nuxtjs/apollo in /webapp [`4648080`](https://github.com/Human-Connection/Human-Connection/commit/4648080a74fa6df60d6bb9b34d1db5030a9d4124)
- Write and refactor backend test which are supposed to fail at first [`6ad9dc2`](https://github.com/Human-Connection/Human-Connection/commit/6ad9dc27e937eb263914846c073172906aa661e1) - Write and refactor backend test which are supposed to fail at first [`6ad9dc2`](https://github.com/Human-Connection/Human-Connection/commit/6ad9dc27e937eb263914846c073172906aa661e1)
#### [0.1.1](https://github.com/Human-Connection/Human-Connection/compare/0.1.0...0.1.1) #### [v0.1.1](https://github.com/Human-Connection/Human-Connection/compare/v0.1.0...v0.1.1)
> 27 September 2019 > 27 September 2019
@ -818,7 +999,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Run with tag 0.1.0 [`c634ad2`](https://github.com/Human-Connection/Human-Connection/commit/c634ad264bd99dd1a87a86f870d7877aa751dc38) - Run with tag 0.1.0 [`c634ad2`](https://github.com/Human-Connection/Human-Connection/commit/c634ad264bd99dd1a87a86f870d7877aa751dc38)
- zwischenspeichern [`e4c7c11`](https://github.com/Human-Connection/Human-Connection/commit/e4c7c1125da6f8fa259241b4d3838b1e7b1e24a2) - zwischenspeichern [`e4c7c11`](https://github.com/Human-Connection/Human-Connection/commit/e4c7c1125da6f8fa259241b4d3838b1e7b1e24a2)
#### 0.1.0 #### v0.1.0
> 18 September 2019 > 18 September 2019

View File

@ -64,7 +64,7 @@ Regular pair programming sessions
* we team up and work on an issue together (often using Visual Studio live sharing sessions) * we team up and work on an issue together (often using Visual Studio live sharing sessions)
Open-Source Community Meeting Open-Source Community Meeting
* every Thursday 13:00 * bi-weekly on Mondays 13:00 (when there is no sprint retrospective)
* the link will be posted in the [discord chat](https://discord.gg/6ub73U3) and on the [Agile Ventures website](https://www.agileventures.org/events?utf8=%E2%9C%93&project_id=220&commit=Filter+by+Project) * the link will be posted in the [discord chat](https://discord.gg/6ub73U3) and on the [Agile Ventures website](https://www.agileventures.org/events?utf8=%E2%9C%93&project_id=220&commit=Filter+by+Project)
* all contributors welcome! * all contributors welcome!
@ -99,3 +99,34 @@ We believe in open source contributions as a learning experience everyone is
We use pair programming sessions as a tool for knowledge sharing. We can learn a lot from each other and only by sharing what we know and overcoming challenges together can we grow as a team and truly own this project collectively. We use pair programming sessions as a tool for knowledge sharing. We can learn a lot from each other and only by sharing what we know and overcoming challenges together can we grow as a team and truly own this project collectively.
As a volunteeer you have no commitment except your own self development and your awesomeness by contributing to this free and open-source software project. Cheers to you! As a volunteeer you have no commitment except your own self development and your awesomeness by contributing to this free and open-source software project. Cheers to you!
## Open-Source Bounties
There are so many good reasons to contribute to Human Connection
* You learn state-of-the-art technologies
* You build your portfolio
* You contribute to a good cause
Now there is one more good reason: You can receive a small fincancial
compensation for your contribution! :tada:
### How it works
Before you can benefit from the Open-Source bounty program you **must get one
pull request approved and merged for free**. You can choose something really
quick and easy. What's important is starting a working relationship with the
team, learning the workflow, and understanding this contribution guide. You can
filter issues by 'good first issue', to get an idea where to start. Please join
our our [community chat](https://human-connection.org/discord), too.
You can filter Github issues with label [bounty](https://github.com/Human-Connection/Human-Connection/issues?q=is%3Aopen+is%3Aissue+label%3Abounty). These issues should have a second label `€<amount>`
which indicate their respective financial compensation in Euros.
You can bill us after your pull request got approved and merged into `master`.
Payment methods are up to you: Bank transfer or PayPal is fine for us. Just send
us your invoice as .pdf file attached to an E-Mail once you are done.
Our Open-Source bounty program is a work-in-progress. Based on our future
experience we will make changes and improvements. So keep an eye on this
contribution guide.

View File

@ -52,6 +52,10 @@ Check out the [contribution guideline](./CONTRIBUTING.md), too!
[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/0)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/0)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/1)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/1)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/2)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/2)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/3)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/3)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/4)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/4)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/5)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/5)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/6)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/6)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/7)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/7) [![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/0)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/0)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/1)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/1)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/2)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/2)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/3)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/3)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/4)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/4)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/5)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/5)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/6)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/6)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/7)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/7)
## Open-Source Bounties
You can get a small financial compensation for your contribution :moneybag: See
details in our [Contribution Guidelines](./CONTRIBUTING.md#open-source-bounties).
## Attributions ## Attributions

View File

@ -32,6 +32,7 @@
* [Volume Snapshots](deployment/volumes/volume-snapshots/README.md) * [Volume Snapshots](deployment/volumes/volume-snapshots/README.md)
* [Reclaim Policy](deployment/volumes/reclaim-policy/README.md) * [Reclaim Policy](deployment/volumes/reclaim-policy/README.md)
* [Velero](deployment/volumes/velero/README.md) * [Velero](deployment/volumes/velero/README.md)
* [Metrics](deployment/monitoring/README.md)
* [Legacy Migration](deployment/legacy-migration/README.md) * [Legacy Migration](deployment/legacy-migration/README.md)
* [Feature Specification](cypress/features.md) * [Feature Specification](cypress/features.md)
* [Code of conduct](CODE_OF_CONDUCT.md) * [Code of conduct](CODE_OF_CONDUCT.md)

View File

@ -1 +0,0 @@
0.2.1

View File

@ -53,6 +53,27 @@ can issue GraphQL requests or access GraphQL Playground in the browser.
![GraphQL Playground](../.gitbook/assets/graphql-playground.png) ![GraphQL Playground](../.gitbook/assets/graphql-playground.png)
### Database Indices and Constraints
Database indices and constraints need to be created when the database and the
backend is running:
{% tabs %}
{% tab title="Docker" %}
```bash
docker-compose exec backend yarn run db:migrate init
```
{% endtab %}
{% tab title="Without Docker" %}
```bash
# in folder backend/
# make sure your database is running on http://localhost:7474/browser/
yarn run db:migrate init
```
{% endtab %}
{% endtabs %}
#### Seed Database #### Seed Database
@ -73,7 +94,7 @@ $ docker-compose exec backend yarn run db:reset
# you could also wipe out your neo4j database and delete all volumes with: # you could also wipe out your neo4j database and delete all volumes with:
$ docker-compose down -v $ docker-compose down -v
# if container is not running, run this command to set up your database indeces and contstraints # if container is not running, run this command to set up your database indeces and contstraints
$ docker-compose run neo4j db_setup $ docker-compose run backend yarn run db:migrate init
``` ```
{% endtab %} {% endtab %}
@ -90,6 +111,38 @@ $ yarn run db:reset
{% endtab %} {% endtab %}
{% endtabs %} {% endtabs %}
### Data migrations
Although Neo4J is schema-less,you might find yourself in a situation in which
you have to migrate your data e.g. because your data modeling has changed.
{% tabs %}
{% tab title="Docker" %}
Generate a data migration file:
```bash
$ docker-compose exec backend yarn run db:migrate:create your_data_migration
# Edit the file in ./src/db/migrations/
```
To run the migration:
```bash
$ docker-compose exec backend yarn run db:migrate up
```
{% endtab %}
{% tab title="Without Docker" %}
Generate a data migration file:
```bash
$ yarn run db:migrate:create your_data_migration
# Edit the file in ./src/db/migrations/
```
To run the migration:
```bash
$ yarn run db:migrate up
```
{% endtab %}
{% endtabs %}
# Testing # Testing
**Beware**: We have no multiple database setup at the moment. We clean the **Beware**: We have no multiple database setup at the moment. We clean the

View File

@ -1,17 +1,22 @@
{ {
"name": "human-connection-backend", "name": "human-connection-backend",
"version": "0.0.1", "version": "0.2.2",
"description": "GraphQL Backend for Human Connection", "description": "GraphQL Backend for Human Connection",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {
"build": "babel src/ -d dist/ --copy-files", "__migrate": "migrate --compiler 'js:@babel/register' --migrations-dir ./src/db/migrations",
"prod:migrate": "migrate --migrations-dir ./dist/db/migrations --store ./dist/db/migrate/store.js",
"start": "node dist/", "start": "node dist/",
"build": "babel src/ -d dist/ --copy-files",
"dev": "nodemon --exec babel-node src/ -e js,gql", "dev": "nodemon --exec babel-node src/ -e js,gql",
"dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/index.js -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", "lint": "eslint src --config .eslintrc.js",
"test": "jest --forceExit --detectOpenHandles --runInBand", "test": "jest --forceExit --detectOpenHandles --runInBand",
"db:reset": "babel-node src/seed/reset-db.js", "db:clean": "babel-node src/db/clean.js",
"db:seed": "babel-node src/seed/seed-db.js" "db:reset": "yarn run db:clean",
"db:seed": "babel-node src/db/seed.js",
"db:migrate": "yarn run __migrate --store ./src/db/migrate/store.js",
"db:migrate:create": "yarn run __migrate --template-file ./src/db/migrate/template.js --date-format 'yyyymmddHHmmss' create"
}, },
"author": "Human Connection gGmbH", "author": "Human Connection gGmbH",
"license": "MIT", "license": "MIT",
@ -32,8 +37,8 @@
] ]
}, },
"dependencies": { "dependencies": {
"@hapi/joi": "^17.0.2", "@hapi/joi": "^17.1.0",
"@sentry/node": "^5.11.0", "@sentry/node": "^5.12.0",
"apollo-cache-inmemory": "~1.6.5", "apollo-cache-inmemory": "~1.6.5",
"apollo-client": "~2.6.8", "apollo-client": "~2.6.8",
"apollo-link-context": "~1.0.19", "apollo-link-context": "~1.0.19",
@ -44,42 +49,43 @@
"bcryptjs": "~2.4.3", "bcryptjs": "~2.4.3",
"cheerio": "~1.0.0-rc.3", "cheerio": "~1.0.0-rc.3",
"cors": "~2.8.5", "cors": "~2.8.5",
"cross-env": "~6.0.3", "cross-env": "~7.0.0",
"date-fns": "2.9.0", "date-fns": "2.9.0",
"debug": "~4.1.1", "debug": "~4.1.1",
"dotenv": "~8.2.0", "dotenv": "~8.2.0",
"express": "^4.17.1", "express": "^4.17.1",
"faker": "Marak/faker.js#master", "faker": "Marak/faker.js#master",
"graphql": "^14.5.8", "graphql": "^14.6.0",
"graphql-custom-directives": "~0.2.14", "graphql-custom-directives": "~0.2.14",
"graphql-iso-date": "~3.6.1", "graphql-iso-date": "~3.6.1",
"graphql-middleware": "~4.0.2", "graphql-middleware": "~4.0.2",
"graphql-middleware-sentry": "^3.2.1", "graphql-middleware-sentry": "^3.2.1",
"graphql-shield": "~7.0.7", "graphql-shield": "~7.0.9",
"graphql-tag": "~2.10.1", "graphql-tag": "~2.10.1",
"helmet": "~3.21.2", "helmet": "~3.21.2",
"jsonwebtoken": "~8.5.1", "jsonwebtoken": "~8.5.1",
"linkifyjs": "~2.1.8", "linkifyjs": "~2.1.8",
"lodash": "~4.17.14", "lodash": "~4.17.14",
"merge-graphql-schemas": "^1.7.6", "merge-graphql-schemas": "^1.7.6",
"metascraper": "^5.10.2", "metascraper": "^5.11.0",
"metascraper-audio": "^5.9.5", "metascraper-audio": "^5.10.6",
"metascraper-author": "^5.9.5", "metascraper-author": "^5.10.6",
"metascraper-clearbit-logo": "^5.3.0", "metascraper-clearbit-logo": "^5.3.0",
"metascraper-date": "^5.9.5", "metascraper-date": "^5.10.7",
"metascraper-description": "^5.9.5", "metascraper-description": "^5.11.0",
"metascraper-image": "^5.9.5", "metascraper-image": "^5.10.7",
"metascraper-lang": "^5.9.5", "metascraper-lang": "^5.10.7",
"metascraper-lang-detector": "^4.10.2", "metascraper-lang-detector": "^4.10.2",
"metascraper-logo": "^5.9.5", "metascraper-logo": "^5.10.7",
"metascraper-publisher": "^5.9.5", "metascraper-publisher": "^5.10.7",
"metascraper-soundcloud": "^5.9.5", "metascraper-soundcloud": "^5.10.7",
"metascraper-title": "^5.9.5", "metascraper-title": "^5.10.6",
"metascraper-url": "^5.9.5", "metascraper-url": "^5.10.6",
"metascraper-video": "^5.9.5", "metascraper-video": "^5.10.7",
"metascraper-youtube": "^5.9.5", "metascraper-youtube": "^5.10.7",
"migrate": "^1.6.2",
"minimatch": "^3.0.4", "minimatch": "^3.0.4",
"mustache": "^3.2.1", "mustache": "^4.0.0",
"neo4j-driver": "^4.0.1", "neo4j-driver": "^4.0.1",
"neo4j-graphql-js": "^2.11.5", "neo4j-graphql-js": "^2.11.5",
"neode": "^0.3.7", "neode": "^0.3.7",
@ -88,37 +94,37 @@
"nodemailer-html-to-text": "^3.1.0", "nodemailer-html-to-text": "^3.1.0",
"npm-run-all": "~4.1.5", "npm-run-all": "~4.1.5",
"request": "~2.88.0", "request": "~2.88.0",
"sanitize-html": "~1.20.1", "sanitize-html": "~1.21.1",
"slug": "~2.1.0", "slug": "~2.1.1",
"trunc-html": "~1.1.2", "trunc-html": "~1.1.2",
"uuid": "~3.3.3", "uuid": "~3.4.0",
"validator": "^12.1.0", "validator": "^12.2.0",
"wait-on": "~3.3.0", "wait-on": "~4.0.0",
"xregexp": "^4.2.4" "xregexp": "^4.2.4"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "~7.8.3", "@babel/cli": "~7.8.4",
"@babel/core": "~7.8.3", "@babel/core": "~7.8.4",
"@babel/node": "~7.8.3", "@babel/node": "~7.8.4",
"@babel/plugin-proposal-throw-expressions": "^7.8.3", "@babel/plugin-proposal-throw-expressions": "^7.8.3",
"@babel/preset-env": "~7.8.3", "@babel/preset-env": "~7.8.4",
"@babel/register": "~7.8.3", "@babel/register": "^7.8.3",
"apollo-server-testing": "~2.9.16", "apollo-server-testing": "~2.9.16",
"babel-core": "~7.0.0-0", "babel-core": "~7.0.0-0",
"babel-eslint": "~10.0.3", "babel-eslint": "~10.0.3",
"babel-jest": "~24.9.0", "babel-jest": "~25.1.0",
"chai": "~4.2.0", "chai": "~4.2.0",
"cucumber": "~6.0.5", "cucumber": "~6.0.5",
"eslint": "~6.8.0", "eslint": "~6.8.0",
"eslint-config-prettier": "~6.9.0", "eslint-config-prettier": "~6.10.0",
"eslint-config-standard": "~14.1.0", "eslint-config-standard": "~14.1.0",
"eslint-plugin-import": "~2.20.0", "eslint-plugin-import": "~2.20.1",
"eslint-plugin-jest": "~23.6.0", "eslint-plugin-jest": "~23.6.0",
"eslint-plugin-node": "~11.0.0", "eslint-plugin-node": "~11.0.0",
"eslint-plugin-prettier": "~3.1.2", "eslint-plugin-prettier": "~3.1.2",
"eslint-plugin-promise": "~4.2.1", "eslint-plugin-promise": "~4.2.1",
"eslint-plugin-standard": "~4.0.1", "eslint-plugin-standard": "~4.0.1",
"jest": "~24.9.0", "jest": "~25.1.0",
"nodemon": "~2.0.2", "nodemon": "~2.0.2",
"prettier": "~1.19.1", "prettier": "~1.19.1",
"supertest": "~4.0.2" "supertest": "~4.0.2"

View File

@ -1,6 +1,6 @@
import { handler } from './webfinger' import { handler } from './webfinger'
import Factory from '../../seed/factories' import Factory from '../../factories'
import { getDriver } from '../../bootstrap/neo4j' import { getDriver } from '../../db/neo4j'
let resource, res, json, status, contentType let resource, res, json, status, contentType

View File

@ -1,7 +1,8 @@
import dotenv from 'dotenv' import dotenv from 'dotenv'
import path from 'path' if (require.resolve) {
// are we in a nodejs environment?
dotenv.config({ path: path.resolve(__dirname, '../../.env') }) dotenv.config({ path: require.resolve('../../.env') })
}
const { const {
MAPBOX_TOKEN, MAPBOX_TOKEN,
@ -27,6 +28,15 @@ export const requiredConfigs = {
PRIVATE_KEY_PASSPHRASE, PRIVATE_KEY_PASSPHRASE,
} }
if (require.resolve) {
// are we in a nodejs environment?
Object.entries(requiredConfigs).map(entry => {
if (!entry[1]) {
throw new Error(`ERROR: "${entry[0]}" env variable is missing.`)
}
})
}
export const smtpConfigs = { export const smtpConfigs = {
SMTP_HOST, SMTP_HOST,
SMTP_PORT, SMTP_PORT,

View File

@ -1,4 +1,4 @@
import { cleanDatabase } from './factories' import { cleanDatabase } from '../factories'
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
throw new Error(`You cannot clean the database in production environment!`) throw new Error(`You cannot clean the database in production environment!`)

View File

@ -0,0 +1,97 @@
import { getDriver, getNeode } from '../../db/neo4j'
class Store {
async init(next) {
const neode = getNeode()
const { driver } = neode
const session = driver.session()
// eslint-disable-next-line no-console
const writeTxResultPromise = session.writeTransaction(async txc => {
await txc.run('CALL apoc.schema.assert({},{},true)') // drop all indices
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"])',
].map(statement => txc.run(statement)),
)
})
try {
await writeTxResultPromise
await getNeode().schema.install()
// eslint-disable-next-line no-console
console.log('Successfully created database indices and constraints!')
next()
} catch (error) {
console.log(error) // eslint-disable-line no-console
next(error, null)
} finally {
session.close()
driver.close()
}
}
async load(next) {
const driver = getDriver()
const session = driver.session()
const readTxResultPromise = session.readTransaction(async txc => {
const result = await txc.run(
'MATCH (migration:Migration) RETURN migration {.*} ORDER BY migration.timestamp DESC',
)
return result.records.map(r => r.get('migration'))
})
try {
const migrations = await readTxResultPromise
if (migrations.length <= 0) {
// eslint-disable-next-line no-console
console.log(
"No migrations found in database. If it's the first time you run migrations, then this is normal.",
)
return next(null, {})
}
const [{ title: lastRun }] = migrations
next(null, { lastRun, migrations })
} catch (error) {
console.log(error) // eslint-disable-line no-console
next(error)
} finally {
session.close()
}
}
async save(set, next) {
const driver = getDriver()
const session = driver.session()
const { migrations } = set
const writeTxResultPromise = session.writeTransaction(txc => {
return Promise.all(
migrations.map(async migration => {
const { title, description, timestamp } = migration
const properties = { title, description, timestamp }
const migrationResult = await txc.run(
`
MERGE (migration:Migration { title: $properties.title })
ON MATCH SET
migration += $properties
ON CREATE SET
migration += $properties,
migration.migratedAt = toString(datetime())
`,
{ properties },
)
return migrationResult
}),
)
})
try {
await writeTxResultPromise
next()
} catch (error) {
console.log(error) // eslint-disable-line no-console
next(error)
} finally {
session.close()
}
}
}
module.exports = Store

View File

@ -0,0 +1,45 @@
import { getDriver } from '../../db/neo4j'
export const description = ''
export async function up(next) {
const driver = getDriver()
const session = driver.session()
const transaction = session.beginTransaction()
try {
// Implement your migration here.
await transaction.run(``)
await transaction.commit()
next()
} catch (error) {
// eslint-disable-next-line no-console
console.log(error)
await transaction.rollback()
// eslint-disable-next-line no-console
console.log('rolled back')
} finally {
session.close()
}
}
export async function down(next) {
const driver = getDriver()
const session = driver.session()
const transaction = session.beginTransaction()
try {
// Implement your migration here.
await transaction.run(``)
await transaction.commit()
next()
} catch (error) {
// eslint-disable-next-line no-console
console.log(error)
await transaction.rollback()
// eslint-disable-next-line no-console
console.log('rolled back')
} finally {
session.close()
}
}

View File

@ -0,0 +1,83 @@
import { throwError, concat } from 'rxjs'
import { flatMap, mergeMap, map, catchError, filter } from 'rxjs/operators'
import { getDriver } from '../neo4j'
import normalizeEmail from '../../schema/resolvers//helpers/normalizeEmail'
export const description = `
This migration merges duplicate :User and :EmailAddress nodes. It became
necessary after we implemented the email normalization but forgot to migrate
the existing data. Some (40) users decided to just register with a new account
but the same email address. On signup our backend would normalize the email,
which is good, but would also keep the existing unnormalized email address.
This led to about 40 duplicate user and email address nodes in our database.
`
export function up(next) {
const driver = getDriver()
const rxSession = driver.rxSession()
rxSession
.beginTransaction()
.pipe(
flatMap(txc =>
concat(
txc
.run('MATCH (email:EmailAddress) RETURN email {.email}')
.records()
.pipe(
map(record => {
const { email } = record.get('email')
const normalizedEmail = normalizeEmail(email)
return { email, normalizedEmail }
}),
filter(({ email, normalizedEmail }) => email !== normalizedEmail),
mergeMap(({ email, normalizedEmail }) => {
return txc
.run(
`
MATCH (oldUser:User)-[:PRIMARY_EMAIL]->(oldEmail:EmailAddress {email: $email})
MATCH (user:User)-[:PRIMARY_EMAIL]->(email:EmailAddress {email: $normalizedEmail})
WITH oldUser, oldEmail, user, email
CALL apoc.refactor.mergeNodes([user, oldUser], { properties: { createdAt: 'overwrite', \`.*\`: 'discard' }, mergeRels: true }) YIELD node as mergedUser
CALL apoc.refactor.mergeNodes([email, oldEmail], { properties: { createdAt: 'overwrite', verifiedAt: 'overwrite', \`.*\`: 'discard' }, mergeRels: true }) YIELD node as mergedEmail
RETURN user {.*}, email {.*}
`,
{ email, normalizedEmail },
)
.records()
.pipe(
map(r => ({
oldEmail: email,
email: r.get('email'),
user: r.get('user'),
})),
)
}),
),
txc.commit(),
).pipe(catchError(err => txc.rollback().pipe(throwError(err)))),
),
)
.subscribe({
next: ({ user, email, oldUser, oldEmail }) =>
// eslint-disable-next-line no-console
console.log(`
Merged:
=============================
userId: ${user.id}
email: ${oldEmail} => ${email.email}
=============================
`),
complete: () => {
// eslint-disable-next-line no-console
console.log('Merging of duplicate users completed')
next()
},
error: error => {
next(new Error(error), null)
},
})
}
export function down(next) {
next(new Error('Irreversible migration'))
}

View File

@ -0,0 +1,77 @@
import { throwError, concat } from 'rxjs'
import { flatMap, mergeMap, map, catchError } from 'rxjs/operators'
import { getDriver } from '../neo4j'
export const description = `
This migration merges duplicate :Location nodes. It became
necessary after we realized that we had not set up constraints for Location.id in production.
`
export function up(next) {
const driver = getDriver()
const rxSession = driver.rxSession()
rxSession
.beginTransaction()
.pipe(
flatMap(transaction =>
concat(
transaction
.run(
`
MATCH (location:Location)
RETURN location {.id}
`,
)
.records()
.pipe(
map(record => {
const { id: locationId } = record.get('location')
return { locationId }
}),
mergeMap(({ locationId }) => {
return transaction
.run(
`
MATCH(location:Location {id: $locationId}), (location2:Location {id: $locationId})
WHERE location.id = location2.id AND id(location) < id(location2)
CALL apoc.refactor.mergeNodes([location, location2], { properties: 'combine', mergeRels: true }) YIELD node as updatedLocation
RETURN location {.*},updatedLocation {.*}
`,
{ locationId },
)
.records()
.pipe(
map(record => ({
location: record.get('location'),
updatedLocation: record.get('updatedLocation'),
})),
)
}),
),
transaction.commit(),
).pipe(catchError(error => transaction.rollback().pipe(throwError(error)))),
),
)
.subscribe({
next: ({ updatedLocation, location }) =>
// eslint-disable-next-line no-console
console.log(`
Merged:
=============================
locationId: ${location.id}
updatedLocation: ${location.id} => ${updatedLocation.id}
=============================
`),
complete: () => {
// eslint-disable-next-line no-console
console.log('Merging of duplicate locations completed')
next()
},
error: error => {
next(new Error(error), null)
},
})
}
export function down(next) {
next(new Error('Irreversible migration'))
}

View File

@ -0,0 +1,46 @@
import { getDriver } from '../../db/neo4j'
export const description = `
This migration creates a MUTED relationship between two edges(:User) that have a pre-existing BLOCKED relationship.
It also sets the createdAt date for the BLOCKED relationship to the datetime the migration was run. This became
necessary after we redefined what it means to block someone, and what it means to mute them. Muting is about filtering
another user's content, whereas blocking means preventing that user from interacting with you/your contributions.
A blocked user will still be able to see your contributions, but will not be able to interact with them and vice versa.
`
export async function up(next) {
const driver = getDriver()
const session = driver.session()
const transaction = session.beginTransaction()
try {
await transaction.run(
`
MATCH (blocker:User)-[blocked:BLOCKED]->(blockee:User)
MERGE (blocker)-[muted:MUTED]->(blockee)
SET muted.createdAt = toString(datetime()), blocked.createdAt = toString(datetime())
`,
)
await transaction.commit()
} catch (error) {
// eslint-disable-next-line no-console
console.log(error)
await transaction.rollback()
// eslint-disable-next-line no-console
console.log('rolled back')
} finally {
session.close()
}
}
export function down(next) {
const driver = getDriver()
const session = driver.session()
try {
// Rollback your migration here.
next()
} catch (err) {
next(err)
} finally {
session.close()
}
}

View File

@ -2,8 +2,8 @@ import faker from 'faker'
import sample from 'lodash/sample' import sample from 'lodash/sample'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import createServer from '../server' import createServer from '../server'
import Factory from './factories' import Factory from '../factories'
import { getNeode, getDriver } from '../bootstrap/neo4j' import { getNeode, getDriver } from '../db/neo4j'
import { gql } from '../helpers/jest' import { gql } from '../helpers/jest'
const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
@ -226,6 +226,10 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
dewey.relateTo(huey, 'following'), dewey.relateTo(huey, 'following'),
louie.relateTo(jennyRostock, 'following'), louie.relateTo(jennyRostock, 'following'),
huey.relateTo(dagobert, 'muted'),
dewey.relateTo(dagobert, 'muted'),
louie.relateTo(dagobert, 'muted'),
dagobert.relateTo(huey, 'blocked'), dagobert.relateTo(huey, 'blocked'),
dagobert.relateTo(dewey, 'blocked'), dagobert.relateTo(dewey, 'blocked'),
dagobert.relateTo(louie, 'blocked'), dagobert.relateTo(louie, 'blocked'),

View File

@ -1,30 +1,18 @@
import { getDriver, getNeode } from '../../bootstrap/neo4j' import { getDriver, getNeode } from '../db/neo4j'
import createBadge from './badges.js'
import createUser from './users.js'
import createPost from './posts.js'
import createComment from './comments.js'
import createCategory from './categories.js'
import createTag from './tags.js'
import createSocialMedia from './socialMedia.js'
import createLocation from './locations.js'
import createEmailAddress from './emailAddresses.js'
import createDonations from './donations.js'
import createUnverifiedEmailAddresss from './unverifiedEmailAddresses.js'
import createReport from './reports.js'
const factories = { const factories = {
Badge: createBadge, Badge: require('./badges.js').default,
User: createUser, User: require('./users.js').default,
Post: createPost, Post: require('./posts.js').default,
Comment: createComment, Comment: require('./comments.js').default,
Category: createCategory, Category: require('./categories.js').default,
Tag: createTag, Tag: require('./tags.js').default,
SocialMedia: createSocialMedia, SocialMedia: require('./socialMedia.js').default,
Location: createLocation, Location: require('./locations.js').default,
EmailAddress: createEmailAddress, EmailAddress: require('./emailAddresses.js').default,
UnverifiedEmailAddress: createUnverifiedEmailAddresss, UnverifiedEmailAddress: require('./unverifiedEmailAddresses.js').default,
Donations: createDonations, Donations: require('./donations.js').default,
Report: createReport, Report: require('./reports.js').default,
} }
export const cleanDatabase = async (options = {}) => { export const cleanDatabase = async (options = {}) => {
@ -34,7 +22,7 @@ export const cleanDatabase = async (options = {}) => {
await session.writeTransaction(transaction => { await session.writeTransaction(transaction => {
return transaction.run( return transaction.run(
` `
MATCH (everything) MATCH (everything)
DETACH DELETE everything DETACH DELETE everything
`, `,
) )

View File

@ -21,11 +21,15 @@ export default function create() {
categoryIds: [], categoryIds: [],
imageBlurred: false, imageBlurred: false,
imageAspectRatio: 1.333, imageAspectRatio: 1.333,
pinned: null,
} }
args = { args = {
...defaults, ...defaults,
...args, ...args,
} }
// Convert false to null
args.pinned = args.pinned || null
args.slug = args.slug || slugify(args.title, { lower: true }) args.slug = args.slug || slugify(args.title, { lower: true })
args.contentExcerpt = args.contentExcerpt || args.content args.contentExcerpt = args.contentExcerpt || args.content
@ -36,7 +40,6 @@ export default function create() {
if (categoryIds) if (categoryIds)
categories = await Promise.all(categoryIds.map(id => neodeInstance.find('Category', id))) categories = await Promise.all(categoryIds.map(id => neodeInstance.find('Category', id)))
categories = categories || (await Promise.all([factoryInstance.create('Category')])) categories = categories || (await Promise.all([factoryInstance.create('Category')]))
const { tagIds = [] } = args const { tagIds = [] } = args
delete args.tags delete args.tags
const tags = await Promise.all( const tags = await Promise.all(
@ -51,9 +54,33 @@ export default function create() {
if (author && authorId) throw new Error('You provided both author and authorId') if (author && authorId) throw new Error('You provided both author and authorId')
if (authorId) author = await neodeInstance.find('User', authorId) if (authorId) author = await neodeInstance.find('User', authorId)
author = author || (await factoryInstance.create('User')) author = author || (await factoryInstance.create('User'))
const post = await neodeInstance.create('Post', args) const post = await neodeInstance.create('Post', args)
const { commentContent } = args
let comment
delete args.commentContent
if (commentContent)
comment = await factoryInstance.create('Comment', {
contentExcerpt: commentContent,
post,
author,
})
await post.relateTo(author, 'author') await post.relateTo(author, 'author')
if (comment) await post.relateTo(comment, 'comments')
if (args.pinned) {
args.pinnedAt = args.pinnedAt || new Date().toISOString()
if (!args.pinnedBy) {
const admin = await factoryInstance.create('User', {
role: 'admin',
updatedAt: new Date().toISOString(),
})
await admin.relateTo(post, 'pinned')
args.pinnedBy = admin
}
}
await Promise.all(categories.map(c => c.relateTo(post, 'post'))) await Promise.all(categories.map(c => c.relateTo(post, 'post')))
await Promise.all(tags.map(t => t.relateTo(post, 'post'))) await Promise.all(tags.map(t => t.relateTo(post, 'post')))
return post return post

View File

@ -1,6 +1,6 @@
import faker from 'faker' import faker from 'faker'
import uuid from 'uuid/v4' import uuid from 'uuid/v4'
import encryptPassword from '../../helpers/encryptPassword' import encryptPassword from '../helpers/encryptPassword'
import slugify from 'slug' import slugify from 'slug'
export default function create() { export default function create() {

View File

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

View File

@ -1,5 +1,5 @@
import Factory from '../seed/factories/index' import Factory from '../factories/index'
import { getDriver, getNeode } from '../bootstrap/neo4j' import { getDriver, getNeode } from '../db/neo4j'
import decode from './decode' import decode from './decode'
const factory = Factory() const factory = Factory()

View File

@ -1,7 +1,7 @@
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import Factory from '../../seed/factories' import Factory from '../../factories'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import { getNeode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'
let server let server

View File

@ -50,7 +50,7 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => {
MATCH (post: Post { id: $id })<-[:WROTE]-(author: User) MATCH (post: Post { id: $id })<-[:WROTE]-(author: User)
MATCH (user: User) MATCH (user: User)
WHERE user.id in $idsOfUsers WHERE user.id in $idsOfUsers
AND NOT (user)<-[:BLOCKED]-(author) AND NOT (user)-[:BLOCKED]-(author)
MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user) MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user)
` `
break break
@ -60,8 +60,8 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => {
MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(author: User) MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(author: User)
MATCH (user: User) MATCH (user: User)
WHERE user.id in $idsOfUsers WHERE user.id in $idsOfUsers
AND NOT (user)<-[:BLOCKED]-(author) AND NOT (user)-[:BLOCKED]-(author)
AND NOT (user)<-[:BLOCKED]-(postAuthor) AND NOT (user)-[:BLOCKED]-(postAuthor)
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user) MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user)
` `
break break

View File

@ -1,7 +1,7 @@
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import Factory from '../../seed/factories' import Factory from '../../factories'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import { getNeode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'
let server, query, mutate, notifiedUser, authenticatedUser let server, query, mutate, notifiedUser, authenticatedUser

View File

@ -1,6 +1,6 @@
import { gql } from '../helpers/jest' import { gql } from '../helpers/jest'
import Factory from '../seed/factories' import Factory from '../factories'
import { getNeode, getDriver } from '../bootstrap/neo4j' import { getNeode, getDriver } from '../db/neo4j'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import createServer from '../server' import createServer from '../server'

View File

@ -1,5 +1,5 @@
import { rule, shield, deny, allow, or } from 'graphql-shield' import { rule, shield, deny, allow, or } from 'graphql-shield'
import { getNeode } from '../bootstrap/neo4j' import { getNeode } from '../db/neo4j'
import CONFIG from '../config' import CONFIG from '../config'
const debug = !!CONFIG.DEBUG const debug = !!CONFIG.DEBUG
@ -101,6 +101,7 @@ export default shield(
Badge: allow, Badge: allow,
PostsEmotionsCountByEmotion: allow, PostsEmotionsCountByEmotion: allow,
PostsEmotionsByCurrentUser: isAuthenticated, PostsEmotionsByCurrentUser: isAuthenticated,
mutedUsers: isAuthenticated,
blockedUsers: isAuthenticated, blockedUsers: isAuthenticated,
notifications: isAuthenticated, notifications: isAuthenticated,
Donations: isAuthenticated, Donations: isAuthenticated,
@ -137,8 +138,10 @@ export default shield(
resetPassword: allow, resetPassword: allow,
AddPostEmotions: isAuthenticated, AddPostEmotions: isAuthenticated,
RemovePostEmotions: isAuthenticated, RemovePostEmotions: isAuthenticated,
block: isAuthenticated, muteUser: isAuthenticated,
unblock: isAuthenticated, unmuteUser: isAuthenticated,
blockUser: isAuthenticated,
unblockUser: isAuthenticated,
markAsRead: isAuthenticated, markAsRead: isAuthenticated,
AddEmailAddress: isAuthenticated, AddEmailAddress: isAuthenticated,
VerifyEmailAddress: isAuthenticated, VerifyEmailAddress: isAuthenticated,

View File

@ -1,8 +1,8 @@
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import createServer from '../server' import createServer from '../server'
import Factory from '../seed/factories' import Factory from '../factories'
import { gql } from '../helpers/jest' import { gql } from '../helpers/jest'
import { getDriver, getNeode } from '../bootstrap/neo4j' import { getDriver, getNeode } from '../db/neo4j'
const factory = Factory() const factory = Factory()
const instance = getNeode() const instance = getNeode()

View File

@ -1,6 +1,6 @@
import Factory from '../seed/factories' import Factory from '../factories'
import { gql } from '../helpers/jest' import { gql } from '../helpers/jest'
import { getNeode, getDriver } from '../bootstrap/neo4j' import { getNeode, getDriver } from '../db/neo4j'
import createServer from '../server' import createServer from '../server'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'

View File

@ -1,6 +1,6 @@
import Factory from '../../seed/factories' import Factory from '../../factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'

View File

@ -1,6 +1,6 @@
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import Factory from '../../seed/factories' import Factory from '../../factories'
import { getNeode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import createServer from '../../server' import createServer from '../../server'

View File

@ -20,6 +20,7 @@ function clean(dirty) {
'hr', 'hr',
'b', 'b',
'i', 'i',
'u',
'em', 'em',
'strong', 'strong',
'a', 'a',

View File

@ -1,4 +1,4 @@
module.exports = { export default {
id: { type: 'string', primary: true, lowercase: true }, id: { type: 'string', primary: true, lowercase: true },
status: { type: 'string', valid: ['permanent', 'temporary'] }, status: { type: 'string', valid: ['permanent', 'temporary'] },
type: { type: 'string', valid: ['role', 'crowdfunding'] }, type: { type: 'string', valid: ['role', 'crowdfunding'] },

View File

@ -1,9 +1,9 @@
import uuid from 'uuid/v4' import uuid from 'uuid/v4'
module.exports = { export default {
id: { type: 'string', primary: true, default: uuid }, id: { type: 'string', primary: true, default: uuid },
name: { type: 'string', required: true, default: false }, name: { type: 'string', required: true, default: false },
slug: { type: 'string' }, slug: { type: 'string', unique: 'true' },
icon: { type: 'string', required: true, default: false }, icon: { type: 'string', required: true, default: false },
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
updatedAt: { updatedAt: {

View File

@ -1,6 +1,6 @@
import uuid from 'uuid/v4' import uuid from 'uuid/v4'
module.exports = { export default {
id: { type: 'string', primary: true, default: uuid }, id: { type: 'string', primary: true, default: uuid },
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
updatedAt: { updatedAt: {

View File

@ -1,6 +1,6 @@
import uuid from 'uuid/v4' import uuid from 'uuid/v4'
module.exports = { export default {
id: { type: 'string', primary: true, default: uuid }, id: { type: 'string', primary: true, default: uuid },
goal: { type: 'number' }, goal: { type: 'number' },
progress: { type: 'number' }, progress: { type: 'number' },

View File

@ -1,4 +1,4 @@
module.exports = { export default {
email: { type: 'string', primary: true, lowercase: true, email: true }, email: { type: 'string', primary: true, lowercase: true, email: true },
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
verifiedAt: { type: 'string', isoDate: true }, verifiedAt: { type: 'string', isoDate: true },

View File

@ -1,4 +1,4 @@
module.exports = { export default {
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
token: { type: 'string', primary: true, token: true }, token: { type: 'string', primary: true, token: true },
generatedBy: { generatedBy: {

View File

@ -1,4 +1,4 @@
module.exports = { export default {
id: { type: 'string', primary: true }, id: { type: 'string', primary: true },
lat: { type: 'number' }, lat: { type: 'number' },
lng: { type: 'number' }, lng: { type: 'number' },

View File

@ -0,0 +1,5 @@
export default {
title: { type: 'string', primary: true, token: true },
description: { type: 'string' },
migratedAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
}

View File

@ -1,6 +1,6 @@
import uuid from 'uuid/v4' import uuid from 'uuid/v4'
module.exports = { export default {
id: { type: 'string', primary: true, default: uuid }, id: { type: 'string', primary: true, default: uuid },
activityId: { type: 'string', allow: [null] }, activityId: { type: 'string', allow: [null] },
objectId: { type: 'string', allow: [null] }, objectId: { type: 'string', allow: [null] },
@ -11,7 +11,7 @@ module.exports = {
direction: 'in', direction: 'in',
}, },
title: { type: 'string', disallow: [null], min: 3 }, title: { type: 'string', disallow: [null], min: 3 },
slug: { type: 'string', allow: [null] }, slug: { type: 'string', allow: [null], unique: 'true' },
content: { type: 'string', disallow: [null], min: 3 }, content: { type: 'string', disallow: [null], min: 3 },
contentExcerpt: { type: 'string', allow: [null] }, contentExcerpt: { type: 'string', allow: [null] },
image: { type: 'string', allow: [null] }, image: { type: 'string', allow: [null] },
@ -41,4 +41,16 @@ module.exports = {
language: { type: 'string', allow: [null] }, language: { type: 'string', allow: [null] },
imageBlurred: { type: 'boolean', default: false }, imageBlurred: { type: 'boolean', default: false },
imageAspectRatio: { type: 'float', default: 1.0 }, imageAspectRatio: { type: 'float', default: 1.0 },
comments: {
type: 'relationship',
relationship: 'COMMENTS',
target: 'Comment',
direction: 'in',
properties: {
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
updatedAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
},
},
pinned: { type: 'boolean', default: null, valid: [null, true] },
pinnedAt: { type: 'string', isoDate: true },
} }

View File

@ -1,6 +1,6 @@
import uuid from 'uuid/v4' import uuid from 'uuid/v4'
module.exports = { export default {
id: { type: 'string', primary: true, default: uuid }, id: { type: 'string', primary: true, default: uuid },
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
updatedAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, updatedAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },

View File

@ -1,6 +1,6 @@
import uuid from 'uuid/v4' import uuid from 'uuid/v4'
module.exports = { export default {
id: { type: 'string', primary: true, default: uuid }, id: { type: 'string', primary: true, default: uuid },
url: { type: 'string', uri: true, required: true }, url: { type: 'string', uri: true, required: true },
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },

View File

@ -1,4 +1,4 @@
module.exports = { export default {
id: { type: 'string', primary: true }, id: { type: 'string', primary: true },
deleted: { type: 'boolean', default: false }, deleted: { type: 'boolean', default: false },
disabled: { type: 'boolean', default: false }, disabled: { type: 'boolean', default: false },

View File

@ -1,5 +1,5 @@
module.exports = { export default {
email: { type: 'string', primary: true, lowercase: true, email: true }, email: { type: 'string', lowercase: true, email: true },
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
nonce: { type: 'string', token: true }, nonce: { type: 'string', token: true },
belongsTo: { belongsTo: {

View File

@ -1,10 +1,10 @@
import uuid from 'uuid/v4' import uuid from 'uuid/v4'
module.exports = { export default {
id: { type: 'string', primary: true, default: uuid }, // TODO: should be type: 'uuid' but simplified for our tests id: { type: 'string', primary: true, default: uuid }, // TODO: should be type: 'uuid' but simplified for our tests
actorId: { type: 'string', allow: [null] }, actorId: { type: 'string', allow: [null] },
name: { type: 'string', disallow: [null], min: 3 }, name: { type: 'string', disallow: [null], min: 3 },
slug: { type: 'string', regex: /^[a-z0-9_-]+$/, lowercase: true }, slug: { type: 'string', unique: 'true', regex: /^[a-z0-9_-]+$/, lowercase: true },
encryptedPassword: 'string', encryptedPassword: 'string',
avatar: { type: 'string', allow: [null] }, avatar: { type: 'string', allow: [null] },
coverImg: { type: 'string', allow: [null] }, coverImg: { type: 'string', allow: [null] },
@ -77,6 +77,18 @@ module.exports = {
relationship: 'BLOCKED', relationship: 'BLOCKED',
target: 'User', target: 'User',
direction: 'out', direction: 'out',
properties: {
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
},
},
muted: {
type: 'relationship',
relationship: 'MUTED',
target: 'User',
direction: 'out',
properties: {
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
},
}, },
notifications: { notifications: {
type: 'relationship', type: 'relationship',

View File

@ -1,5 +1,5 @@
import Factory from '../seed/factories' import Factory from '../factories'
import { getNeode } from '../bootstrap/neo4j' import { getNeode } from '../db/neo4j'
const factory = Factory() const factory = Factory()
const neode = getNeode() const neode = getNeode()

View File

@ -1,16 +1,17 @@
// NOTE: We cannot use `fs` here to clean up the code. Cypress breaks on any npm // NOTE: We cannot use `fs` here to clean up the code. Cypress breaks on any npm
// module that is not browser-compatible. Node's `fs` module is server-side only // module that is not browser-compatible. Node's `fs` module is server-side only
export default { export default {
Badge: require('./Badge.js'), Badge: require('./Badge.js').default,
User: require('./User.js'), User: require('./User.js').default,
EmailAddress: require('./EmailAddress.js'), EmailAddress: require('./EmailAddress.js').default,
UnverifiedEmailAddress: require('./UnverifiedEmailAddress.js'), UnverifiedEmailAddress: require('./UnverifiedEmailAddress.js').default,
SocialMedia: require('./SocialMedia.js'), SocialMedia: require('./SocialMedia.js').default,
Post: require('./Post.js'), Post: require('./Post.js').default,
Comment: require('./Comment.js'), Comment: require('./Comment.js').default,
Category: require('./Category.js'), Category: require('./Category.js').default,
Tag: require('./Tag.js'), Tag: require('./Tag.js').default,
Location: require('./Location.js'), Location: require('./Location.js').default,
Donations: require('./Donations.js'), Donations: require('./Donations.js').default,
Report: require('./Report.js'), Report: require('./Report.js').default,
Migration: require('./Migration.js').default,
} }

View File

@ -1,8 +1,8 @@
import Factory from '../../seed/factories' import Factory from '../../factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import createServer from '../../server' import createServer from '../../server'
import { getNeode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
const driver = getDriver() const driver = getDriver()
const neode = getNeode() const neode = getNeode()

View File

@ -1,7 +1,7 @@
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import Factory from '../../seed/factories' import Factory from '../../factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'
let mutate, query, authenticatedUser, variables let mutate, query, authenticatedUser, variables

View File

@ -1,6 +1,6 @@
import Factory from '../../seed/factories' import Factory from '../../factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { getDriver, getNeode } from '../../bootstrap/neo4j' import { getDriver, getNeode } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'

View File

@ -1,4 +1,4 @@
import { getNeode } from '../../bootstrap/neo4j' import { getNeode } from '../../db/neo4j'
const neode = getNeode() const neode = getNeode()

View File

@ -1,6 +1,6 @@
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import Factory from '../../seed/factories' import Factory from '../../factories'
import { getDriver, getNeode } from '../../bootstrap/neo4j' import { getDriver, getNeode } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'

View File

@ -1,25 +0,0 @@
import { getBlockedUsers, getBlockedByUsers } from '../users.js'
import { mergeWith, isArray } from 'lodash'
export const filterForBlockedUsers = async (params, context) => {
if (!context.user) return params
const [blockedUsers, blockedByUsers] = await Promise.all([
getBlockedUsers(context),
getBlockedByUsers(context),
])
const blockedUsersIds = [...blockedByUsers.map(b => b.id), ...blockedUsers.map(b => b.id)]
if (!blockedUsersIds.length) return params
params.filter = mergeWith(
params.filter,
{
author_not: { id_in: blockedUsersIds },
},
(objValue, srcValue) => {
if (isArray(objValue)) {
return objValue.concat(srcValue)
}
},
)
return params
}

View File

@ -0,0 +1,22 @@
import { getMutedUsers } from '../users.js'
import { mergeWith, isArray } from 'lodash'
export const filterForMutedUsers = async (params, context) => {
if (!context.user) return params
const [mutedUsers] = await Promise.all([getMutedUsers(context)])
const mutedUsersIds = [...mutedUsers.map(user => user.id)]
if (!mutedUsersIds.length) return params
params.filter = mergeWith(
params.filter,
{
author_not: { id_in: mutedUsersIds },
},
(objValue, srcValue) => {
if (isArray(objValue)) {
return objValue.concat(srcValue)
}
},
)
return params
}

View File

@ -1,6 +1,6 @@
import Factory from '../../seed/factories' import Factory from '../../factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'

View File

@ -1,7 +1,7 @@
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import Factory from '../../seed/factories' import Factory from '../../factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'
const factory = Factory() const factory = Factory()

View File

@ -1,6 +1,6 @@
import Factory from '../../seed/factories' import Factory from '../../factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { getDriver } from '../../bootstrap/neo4j' import { getDriver } from '../../db/neo4j'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import createServer from '../.././server' import createServer from '../.././server'

View File

@ -1,6 +1,6 @@
import Factory from '../../seed/factories' import Factory from '../../factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import createPasswordReset from './helpers/createPasswordReset' import createPasswordReset from './helpers/createPasswordReset'
import createServer from '../../server' import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'

View File

@ -1,10 +1,10 @@
import uuid from 'uuid/v4' import uuid from 'uuid/v4'
import { neo4jgraphql } from 'neo4j-graphql-js' import { neo4jgraphql } from 'neo4j-graphql-js'
import { isEmpty } from 'lodash' import { isEmpty } from 'lodash'
import fileUpload from './fileUpload'
import { UserInputError } from 'apollo-server' import { UserInputError } from 'apollo-server'
import fileUpload from './fileUpload'
import Resolver from './helpers/Resolver' import Resolver from './helpers/Resolver'
import { filterForBlockedUsers } from './helpers/filterForBlockedUsers' import { filterForMutedUsers } from './helpers/filterForMutedUsers'
const maintainPinnedPosts = params => { const maintainPinnedPosts = params => {
const pinnedPostFilter = { pinned: true } const pinnedPostFilter = { pinned: true }
@ -19,16 +19,16 @@ const maintainPinnedPosts = params => {
export default { export default {
Query: { Query: {
Post: async (object, params, context, resolveInfo) => { Post: async (object, params, context, resolveInfo) => {
params = await filterForBlockedUsers(params, context) params = await filterForMutedUsers(params, context)
params = await maintainPinnedPosts(params) params = await maintainPinnedPosts(params)
return neo4jgraphql(object, params, context, resolveInfo) return neo4jgraphql(object, params, context, resolveInfo)
}, },
findPosts: async (object, params, context, resolveInfo) => { findPosts: async (object, params, context, resolveInfo) => {
params = await filterForBlockedUsers(params, context) params = await filterForMutedUsers(params, context)
return neo4jgraphql(object, params, context, resolveInfo) return neo4jgraphql(object, params, context, resolveInfo)
}, },
profilePagePosts: async (object, params, context, resolveInfo) => { profilePagePosts: async (object, params, context, resolveInfo) => {
params = await filterForBlockedUsers(params, context) params = await filterForMutedUsers(params, context)
return neo4jgraphql(object, params, context, resolveInfo) return neo4jgraphql(object, params, context, resolveInfo)
}, },
PostsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => { PostsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => {

View File

@ -1,7 +1,7 @@
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import Factory from '../../seed/factories' import Factory from '../../factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'
const driver = getDriver() const driver = getDriver()
@ -682,58 +682,62 @@ describe('UpdatePost', () => {
}) })
describe('PostOrdering', () => { describe('PostOrdering', () => {
let pinnedPost, admin
beforeEach(async () => { beforeEach(async () => {
;[pinnedPost] = await Promise.all([ await factory.create('Post', {
neode.create('Post', { id: 'im-a-pinned-post',
id: 'im-a-pinned-post', createdAt: '2019-11-22T17:26:29.070Z',
pinned: true, pinned: true,
}), })
neode.create('Post', { await factory.create('Post', {
id: 'i-was-created-after-pinned-post', id: 'i-was-created-before-pinned-post',
createdAt: '2019-10-22T17:26:29.070Z', // this should always be 3rd // fairly old, so this should be 3rd
}), createdAt: '2019-10-22T17:26:29.070Z',
])
admin = await user.update({
role: 'admin',
name: 'Admin',
updatedAt: new Date().toISOString(),
}) })
await admin.relateTo(pinnedPost, 'pinned')
}) })
it('pinned post appear first even when created before other posts', async () => { describe('order by `pinned_asc` and `createdAt_desc`', () => {
const postOrderingQuery = gql` beforeEach(() => {
query($orderBy: [_PostOrdering]) { // this is the ordering in the frontend
Post(orderBy: $orderBy) { variables = { orderBy: ['pinned_asc', 'createdAt_desc'] }
id })
pinnedAt
it('pinned post appear first even when created before other posts', async () => {
const postOrderingQuery = gql`
query($orderBy: [_PostOrdering]) {
Post(orderBy: $orderBy) {
id
pinned
createdAt
pinnedAt
}
} }
} `
` await expect(query({ query: postOrderingQuery, variables })).resolves.toMatchObject({
const expected = { data: {
data: { Post: [
Post: [ {
{ id: 'im-a-pinned-post',
id: 'im-a-pinned-post', pinned: true,
pinnedAt: expect.any(String), createdAt: '2019-11-22T17:26:29.070Z',
}, pinnedAt: expect.any(String),
{ },
id: 'p9876', {
pinnedAt: null, id: 'p9876',
}, pinned: null,
{ createdAt: expect.any(String),
id: 'i-was-created-after-pinned-post', pinnedAt: null,
pinnedAt: null, },
}, {
], id: 'i-was-created-before-pinned-post',
}, pinned: null,
errors: undefined, createdAt: '2019-10-22T17:26:29.070Z',
} pinnedAt: null,
variables = { orderBy: ['pinned_desc', 'createdAt_desc'] } },
await expect(query({ query: postOrderingQuery, variables })).resolves.toMatchObject( ],
expected, },
) errors: undefined,
})
})
}) })
}) })
}) })

View File

@ -1,5 +1,5 @@
import { UserInputError } from 'apollo-server' import { UserInputError } from 'apollo-server'
import { getNeode } from '../../bootstrap/neo4j' import { getNeode } from '../../db/neo4j'
import fileUpload from './fileUpload' import fileUpload from './fileUpload'
import encryptPassword from '../../helpers/encryptPassword' import encryptPassword from '../../helpers/encryptPassword'
import generateNonce from './helpers/generateNonce' import generateNonce from './helpers/generateNonce'

View File

@ -1,6 +1,6 @@
import Factory from '../../seed/factories' import Factory from '../../factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { getDriver, getNeode } from '../../bootstrap/neo4j' import { getDriver, getNeode } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'

View File

@ -1,8 +1,8 @@
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import createServer from '../.././server' import createServer from '../.././server'
import Factory from '../../seed/factories' import Factory from '../../factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { getDriver, getNeode } from '../../bootstrap/neo4j' import { getDriver, getNeode } from '../../db/neo4j'
const factory = Factory() const factory = Factory()
const instance = getNeode() const instance = getNeode()

View File

@ -1,4 +1,4 @@
import { getNeode } from '../../bootstrap/neo4j' import { getNeode } from '../../db/neo4j'
import { UserInputError } from 'apollo-server' import { UserInputError } from 'apollo-server'
const neode = getNeode() const neode = getNeode()

View File

@ -1,7 +1,7 @@
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import Factory from '../../seed/factories' import Factory from '../../factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'
const factory = Factory() const factory = Factory()

View File

@ -20,7 +20,7 @@ export default {
AND NOT ( AND NOT (
author.deleted = true OR author.disabled = true author.deleted = true OR author.disabled = true
OR resource.deleted = true OR resource.disabled = true OR resource.deleted = true OR resource.disabled = true
OR (:User { id: $thisUserId })-[:BLOCKED]-(author) OR (:User {id: $thisUserId})-[:MUTED]->(author)
) )
WITH resource, author, WITH resource, author,
[(resource)<-[:COMMENTS]-(comment:Comment) | comment] as comments, [(resource)<-[:COMMENTS]-(comment:Comment) | comment] as comments,
@ -40,8 +40,7 @@ export default {
YIELD node as resource, score YIELD node as resource, score
MATCH (resource) MATCH (resource)
WHERE score >= 0.5 WHERE score >= 0.5
AND NOT (resource.deleted = true OR resource.disabled = true AND NOT (resource.deleted = true OR resource.disabled = true)
OR (:User { id: $thisUserId })-[:BLOCKED]-(resource))
RETURN resource {.*, __typename: labels(resource)[0]} RETURN resource {.*, __typename: labels(resource)[0]}
LIMIT $limit LIMIT $limit
` `

View File

@ -1,7 +1,7 @@
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import Factory from '../../seed/factories' import Factory from '../../factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'
let mutate, query, authenticatedUser, variables let mutate, query, authenticatedUser, variables

View File

@ -1,4 +1,4 @@
import { getNeode } from '../../bootstrap/neo4j' import { getNeode } from '../../db/neo4j'
import Resolver from './helpers/Resolver' import Resolver from './helpers/Resolver'
const neode = getNeode() const neode = getNeode()

View File

@ -1,8 +1,8 @@
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import createServer from '../../server' import createServer from '../../server'
import Factory from '../../seed/factories' import Factory from '../../factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
const driver = getDriver() const driver = getDriver()
const factory = Factory() const factory = Factory()

View File

@ -1,7 +1,7 @@
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import Factory from '../../seed/factories' import Factory from '../../factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'
let query, authenticatedUser let query, authenticatedUser

View File

@ -1,7 +1,7 @@
import encode from '../../jwt/encode' import encode from '../../jwt/encode'
import bcrypt from 'bcryptjs' import bcrypt from 'bcryptjs'
import { AuthenticationError } from 'apollo-server' import { AuthenticationError } from 'apollo-server'
import { getNeode } from '../../bootstrap/neo4j' import { getNeode } from '../../db/neo4j'
import normalizeEmail from './helpers/normalizeEmail' import normalizeEmail from './helpers/normalizeEmail'
import log from './helpers/databaseLogger' import log from './helpers/databaseLogger'

View File

@ -1,11 +1,11 @@
import jwt from 'jsonwebtoken' import jwt from 'jsonwebtoken'
import CONFIG from './../../config' import CONFIG from './../../config'
import Factory from '../../seed/factories' import Factory from '../../factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import createServer, { context } from '../../server' import createServer, { context } from '../../server'
import encode from '../../jwt/encode' import encode from '../../jwt/encode'
import { getNeode } from '../../bootstrap/neo4j' import { getNeode } from '../../db/neo4j'
const factory = Factory() const factory = Factory()
const neode = getNeode() const neode = getNeode()

View File

@ -1,6 +1,6 @@
import { neo4jgraphql } from 'neo4j-graphql-js' import { neo4jgraphql } from 'neo4j-graphql-js'
import fileUpload from './fileUpload' import fileUpload from './fileUpload'
import { getNeode } from '../../bootstrap/neo4j' import { getNeode } from '../../db/neo4j'
import { UserInputError, ForbiddenError } from 'apollo-server' import { UserInputError, ForbiddenError } from 'apollo-server'
import Resolver from './helpers/Resolver' import Resolver from './helpers/Resolver'
import log from './helpers/databaseLogger' import log from './helpers/databaseLogger'
@ -8,6 +8,21 @@ import createOrUpdateLocations from './users/location'
const neode = getNeode() const neode = getNeode()
export const getMutedUsers = async context => {
const { neode } = context
const userModel = neode.model('User')
let mutedUsers = neode
.query()
.match('user', userModel)
.where('user.id', context.user.id)
.relationship(userModel.relationships().get('muted'))
.to('muted', userModel)
.return('muted')
mutedUsers = await mutedUsers.execute()
mutedUsers = mutedUsers.records.map(r => r.get('muted').properties)
return mutedUsers
}
export const getBlockedUsers = async context => { export const getBlockedUsers = async context => {
const { neode } = context const { neode } = context
const userModel = neode.model('User') const userModel = neode.model('User')
@ -23,24 +38,15 @@ export const getBlockedUsers = async context => {
return blockedUsers return blockedUsers
} }
export const getBlockedByUsers = async context => {
if (context.user.role === 'moderator' || context.user.role === 'admin') return []
const { neode } = context
const userModel = neode.model('User')
let blockedByUsers = neode
.query()
.match('user', userModel)
.relationship(userModel.relationships().get('blocked'))
.to('blocked', userModel)
.where('blocked.id', context.user.id)
.return('user')
blockedByUsers = await blockedByUsers.execute()
blockedByUsers = blockedByUsers.records.map(r => r.get('user').properties)
return blockedByUsers
}
export default { export default {
Query: { Query: {
mutedUsers: async (object, args, context, resolveInfo) => {
try {
return getMutedUsers(context)
} catch (e) {
throw new UserInputError(e.message)
}
},
blockedUsers: async (object, args, context, resolveInfo) => { blockedUsers: async (object, args, context, resolveInfo) => {
try { try {
return getBlockedUsers(context) return getBlockedUsers(context)
@ -72,7 +78,37 @@ export default {
}, },
}, },
Mutation: { Mutation: {
block: async (object, args, context, resolveInfo) => { muteUser: async (_parent, params, context, _resolveInfo) => {
const { user: currentUser } = context
if (currentUser.id === params.id) return null
await neode.cypher(
`
MATCH(u:User {id: $currentUser.id})-[previousRelationship:FOLLOWS]->(b:User {id: $params.id})
DELETE previousRelationship
`,
{ currentUser, params },
)
const [user, mutedUser] = await Promise.all([
neode.find('User', currentUser.id),
neode.find('User', params.id),
])
await user.relateTo(mutedUser, 'muted')
return mutedUser.toJson()
},
unmuteUser: async (_parent, params, context, _resolveInfo) => {
const { user: currentUser } = context
if (currentUser.id === params.id) return null
await neode.cypher(
`
MATCH(u:User {id: $currentUser.id})-[previousRelationship:MUTED]->(b:User {id: $params.id})
DELETE previousRelationship
`,
{ currentUser, params },
)
const unmutedUser = await neode.find('User', params.id)
return unmutedUser.toJson()
},
blockUser: async (object, args, context, resolveInfo) => {
const { user: currentUser } = context const { user: currentUser } = context
if (currentUser.id === args.id) return null if (currentUser.id === args.id) return null
await neode.cypher( await neode.cypher(
@ -89,7 +125,7 @@ export default {
await user.relateTo(blockedUser, 'blocked') await user.relateTo(blockedUser, 'blocked')
return blockedUser.toJson() return blockedUser.toJson()
}, },
unblock: async (object, args, context, resolveInfo) => { unblockUser: async (object, args, context, resolveInfo) => {
const { user: currentUser } = context const { user: currentUser } = context
if (currentUser.id === args.id) return null if (currentUser.id === args.id) return null
await neode.cypher( await neode.cypher(
@ -215,8 +251,10 @@ export default {
boolean: { boolean: {
followedByCurrentUser: followedByCurrentUser:
'MATCH (this)<-[:FOLLOWS]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1', 'MATCH (this)<-[:FOLLOWS]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
isBlocked: blocked:
'MATCH (this)<-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1', 'MATCH (this)<-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
isMuted:
'MATCH (this)<-[:MUTED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
}, },
count: { count: {
contributionsCount: contributionsCount:

View File

@ -1,6 +1,6 @@
import Factory from '../../seed/factories' import Factory from '../../factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'

View File

@ -1,6 +1,6 @@
import { gql } from '../../../helpers/jest' import { gql } from '../../../helpers/jest'
import Factory from '../../../seed/factories' import Factory from '../../../factories'
import { getNeode, getDriver } from '../../../bootstrap/neo4j' import { getNeode, getDriver } from '../../../db/neo4j'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import createServer from '../../../server' import createServer from '../../../server'

View File

@ -1,15 +1,15 @@
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import createServer from '../../../server' import createServer from '../../../server'
import Factory from '../../../seed/factories' import Factory from '../../../factories'
import { gql } from '../../../helpers/jest' import { gql } from '../../../helpers/jest'
import { getNeode, getDriver } from '../../../bootstrap/neo4j' import { getNeode, getDriver } from '../../../db/neo4j'
const driver = getDriver() const driver = getDriver()
const factory = Factory() const factory = Factory()
const neode = getNeode() const neode = getNeode()
let currentUser let currentUser
let blockedUser let mutedUser
let authenticatedUser let authenticatedUser
let server let server
@ -33,15 +33,15 @@ afterEach(async () => {
await factory.cleanDatabase() await factory.cleanDatabase()
}) })
describe('blockedUsers', () => { describe('mutedUsers', () => {
let blockedUserQuery let mutedUserQuery
beforeEach(() => { beforeEach(() => {
blockedUserQuery = gql` mutedUserQuery = gql`
query { query {
blockedUsers { mutedUsers {
id id
name name
isBlocked isMuted
} }
} }
` `
@ -49,34 +49,34 @@ describe('blockedUsers', () => {
it('throws permission error', async () => { it('throws permission error', async () => {
const { query } = createTestClient(server) const { query } = createTestClient(server)
const result = await query({ query: blockedUserQuery }) const result = await query({ query: mutedUserQuery })
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!') expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
}) })
describe('authenticated and given a blocked user', () => { describe('authenticated and given a muted user', () => {
beforeEach(async () => { beforeEach(async () => {
currentUser = await neode.create('User', { currentUser = await neode.create('User', {
name: 'Current User', name: 'Current User',
id: 'u1', id: 'u1',
}) })
blockedUser = await neode.create('User', { mutedUser = await neode.create('User', {
name: 'Blocked User', name: 'Muted User',
id: 'u2', id: 'u2',
}) })
await currentUser.relateTo(blockedUser, 'blocked') await currentUser.relateTo(mutedUser, 'muted')
authenticatedUser = await currentUser.toJson() authenticatedUser = await currentUser.toJson()
}) })
it('returns a list of blocked users', async () => { it('returns a list of muted users', async () => {
const { query } = createTestClient(server) const { query } = createTestClient(server)
await expect(query({ query: blockedUserQuery })).resolves.toEqual( await expect(query({ query: mutedUserQuery })).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
data: { data: {
blockedUsers: [ mutedUsers: [
{ {
name: 'Blocked User', name: 'Muted User',
id: 'u2', id: 'u2',
isBlocked: true, isMuted: true,
}, },
], ],
}, },
@ -86,28 +86,28 @@ describe('blockedUsers', () => {
}) })
}) })
describe('block', () => { describe('muteUser', () => {
let blockAction let muteAction
beforeEach(() => { beforeEach(() => {
currentUser = undefined currentUser = undefined
blockAction = variables => { muteAction = variables => {
const { mutate } = createTestClient(server) const { mutate } = createTestClient(server)
const blockMutation = gql` const muteUserMutation = gql`
mutation($id: ID!) { mutation($id: ID!) {
block(id: $id) { muteUser(id: $id) {
id id
name name
isBlocked isMuted
} }
} }
` `
return mutate({ mutation: blockMutation, variables }) return mutate({ mutation: muteUserMutation, variables })
} }
}) })
it('throws permission error', async () => { it('throws permission error', async () => {
const result = await blockAction({ id: 'u2' }) const result = await muteAction({ id: 'u2' })
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!') expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
}) })
@ -120,45 +120,47 @@ describe('block', () => {
authenticatedUser = await currentUser.toJson() authenticatedUser = await currentUser.toJson()
}) })
describe('block yourself', () => { describe('mute yourself', () => {
it('returns null', async () => { it('returns null', async () => {
await expect(blockAction({ id: 'u1' })).resolves.toEqual( await expect(muteAction({ id: 'u1' })).resolves.toEqual(
expect.objectContaining({ data: { block: null } }), expect.objectContaining({ data: { muteUser: null } }),
) )
}) })
}) })
describe('block not existing user', () => { describe('mute not existing user', () => {
it('returns null', async () => { it('returns null', async () => {
await expect(blockAction({ id: 'u2' })).resolves.toEqual( await expect(muteAction({ id: 'u2' })).resolves.toEqual(
expect.objectContaining({ data: { block: null } }), expect.objectContaining({ data: { muteUser: null } }),
) )
}) })
}) })
describe('given a to-be-blocked user', () => { describe('given a to-be-muted user', () => {
beforeEach(async () => { beforeEach(async () => {
blockedUser = await neode.create('User', { mutedUser = await neode.create('User', {
name: 'Blocked User', name: 'Muted User',
id: 'u2', id: 'u2',
}) })
}) })
it('blocks a user', async () => { it('mutes a user', async () => {
await expect(blockAction({ id: 'u2' })).resolves.toEqual( await expect(muteAction({ id: 'u2' })).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
data: { block: { id: 'u2', name: 'Blocked User', isBlocked: true } }, data: {
muteUser: { id: 'u2', name: 'Muted User', isMuted: true },
},
}), }),
) )
}) })
it('unfollows the user', async () => { it('unfollows the user', async () => {
await currentUser.relateTo(blockedUser, 'following') await currentUser.relateTo(mutedUser, 'following')
const queryUser = gql` const queryUser = gql`
query { query {
User(id: "u2") { User(id: "u2") {
id id
isBlocked isMuted
followedByCurrentUser followedByCurrentUser
} }
} }
@ -166,18 +168,18 @@ describe('block', () => {
const { query } = createTestClient(server) const { query } = createTestClient(server)
await expect(query({ query: queryUser })).resolves.toEqual( await expect(query({ query: queryUser })).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
data: { User: [{ id: 'u2', isBlocked: false, followedByCurrentUser: true }] }, data: { User: [{ id: 'u2', isMuted: false, followedByCurrentUser: true }] },
}), }),
) )
await blockAction({ id: 'u2' }) await muteAction({ id: 'u2' })
await expect(query({ query: queryUser })).resolves.toEqual( await expect(query({ query: queryUser })).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
data: { User: [{ id: 'u2', isBlocked: true, followedByCurrentUser: false }] }, data: { User: [{ id: 'u2', isMuted: true, followedByCurrentUser: false }] },
}), }),
) )
}) })
describe('given both the current user and the to-be-blocked user write a post', () => { describe('given both the current user and the to-be-muted user write a post', () => {
let postQuery let postQuery
beforeEach(async () => { beforeEach(async () => {
@ -187,11 +189,11 @@ describe('block', () => {
}) })
const post2 = await neode.create('Post', { const post2 = await neode.create('Post', {
id: 'p23', id: 'p23',
title: 'A post written by the blocked user', title: 'A post written by the muted user',
}) })
await Promise.all([ await Promise.all([
post1.relateTo(currentUser, 'author'), post1.relateTo(currentUser, 'author'),
post2.relateTo(blockedUser, 'author'), post2.relateTo(mutedUser, 'author'),
]) ])
postQuery = gql` postQuery = gql`
query { query {
@ -223,9 +225,9 @@ describe('block', () => {
}, },
{ {
id: 'p23', id: 'p23',
title: 'A post written by the blocked user', title: 'A post written by the muted user',
author: { author: {
name: 'Blocked User', name: 'Muted User',
id: 'u2', id: 'u2',
}, },
}, },
@ -238,12 +240,12 @@ describe('block', () => {
describe('from the perspective of the current user', () => { describe('from the perspective of the current user', () => {
it('both posts are in the newsfeed', bothPostsAreInTheNewsfeed) it('both posts are in the newsfeed', bothPostsAreInTheNewsfeed)
describe('but if the current user blocks the other user', () => { describe('but if the current user mutes the other user', () => {
beforeEach(async () => { beforeEach(async () => {
await currentUser.relateTo(blockedUser, 'blocked') await currentUser.relateTo(mutedUser, 'muted')
}) })
it("the blocked user's post won't show up in the newsfeed of the current user", async () => { it("the muted user's post won't show up in the newsfeed of the current user", async () => {
const { query } = createTestClient(server) const { query } = createTestClient(server)
await expect(query({ query: postQuery })).resolves.toEqual( await expect(query({ query: postQuery })).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
@ -262,29 +264,34 @@ describe('block', () => {
}) })
}) })
describe('from the perspective of the blocked user', () => { describe('from the perspective of the muted user', () => {
beforeEach(async () => { beforeEach(async () => {
authenticatedUser = await blockedUser.toJson() authenticatedUser = await mutedUser.toJson()
}) })
it('both posts are in the newsfeed', bothPostsAreInTheNewsfeed) it('both posts are in the newsfeed', bothPostsAreInTheNewsfeed)
describe('but if the current user blocks the other user', () => { describe('but if the current user mutes the other user', () => {
beforeEach(async () => { beforeEach(async () => {
await currentUser.relateTo(blockedUser, 'blocked') await currentUser.relateTo(mutedUser, 'muted')
}) })
it("the current user's post won't show up in the newsfeed of the blocked user", async () => { it("the current user's post will show up in the newsfeed of the muted user", async () => {
const { query } = createTestClient(server) const { query } = createTestClient(server)
await expect(query({ query: postQuery })).resolves.toEqual( await expect(query({ query: postQuery })).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
data: { data: {
Post: [ Post: expect.arrayContaining([
{ {
id: 'p23', id: 'p23',
title: 'A post written by the blocked user', title: 'A post written by the muted user',
author: { name: 'Blocked User', id: 'u2' }, author: { name: 'Muted User', id: 'u2' },
}, },
], {
id: 'p12',
title: 'A post written by the current user',
author: { name: 'Current User', id: 'u1' },
},
]),
}, },
}), }),
) )
@ -296,28 +303,28 @@ describe('block', () => {
}) })
}) })
describe('unblock', () => { describe('unmuteUser', () => {
let unblockAction let unmuteAction
beforeEach(() => { beforeEach(() => {
currentUser = undefined currentUser = undefined
unblockAction = variables => { unmuteAction = variables => {
const { mutate } = createTestClient(server) const { mutate } = createTestClient(server)
const unblockMutation = gql` const unmuteUserMutation = gql`
mutation($id: ID!) { mutation($id: ID!) {
unblock(id: $id) { unmuteUser(id: $id) {
id id
name name
isBlocked isMuted
} }
} }
` `
return mutate({ mutation: unblockMutation, variables }) return mutate({ mutation: unmuteUserMutation, variables })
} }
}) })
it('throws permission error', async () => { it('throws permission error', async () => {
const result = await unblockAction({ id: 'u2' }) const result = await unmuteAction({ id: 'u2' })
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!') expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
}) })
@ -330,59 +337,69 @@ describe('unblock', () => {
authenticatedUser = await currentUser.toJson() authenticatedUser = await currentUser.toJson()
}) })
describe('unblock yourself', () => { describe('unmute yourself', () => {
it('returns null', async () => { it('returns null', async () => {
await expect(unblockAction({ id: 'u1' })).resolves.toEqual( await expect(unmuteAction({ id: 'u1' })).resolves.toEqual(
expect.objectContaining({ data: { unblock: null } }), expect.objectContaining({ data: { unmuteUser: null } }),
) )
}) })
}) })
describe('unblock not-existing user', () => { describe('unmute not-existing user', () => {
it('returns null', async () => { it('returns null', async () => {
await expect(unblockAction({ id: 'lksjdflksfdj' })).resolves.toEqual( await expect(unmuteAction({ id: 'lksjdflksfdj' })).resolves.toEqual(
expect.objectContaining({ data: { unblock: null } }), expect.objectContaining({ data: { unmuteUser: null } }),
) )
}) })
}) })
describe('given another user', () => { describe('given another user', () => {
beforeEach(async () => { beforeEach(async () => {
blockedUser = await neode.create('User', { mutedUser = await neode.create('User', {
name: 'Blocked User', name: 'Muted User',
id: 'u2', id: 'u2',
}) })
}) })
describe('unblocking a not yet blocked user', () => { describe('unmuting a not yet muted user', () => {
it('does not hurt', async () => { it('does not hurt', async () => {
await expect(unblockAction({ id: 'u2' })).resolves.toEqual( await expect(unmuteAction({ id: 'u2' })).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
data: { unblock: { id: 'u2', name: 'Blocked User', isBlocked: false } }, data: {
unmuteUser: { id: 'u2', name: 'Muted User', isMuted: false },
},
}), }),
) )
}) })
}) })
describe('given a blocked user', () => { describe('given a muted user', () => {
beforeEach(async () => { beforeEach(async () => {
await currentUser.relateTo(blockedUser, 'blocked') await currentUser.relateTo(mutedUser, 'muted')
}) })
it('unblocks a user', async () => { it('unmutes a user', async () => {
await expect(unblockAction({ id: 'u2' })).resolves.toEqual( await expect(unmuteAction({ id: 'u2' })).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
data: { unblock: { id: 'u2', name: 'Blocked User', isBlocked: false } }, data: {
unmuteUser: { id: 'u2', name: 'Muted User', isMuted: false },
},
}), }),
) )
}) })
describe('unblocking twice', () => { describe('unmuting twice', () => {
it('has no effect', async () => { it('has no effect', async () => {
await unblockAction({ id: 'u2' }) await unmuteAction({ id: 'u2' })
await expect(unblockAction({ id: 'u2' })).resolves.toEqual( await expect(unmuteAction({ id: 'u2' })).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
data: { unblock: { id: 'u2', name: 'Blocked User', isBlocked: false } }, data: {
unmuteUser: {
id: 'u2',
name: 'Muted User',
isMuted: false,
},
},
}), }),
) )
}) })

View File

@ -1,30 +1,5 @@
import fs from 'fs'
import path from 'path' import path from 'path'
import { mergeTypes } from 'merge-graphql-schemas' import { mergeTypes, fileLoader } from 'merge-graphql-schemas'
const findGqlFiles = dir => {
var results = []
var list = fs.readdirSync(dir)
list.forEach(file => {
file = path.join(dir, file).toString('utf-8')
var stat = fs.statSync(file)
if (stat && stat.isDirectory()) {
// Recurse into a subdirectory
results = results.concat(findGqlFiles(file))
} else {
if (path.extname(file) === '.gql') {
// Is a gql file
results.push(file)
}
}
})
return results
}
const typeDefs = []
findGqlFiles(__dirname).forEach(file => {
typeDefs.push(fs.readFileSync(file).toString('utf-8'))
})
const typeDefs = fileLoader(path.join(__dirname, './**/*.gql'))
export default mergeTypes(typeDefs, { all: true }) export default mergeTypes(typeDefs, { all: true })

View File

@ -68,10 +68,18 @@ type User {
RETURN COUNT(u) >= 1 RETURN COUNT(u) >= 1
""" """
) )
isBlocked: Boolean! @cypher(
blocked: Boolean! @cypher(
statement: """ statement: """
MATCH (this)<-[: BLOCKED]-(u: User { id: $cypherParams.currentUserId}) MATCH (this)-[:BLOCKED]-(user:User {id: $cypherParams.currentUserId})
RETURN COUNT(u) >= 1 RETURN COUNT(user) >= 1
"""
)
isMuted: Boolean! @cypher(
statement: """
MATCH (this)<-[:MUTED]-(user:User { id: $cypherParams.currentUserId})
RETURN COUNT(user) >= 1
""" """
) )
@ -160,6 +168,7 @@ type Query {
filter: _UserFilter filter: _UserFilter
): [User] ): [User]
mutedUsers: [User]
blockedUsers: [User] blockedUsers: [User]
isLoggedIn: Boolean! isLoggedIn: Boolean!
currentUser: User currentUser: User
@ -197,7 +206,8 @@ type Mutation {
DeleteUser(id: ID!, resource: [Deletable]): User DeleteUser(id: ID!, resource: [Deletable]): User
muteUser(id: ID!): User
block(id: ID!): User unmuteUser(id: ID!): User
unblock(id: ID!): User blockUser(id: ID!): User
unblockUser(id: ID!): User
} }

View File

@ -1,134 +0,0 @@
const _ = require('lodash')
const faker = require('faker')
const unsplashTopics = [
'love',
'family',
'spring',
'business',
'nature',
'travel',
'happy',
'landscape',
'health',
'friends',
'computer',
'autumn',
'space',
'animal',
'smile',
'face',
'people',
'portrait',
'amazing',
]
let unsplashTopicsTmp = []
const ngoLogos = [
'http://www.fetchlogos.com/wp-content/uploads/2015/11/Girl-Scouts-Of-The-Usa-Logo.jpg',
'http://logos.textgiraffe.com/logos/logo-name/Ngo-designstyle-friday-m.png',
'http://seeklogo.com/images/N/ngo-logo-BD53A3E024-seeklogo.com.png',
'https://dcassetcdn.com/design_img/10133/25833/25833_303600_10133_image.jpg',
'https://cdn.tutsplus.com/vector/uploads/legacy/articles/08bad_ngologos/20.jpg',
'https://cdn.tutsplus.com/vector/uploads/legacy/articles/08bad_ngologos/33.jpg',
null,
]
const difficulties = ['easy', 'medium', 'hard']
export default {
randomItem: (items, filter) => {
const ids = filter
? Object.keys(items).filter(id => {
return filter(items[id])
})
: _.keys(items)
const randomIds = _.shuffle(ids)
return items[randomIds.pop()]
},
randomItems: (items, key = 'id', min = 1, max = 1) => {
const randomIds = _.shuffle(_.keys(items))
const res = []
const count = _.random(min, max)
for (let i = 0; i < count; i++) {
let r = items[randomIds.pop()][key]
if (key === 'id') {
r = r.toString()
}
res.push(r)
}
return res
},
random: items => {
return _.shuffle(items).pop()
},
randomDifficulty: () => {
return _.shuffle(difficulties).pop()
},
randomLogo: () => {
return _.shuffle(ngoLogos).pop()
},
randomUnsplashUrl: () => {
if (Math.random() < 0.6) {
// do not attach images in 60 percent of the cases (faster seeding)
return
}
if (unsplashTopicsTmp.length < 2) {
unsplashTopicsTmp = _.shuffle(unsplashTopics)
}
return (
'https://source.unsplash.com/daily?' + unsplashTopicsTmp.pop() + ',' + unsplashTopicsTmp.pop()
)
},
randomCategories: (seederstore, allowEmpty = false) => {
let count = Math.round(Math.random() * 3)
if (allowEmpty === false && count === 0) {
count = 1
}
const categorieIds = _.shuffle(_.keys(seederstore.categories))
const ids = []
for (let i = 0; i < count; i++) {
ids.push(categorieIds.pop())
}
return ids
},
randomAddresses: () => {
const count = Math.round(Math.random() * 3)
const addresses = []
for (let i = 0; i < count; i++) {
addresses.push({
city: faker.address.city(),
zipCode: faker.address.zipCode(),
street: faker.address.streetAddress(),
country: faker.address.countryCode(),
lat: 54.032726 - Math.random() * 10,
lng: 6.558838 + Math.random() * 10,
})
}
return addresses
},
/**
* Get array of ids from the given seederstore items after mapping them by the key in the values
*
* @param items items from the seederstore
* @param values values for which you need the ids
* @param key the field key that is represented in the values (slug, name, etc.)
*/
mapIdsByKey: (items, values, key) => {
const res = []
values.forEach(value => {
res.push(_.find(items, [key, value]).id.toString())
})
return res
},
genInviteCode: () => {
const chars = '23456789abcdefghkmnpqrstuvwxyzABCDEFGHJKLMNPRSTUVWXYZ'
let code = ''
for (let i = 0; i < 8; i++) {
const n = _.random(0, chars.length - 1)
code += chars.substr(n, 1)
}
return code
},
}

View File

@ -1,21 +1,13 @@
import express from 'express' import express from 'express'
import helmet from 'helmet' import helmet from 'helmet'
import { ApolloServer } from 'apollo-server-express' import { ApolloServer } from 'apollo-server-express'
import CONFIG, { requiredConfigs } from './config' import CONFIG from './config'
import middleware from './middleware' import middleware from './middleware'
import { getNeode, getDriver } from './bootstrap/neo4j' import { getNeode, getDriver } from './db/neo4j'
import decode from './jwt/decode' import decode from './jwt/decode'
import schema from './schema' import schema from './schema'
import webfinger from './activitypub/routes/webfinger' import webfinger from './activitypub/routes/webfinger'
// check required configs and throw error
// TODO check this directly in config file - currently not possible due to testsetup
Object.entries(requiredConfigs).map(entry => {
if (!entry[1]) {
throw new Error(`ERROR: "${entry[0]}" env variable is missing.`)
}
})
const driver = getDriver() const driver = getDriver()
const neode = getNeode() const neode = getNeode()

View File

@ -3,7 +3,7 @@ import { Given, When, Then, AfterAll } from 'cucumber'
import { expect } from 'chai' import { expect } from 'chai'
// import { client } from '../../../src/activitypub/apollo-client' // import { client } from '../../../src/activitypub/apollo-client'
import { GraphQLClient } from 'graphql-request' import { GraphQLClient } from 'graphql-request'
import Factory from '../../../src/seed/factories' import Factory from '../../../src/factories'
const debug = require('debug')('ea:test:steps') const debug = require('debug')('ea:test:steps')
const factory = Factory() const factory = Factory()

File diff suppressed because it is too large Load Diff

View File

@ -249,10 +249,12 @@ Shows automatically related actions for existing post.
### Administration ### Administration
[Cucumber Features](https://github.com/Human-Connection/Human-Connection/tree/master/cypress/integration/administration)
* Provide Admin-Interface to send Users Invite Code * Provide Admin-Interface to send Users Invite Code
* Static Pages for Data Privacy Statement ... * Static Pages for Data Privacy Statement ...
* Create, edit and delete Announcements * Create, edit and delete Announcements
* Show Announcements on top of User Interface * Pin a post to inform users
### Invitation ### Invitation

View File

@ -0,0 +1,36 @@
Feature: Pin a post
As an admin
I want to pin a post so that it always appears at the top
In order to make sure all network users read it - e.g. notify people about security incidents, maintenance downtimes
Background:
Given we have the following posts in our database:
| id | title | pinned | createdAt |
| p1 | Some other post | | 2020-01-21 |
| p2 | Houston we have a problem | x | 2020-01-20 |
| p3 | Yet another post | | 2020-01-19 |
Scenario: Pinned post always appears on the top of the newsfeed
Given I am logged in with a "user" role
Then the first post on the landing page has the title:
"""
Houston we have a problem
"""
And the post with title "Houston we have a problem" has a ribbon for pinned posts
Scenario: Ordinary users cannot pin a post
Given I am logged in with a "user" role
When I open the content menu of post "Yet another post"
Then there is no button to pin a post
Scenario: Admins are allowed to pin a post
Given I am logged in with a "admin" role
And I open the content menu of post "Yet another post"
When I click on 'Pin post'
Then I see a toaster with "Post pinned successfully"
And the first post on the landing page has the title:
"""
Yet another post
"""
And the post with title "Yet another post" has a ribbon for pinned posts

View File

@ -17,17 +17,22 @@ Then("I click on the {string} button", text => {
.click(); .click();
}); });
Then("I click on the reply button", () => {
cy.get(".reply-button")
.click();
});
Then("my comment should be successfully created", () => { Then("my comment should be successfully created", () => {
cy.get(".iziToast-message").contains("Comment Submitted"); cy.get(".iziToast-message").contains("Comment submitted!");
}); });
Then("I should see my comment", () => { Then("I should see my comment", () => {
cy.get("div.comment p") cy.get("div.comment p")
.should("contain", "Human Connection rocks") .should("contain", "Human Connection rocks")
.get(".ds-avatar img") .get(".user-avatar img")
.should("have.attr", "src") .should("have.attr", "src")
.and("contain", narratorAvatar) .and("contain", narratorAvatar)
.get("div p.ds-text span") .get(".user-teaser > .info > .text")
.should("contain", "today at"); .should("contain", "today at");
}); });
@ -44,3 +49,37 @@ Then("I should see an abreviated version of my comment", () => {
Then("the editor should be cleared", () => { Then("the editor should be cleared", () => {
cy.get(".ProseMirror p").should("have.class", "is-empty"); cy.get(".ProseMirror p").should("have.class", "is-empty");
}); });
Then("it should create a mention in the CommentForm", () => {
cy.get(".ProseMirror a")
.should('have.class', 'mention')
.should('contain', '@peter-pan')
})
When("I open the content menu of post {string}", (title)=> {
cy.contains('.post-card', title)
.find('.content-menu .base-button')
.click()
})
When("I click on 'Pin post'", (string)=> {
cy.get("a.ds-menu-item-link").contains("Pin post")
.click()
})
Then("there is no button to pin a post", () => {
cy.get("a.ds-menu-item-link")
.should('contain', "Report Post") // sanity check
.should('not.contain', "Pin post")
})
And("the post with title {string} has a ribbon for pinned posts", (title) => {
cy.get("article.post-card").contains(title)
.parent()
.find("div.ribbon.ribbon--pinned")
.should("contain", "Announcement")
})
Then("I see a toaster with {string}", (title) => {
cy.get(".iziToast-message").should("contain", title);
})

View File

@ -32,5 +32,5 @@ Then("I cannot upload a picture", () => {
cy.get(".ds-card-content") cy.get(".ds-card-content")
.children() .children()
.should("not.have.id", "customdropzone") .should("not.have.id", "customdropzone")
.should("have.class", "ds-avatar"); .should("have.class", "user-avatar");
}); });

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