Merge branch 'master' into migrate-styleguide-buttons

This commit is contained in:
Alina Beck 2019-12-18 12:59:23 +05:30
commit c83f158ca4
115 changed files with 3272 additions and 2236 deletions

View File

@ -12,7 +12,7 @@ install:
- yarn global add wait-on - yarn global add wait-on
# Install Codecov # Install Codecov
- yarn install - yarn install
- cp cypress.env.template.json cypress.env.json - cp backend/.env.template backend/.env
before_script: before_script:
- docker-compose -f docker-compose.yml build --parallel - docker-compose -f docker-compose.yml build --parallel
@ -63,14 +63,14 @@ before_deploy:
deploy: deploy:
- provider: script - provider: script
script: scripts/docker_push.sh script: bash scripts/docker_push.sh
on: on:
branch: master branch: master
- provider: script - provider: script
script: scripts/deploy.sh script: bash scripts/deploy.sh
on: on:
branch: master branch: master
- provider: script - provider: script
script: scripts/github_release.sh script: bash scripts/github_release.sh
on: on:
branch: master branch: master

View File

@ -4,10 +4,131 @@ 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.1.11](https://github.com/Human-Connection/Human-Connection/compare/v0.1.10...v0.1.11) #### [v0.1.13](https://github.com/Human-Connection/Human-Connection/compare/v0.1.12...v0.1.13)
> 25 November 2019 > 13 December 2019
- Update de.json [`#2492`](https://github.com/Human-Connection/Human-Connection/pull/2492)
- Fix broken scroll behaviour on index and profile page [`#2487`](https://github.com/Human-Connection/Human-Connection/pull/2487)
- Lokalise: Translations update [`#2503`](https://github.com/Human-Connection/Human-Connection/pull/2503)
- build(deps): bump node from 13.1.0-alpine to 13.3.0-alpine in /webapp [`#2454`](https://github.com/Human-Connection/Human-Connection/pull/2454)
- Lokalise: Translations update [`#2485`](https://github.com/Human-Connection/Human-Connection/pull/2485)
- build(deps-dev): bump css-loader from 3.3.0 to 3.3.2 in /webapp [`#2505`](https://github.com/Human-Connection/Human-Connection/pull/2505)
- build(deps-dev): bump cypress from 3.7.0 to 3.8.0 [`#2504`](https://github.com/Human-Connection/Human-Connection/pull/2504)
- Favor transaction functions [`#2433`](https://github.com/Human-Connection/Human-Connection/pull/2433)
- build(deps): bump nodemailer from 6.4.1 to 6.4.2 in /backend [`#2500`](https://github.com/Human-Connection/Human-Connection/pull/2500)
- Update en.json [`#2491`](https://github.com/Human-Connection/Human-Connection/pull/2491)
- Update es.json [`#2493`](https://github.com/Human-Connection/Human-Connection/pull/2493)
- Update fr.json [`#2494`](https://github.com/Human-Connection/Human-Connection/pull/2494)
- Update it.json [`#2496`](https://github.com/Human-Connection/Human-Connection/pull/2496)
- build(deps-dev): bump nodemon from 2.0.1 to 2.0.2 in /backend [`#2499`](https://github.com/Human-Connection/Human-Connection/pull/2499)
- build(deps): bump @nuxtjs/apollo from 4.0.0-rc18 to 4.0.0-rc19 in /webapp [`#2498`](https://github.com/Human-Connection/Human-Connection/pull/2498)
- build(deps): bump neo4j-graphql-js from 2.10.0 to 2.10.1 in /backend [`#2497`](https://github.com/Human-Connection/Human-Connection/pull/2497)
- Fix docker manifest on Travis CI [`#2488`](https://github.com/Human-Connection/Human-Connection/pull/2488)
- build(deps-dev): bump @babel/core from 7.7.4 to 7.7.5 [`#2453`](https://github.com/Human-Connection/Human-Connection/pull/2453)
- build(deps-dev): bump cypress-file-upload from 3.5.0 to 3.5.1 [`#2489`](https://github.com/Human-Connection/Human-Connection/pull/2489)
- build(deps): bump cookie-universal-nuxt from 2.0.19 to 2.1.0 in /webapp [`#2490`](https://github.com/Human-Connection/Human-Connection/pull/2490)
- Update to version 0.1.12 [`#2483`](https://github.com/Human-Connection/Human-Connection/pull/2483)
- Lokalise: update of locale/ru.json [`60b3035`](https://github.com/Human-Connection/Human-Connection/commit/60b3035a3d475cb481130c6fe94f2901711a4053)
- Write test/refactor tests/resolvers/middleware [`d375ebe`](https://github.com/Human-Connection/Human-Connection/commit/d375ebe7d90e3251b17f59ffba8fb1470923ebe8)
- Fix this annoying bug with a tested helper [`e24d803`](https://github.com/Human-Connection/Human-Connection/commit/e24d8035b13040dc29f5f9cb033de8c1a401ac34)
#### [v0.1.12](https://github.com/Human-Connection/Human-Connection/compare/v0.1.10...v0.1.12)
> 10 December 2019
- Show the comments again [`#2482`](https://github.com/Human-Connection/Human-Connection/pull/2482)
- Improve notification query performance by reducing db calls [`#2470`](https://github.com/Human-Connection/Human-Connection/pull/2470)
- Fix `Cannot read 'Post' of undefined` [`#2481`](https://github.com/Human-Connection/Human-Connection/pull/2481)
- Hope to fix our deployment with explicit call of `bash` [`#2480`](https://github.com/Human-Connection/Human-Connection/pull/2480)
- Revert layout changes image aspect ratio [`#2467`](https://github.com/Human-Connection/Human-Connection/pull/2467)
- Quick fix for null pointer error in User.vue [`#2472`](https://github.com/Human-Connection/Human-Connection/pull/2472)
- Checkbox 'no comercial + no political account' add to creat user account [`#2416`](https://github.com/Human-Connection/Human-Connection/pull/2416)
- Remove data-test attriubutes in non-dev env [`#2421`](https://github.com/Human-Connection/Human-Connection/pull/2421)
- build(deps-dev): bump @babel/preset-env from 7.7.4 to 7.7.6 [`#2452`](https://github.com/Human-Connection/Human-Connection/pull/2452)
- build(deps-dev): bump @babel/preset-env from 7.7.4 to 7.7.6 in /backend [`#2455`](https://github.com/Human-Connection/Human-Connection/pull/2455)
- build(deps-dev): bump @babel/cli from 7.7.4 to 7.7.5 in /backend [`#2458`](https://github.com/Human-Connection/Human-Connection/pull/2458)
- build(deps): bump @sentry/node from 5.10.1 to 5.10.2 in /backend [`#2473`](https://github.com/Human-Connection/Human-Connection/pull/2473)
- build(deps-dev): bump eslint-plugin-import from 2.18.2 to 2.19.1 in /backend [`#2474`](https://github.com/Human-Connection/Human-Connection/pull/2474)
- build(deps-dev): bump css-loader from 3.2.1 to 3.3.0 in /webapp [`#2475`](https://github.com/Human-Connection/Human-Connection/pull/2475)
- build(deps-dev): bump eslint-plugin-import from 2.18.2 to 2.19.1 in /webapp [`#2477`](https://github.com/Human-Connection/Human-Connection/pull/2477)
- Fix #2237, Comments 4 times as long before "show more" [`#2443`](https://github.com/Human-Connection/Human-Connection/pull/2443)
- Get rid of inconsistency with neode setup [`#2404`](https://github.com/Human-Connection/Human-Connection/pull/2404)
- Bump styleguide to version 0.5.22 [`#2468`](https://github.com/Human-Connection/Human-Connection/pull/2468)
- build(deps): bump nodemailer from 6.4.0 to 6.4.1 in /backend [`#2456`](https://github.com/Human-Connection/Human-Connection/pull/2456)
- build(deps-dev): bump eslint-loader from 3.0.2 to 3.0.3 in /webapp [`#2459`](https://github.com/Human-Connection/Human-Connection/pull/2459)
- build(deps-dev): bump @babel/core from 7.7.4 to 7.7.5 in /backend [`#2460`](https://github.com/Human-Connection/Human-Connection/pull/2460)
- build(deps-dev): bump @babel/core from 7.7.4 to 7.7.5 in /webapp [`#2461`](https://github.com/Human-Connection/Human-Connection/pull/2461)
- build(deps-dev): bump @babel/preset-env from 7.7.4 to 7.7.6 in /webapp [`#2463`](https://github.com/Human-Connection/Human-Connection/pull/2463)
- build(deps-dev): bump async-validator from 3.2.2 to 3.2.3 in /webapp [`#2464`](https://github.com/Human-Connection/Human-Connection/pull/2464)
- build(deps): bump styleguide from `808b3c5` to `7ef8340` [`#2465`](https://github.com/Human-Connection/Human-Connection/pull/2465)
- Paginate moderations page without losing filtering [`#2466`](https://github.com/Human-Connection/Human-Connection/pull/2466)
- Update it.json [`#2451`](https://github.com/Human-Connection/Human-Connection/pull/2451)
- build(deps): bump metascraper from 5.8.8 to 5.8.9 in /backend [`#2304`](https://github.com/Human-Connection/Human-Connection/pull/2304)
- build(deps): bump metascraper-video from 5.8.7 to 5.8.9 in /backend [`#2303`](https://github.com/Human-Connection/Human-Connection/pull/2303)
- build(deps): bump neo4j-graphql-js from 2.9.3 to 2.10.0 in /backend [`#2440`](https://github.com/Human-Connection/Human-Connection/pull/2440)
- Hide Donations Bar [`#2422`](https://github.com/Human-Connection/Human-Connection/pull/2422)
- build(deps): bump @sentry/node from 5.10.0 to 5.10.1 in /backend [`#2436`](https://github.com/Human-Connection/Human-Connection/pull/2436)
- build(deps-dev): bump cypress-cucumber-preprocessor from 1.17.0 to 1.18.0 [`#2437`](https://github.com/Human-Connection/Human-Connection/pull/2437)
- build(deps-dev): bump apollo-server-testing from 2.9.12 to 2.9.13 in /backend [`#2439`](https://github.com/Human-Connection/Human-Connection/pull/2439)
- build(deps): bump apollo-server from 2.9.12 to 2.9.13 in /backend [`#2441`](https://github.com/Human-Connection/Human-Connection/pull/2441)
- Eliminate database calls for reports query [`#2435`](https://github.com/Human-Connection/Human-Connection/pull/2435)
- Use babel-loader with vue-svg-loader [`#2430`](https://github.com/Human-Connection/Human-Connection/pull/2430)
- Remove disable from reports.disable Query [`#2432`](https://github.com/Human-Connection/Human-Connection/pull/2432)
- 2253 fix scroll layout issue [`#2317`](https://github.com/Human-Connection/Human-Connection/pull/2317)
- Update test description [`#2424`](https://github.com/Human-Connection/Human-Connection/pull/2424)
- Update yarn.lock after pulling in latest changes [`#2419`](https://github.com/Human-Connection/Human-Connection/pull/2419)
- Update privacy path [`#2417`](https://github.com/Human-Connection/Human-Connection/pull/2417)
- Add browserstack logo to attributions [`#2431`](https://github.com/Human-Connection/Human-Connection/pull/2431)
- build(deps): bump @sentry/node from 5.9.0 to 5.10.0 in /backend [`#2428`](https://github.com/Human-Connection/Human-Connection/pull/2428)
- build(deps): bump nodemailer from 6.3.1 to 6.4.0 in /backend [`#2427`](https://github.com/Human-Connection/Human-Connection/pull/2427)
- List and protocol moderation [`#1954`](https://github.com/Human-Connection/Human-Connection/pull/1954)
- fix: Re-enable webfinger feature [`#2335`](https://github.com/Human-Connection/Human-Connection/pull/2335)
- Close neo4j driver sessions [`#2402`](https://github.com/Human-Connection/Human-Connection/pull/2402)
- feat: swap user name<=>handle for discriminability [`#2385`](https://github.com/Human-Connection/Human-Connection/pull/2385)
- build(deps-dev): bump @storybook/vue from 5.2.6 to 5.2.8 in /webapp [`#2397`](https://github.com/Human-Connection/Human-Connection/pull/2397)
- build(deps-dev): bump @storybook/addon-actions from 5.2.6 to 5.2.8 in /webapp [`#2398`](https://github.com/Human-Connection/Human-Connection/pull/2398)
- Fix German translation of "Shouts" [`#2400`](https://github.com/Human-Connection/Human-Connection/pull/2400)
- build(deps): bump tiptap-extensions from 1.28.4 to 1.28.5 in /webapp [`#2407`](https://github.com/Human-Connection/Human-Connection/pull/2407)
- build(deps-dev): bump @storybook/addon-a11y from 5.2.7 to 5.2.8 in /webapp [`#2406`](https://github.com/Human-Connection/Human-Connection/pull/2406)
- build(deps-dev): bump css-loader from 3.2.0 to 3.2.1 in /webapp [`#2405`](https://github.com/Human-Connection/Human-Connection/pull/2405)
- build(deps-dev): bump @storybook/addon-notes from 5.2.6 to 5.2.8 in /webapp [`#2399`](https://github.com/Human-Connection/Human-Connection/pull/2399)
- build(deps-dev): bump eslint from 6.7.1 to 6.7.2 in /webapp [`#2393`](https://github.com/Human-Connection/Human-Connection/pull/2393)
- build(deps-dev): bump @vue/cli-shared-utils from 4.0.5 to 4.1.1 in /webapp [`#2374`](https://github.com/Human-Connection/Human-Connection/pull/2374)
- build(deps-dev): bump eslint-plugin-jest from 23.0.5 to 23.1.1 in /webapp [`#2392`](https://github.com/Human-Connection/Human-Connection/pull/2392)
- Terms of use extended with dot - no commercial use [`#2316`](https://github.com/Human-Connection/Human-Connection/pull/2316)
- build(deps-dev): bump cypress-cucumber-preprocessor from 1.16.2 to 1.17.0 [`#2389`](https://github.com/Human-Connection/Human-Connection/pull/2389)
- Lokalise: Translations update [`#2380`](https://github.com/Human-Connection/Human-Connection/pull/2380)
- build(deps-dev): bump @storybook/addon-a11y from 5.2.6 to 5.2.7 in /webapp [`#2391`](https://github.com/Human-Connection/Human-Connection/pull/2391)
- build(deps-dev): bump eslint-plugin-jest from 23.0.5 to 23.1.1 in /backend [`#2390`](https://github.com/Human-Connection/Human-Connection/pull/2390)
- build(deps-dev): bump eslint from 6.7.1 to 6.7.2 in /backend [`#2388`](https://github.com/Human-Connection/Human-Connection/pull/2388)
- build(deps-dev): bump @vue/server-test-utils from 1.0.0-beta.29 to 1.0.0-beta.30 in /webapp [`#2379`](https://github.com/Human-Connection/Human-Connection/pull/2379)
- build(deps): bump neo4j from 3.5.12-enterprise to 3.5.13-enterprise in /neo4j [`#2377`](https://github.com/Human-Connection/Human-Connection/pull/2377)
- build(deps-dev): bump @babel/cli from 7.7.0 to 7.7.4 in /backend [`#2366`](https://github.com/Human-Connection/Human-Connection/pull/2366)
- build(deps-dev): bump cypress-plugin-retries from 1.4.0 to 1.5.0 [`#2360`](https://github.com/Human-Connection/Human-Connection/pull/2360)
- No public registration in development so that backend test pass [`#2382`](https://github.com/Human-Connection/Human-Connection/pull/2382)
- Don't remove sub-addresses in emails [`#2375`](https://github.com/Human-Connection/Human-Connection/pull/2375)
- refactor: Remove obsolete code about invitation codes [`#2333`](https://github.com/Human-Connection/Human-Connection/pull/2333)
- build(deps): bump @nuxtjs/apollo from 4.0.0-rc17 to 4.0.0-rc18 in /webapp [`#2373`](https://github.com/Human-Connection/Human-Connection/pull/2373)
- build(deps): bump graphql-shield from 7.0.2 to 7.0.4 in /backend [`#2372`](https://github.com/Human-Connection/Human-Connection/pull/2372)
- build(deps-dev): bump cypress from 3.6.1 to 3.7.0 [`#2371`](https://github.com/Human-Connection/Human-Connection/pull/2371)
- build(deps-dev): bump @babel/core from 7.7.2 to 7.7.4 in /backend [`#2359`](https://github.com/Human-Connection/Human-Connection/pull/2359)
- build(deps): bump apollo-server from 2.9.11 to 2.9.12 in /backend [`#2357`](https://github.com/Human-Connection/Human-Connection/pull/2357)
- build(deps-dev): bump eslint-plugin-jest from 23.0.4 to 23.0.5 in /webapp [`#2369`](https://github.com/Human-Connection/Human-Connection/pull/2369)
- build(deps): bump @hapi/joi from 16.1.7 to 16.1.8 in /backend [`#2368`](https://github.com/Human-Connection/Human-Connection/pull/2368)
- build(deps-dev): bump eslint-plugin-jest from 23.0.4 to 23.0.5 in /backend [`#2365`](https://github.com/Human-Connection/Human-Connection/pull/2365)
- build(deps-dev): bump @babel/plugin-proposal-throw-expressions from 7.2.0 to 7.7.4 in /backend [`#2339`](https://github.com/Human-Connection/Human-Connection/pull/2339)
- refactor: Close session in isAuthor permission [`#2334`](https://github.com/Human-Connection/Human-Connection/pull/2334)
- build(deps): bump date-fns from 2.7.0 to 2.8.1 in /webapp [`#2323`](https://github.com/Human-Connection/Human-Connection/pull/2323)
- 1967 component tests content view [`#2169`](https://github.com/Human-Connection/Human-Connection/pull/2169)
- If an admin searches for a user by email, don't crash if no user can be found [`#2295`](https://github.com/Human-Connection/Human-Connection/pull/2295)
- Migrate styleguide icons [`#2288`](https://github.com/Human-Connection/Human-Connection/pull/2288)
- build(deps-dev): bump eslint from 6.6.0 to 6.7.1 in /backend [`#2358`](https://github.com/Human-Connection/Human-Connection/pull/2358)
- build(deps-dev): bump @babel/preset-env from 7.7.1 to 7.7.4 in /backend [`#2341`](https://github.com/Human-Connection/Human-Connection/pull/2341)
- build(deps-dev): bump @babel/core from 7.7.2 to 7.7.4 in /webapp [`#2340`](https://github.com/Human-Connection/Human-Connection/pull/2340)
- build(deps): bump date-fns from 2.7.0 to 2.8.1 in /backend [`#2322`](https://github.com/Human-Connection/Human-Connection/pull/2322)
- build(deps): bump validator from 12.0.0 to 12.1.0 in /webapp [`#2319`](https://github.com/Human-Connection/Human-Connection/pull/2319)
- Update to version 0.1.11 with bug fixes [`#2354`](https://github.com/Human-Connection/Human-Connection/pull/2354)
- Fix updating post by adding/changing image bug submits form [`#2350`](https://github.com/Human-Connection/Human-Connection/pull/2350) - Fix updating post by adding/changing image bug submits form [`#2350`](https://github.com/Human-Connection/Human-Connection/pull/2350)
- Add shoutedBy_some to _PostFilter [`#2353`](https://github.com/Human-Connection/Human-Connection/pull/2353) - Add shoutedBy_some to _PostFilter [`#2353`](https://github.com/Human-Connection/Human-Connection/pull/2353)
- build(deps-dev): bump date-fns from 2.8.0 to 2.8.1 [`#2342`](https://github.com/Human-Connection/Human-Connection/pull/2342) - build(deps-dev): bump date-fns from 2.8.0 to 2.8.1 [`#2342`](https://github.com/Human-Connection/Human-Connection/pull/2342)
@ -71,7 +192,9 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- build(deps-dev): bump eslint from 5.16.0 to 6.6.0 in /webapp [`#2205`](https://github.com/Human-Connection/Human-Connection/pull/2205) - build(deps-dev): bump eslint from 5.16.0 to 6.6.0 in /webapp [`#2205`](https://github.com/Human-Connection/Human-Connection/pull/2205)
- Add locale to undefined to null [`#2233`](https://github.com/Human-Connection/Human-Connection/pull/2233) - Add locale to undefined to null [`#2233`](https://github.com/Human-Connection/Human-Connection/pull/2233)
- Update to version 0.1.10 [`#2231`](https://github.com/Human-Connection/Human-Connection/pull/2231) - Update to version 0.1.10 [`#2231`](https://github.com/Human-Connection/Human-Connection/pull/2231)
- Merge pull request #2443 from Human-Connection/2237-longer-comments [`#2237`](https://github.com/Human-Connection/Human-Connection/issues/2237)
- fix #2329: Normalize email on login in the backend [`#2329`](https://github.com/Human-Connection/Human-Connection/issues/2329) - fix #2329: Normalize email on login in the backend [`#2329`](https://github.com/Human-Connection/Human-Connection/issues/2329)
- Fix #2294 [`#2294`](https://github.com/Human-Connection/Human-Connection/issues/2294)
- Merge pull request #2078 from Human-Connection/fix-2042-back-link [`#2042`](https://github.com/Human-Connection/Human-Connection/issues/2042) - Merge pull request #2078 from Human-Connection/fix-2042-back-link [`#2042`](https://github.com/Human-Connection/Human-Connection/issues/2042)
- Tell github-linguists to ignore snapshots [`978347b`](https://github.com/Human-Connection/Human-Connection/commit/978347ba7b5a6aa1bc915ada972ffffa2816d37c) - Tell github-linguists to ignore snapshots [`978347b`](https://github.com/Human-Connection/Human-Connection/commit/978347ba7b5a6aa1bc915ada972ffffa2816d37c)
- Lokalise: update of webapp/locales/ru.json [`906e851`](https://github.com/Human-Connection/Human-Connection/commit/906e8518bf060134150187fb1574ac50ffd502f6) - Lokalise: update of webapp/locales/ru.json [`906e851`](https://github.com/Human-Connection/Human-Connection/commit/906e8518bf060134150187fb1574ac50ffd502f6)
@ -135,8 +258,8 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Update feature template [`#2116`](https://github.com/Human-Connection/Human-Connection/pull/2116) - Update feature template [`#2116`](https://github.com/Human-Connection/Human-Connection/pull/2116)
- Update to version 0.1.9 [`#2114`](https://github.com/Human-Connection/Human-Connection/pull/2114) - Update to version 0.1.9 [`#2114`](https://github.com/Human-Connection/Human-Connection/pull/2114)
- remove package-lock.json [`3cf3c31`](https://github.com/Human-Connection/Human-Connection/commit/3cf3c31808dc6ae59fb9c6ec33e9e178c5556438) - remove package-lock.json [`3cf3c31`](https://github.com/Human-Connection/Human-Connection/commit/3cf3c31808dc6ae59fb9c6ec33e9e178c5556438)
- add current file [`26c0d4d`](https://github.com/Human-Connection/Human-Connection/commit/26c0d4d83e4418a2378e05b66b6b47461f82735f) - Extract AvatarMenu into its own component [`994a0b0`](https://github.com/Human-Connection/Human-Connection/commit/994a0b049d1803784d9c06383872f1c9e33095a0)
- Finish portuguese translations [`15c671c`](https://github.com/Human-Connection/Human-Connection/commit/15c671c4a8aae86317896ca30601389504bce9e1) - Add notifications page with Notifications in table [`7cdc12f`](https://github.com/Human-Connection/Human-Connection/commit/7cdc12f4b9943062e15a874dd39f8a50142b6c61)
#### [v0.1.9](https://github.com/Human-Connection/Human-Connection/compare/v0.1.8...v0.1.9) #### [v0.1.9](https://github.com/Human-Connection/Human-Connection/compare/v0.1.8...v0.1.9)
@ -200,9 +323,9 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Fix #2042 Back Link To Login Page [`#2042`](https://github.com/Human-Connection/Human-Connection/issues/2042) - Fix #2042 Back Link To Login Page [`#2042`](https://github.com/Human-Connection/Human-Connection/issues/2042)
- Merge pull request #2043 from Human-Connection/fix-1993 [`#1993`](https://github.com/Human-Connection/Human-Connection/issues/1993) - Merge pull request #2043 from Human-Connection/fix-1993 [`#1993`](https://github.com/Human-Connection/Human-Connection/issues/1993)
- fix #1993 [`#1993`](https://github.com/Human-Connection/Human-Connection/issues/1993) - fix #1993 [`#1993`](https://github.com/Human-Connection/Human-Connection/issues/1993)
- Prepare backend for next implementation step [`7b32243`](https://github.com/Human-Connection/Human-Connection/commit/7b3224327e67a2895e4bc15b8987b13c6f57f015)
- 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)
- add migration plan to webapp readme [`8816f7b`](https://github.com/Human-Connection/Human-Connection/commit/8816f7be2a9662bc1333e37b306dee6b964fc2e0)
#### [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/0.1.7...v0.1.8)
@ -224,7 +347,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Update to version 0.1.7 [`#2015`](https://github.com/Human-Connection/Human-Connection/pull/2015) - Update to version 0.1.7 [`#2015`](https://github.com/Human-Connection/Human-Connection/pull/2015)
- Update to version 0.1.8 [`d45264b`](https://github.com/Human-Connection/Human-Connection/commit/d45264b3afa1557c2205e7ca1b77c778ee37ab5a) - Update to version 0.1.8 [`d45264b`](https://github.com/Human-Connection/Human-Connection/commit/d45264b3afa1557c2205e7ca1b77c778ee37ab5a)
- 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)
- build(deps-dev): bump @storybook/addon-actions in /webapp [`7e95d37`](https://github.com/Human-Connection/Human-Connection/commit/7e95d376a311a5ede6351d577d30e25aea9cb65d) - 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) #### [0.1.7](https://github.com/Human-Connection/Human-Connection/compare/0.1.6...0.1.7)

View File

@ -1 +1 @@
0.1.11 0.1.13

View File

@ -33,9 +33,9 @@
}, },
"dependencies": { "dependencies": {
"@hapi/joi": "^16.1.8", "@hapi/joi": "^16.1.8",
"@sentry/node": "^5.10.1", "@sentry/node": "^5.10.2",
"apollo-cache-inmemory": "~1.6.3", "apollo-cache-inmemory": "~1.6.5",
"apollo-client": "~2.6.4", "apollo-client": "~2.6.8",
"apollo-link-context": "~1.0.19", "apollo-link-context": "~1.0.19",
"apollo-link-http": "~1.5.16", "apollo-link-http": "~1.5.16",
"apollo-server": "~2.9.13", "apollo-server": "~2.9.13",
@ -63,28 +63,28 @@
"lodash": "~4.17.14", "lodash": "~4.17.14",
"merge-graphql-schemas": "^1.7.3", "merge-graphql-schemas": "^1.7.3",
"metascraper": "^5.8.9", "metascraper": "^5.8.9",
"metascraper-audio": "^5.8.7", "metascraper-audio": "^5.8.10",
"metascraper-author": "^5.8.7", "metascraper-author": "^5.8.7",
"metascraper-clearbit-logo": "^5.3.0", "metascraper-clearbit-logo": "^5.3.0",
"metascraper-date": "^5.8.7", "metascraper-date": "^5.8.7",
"metascraper-description": "^5.8.7", "metascraper-description": "^5.8.10",
"metascraper-image": "^5.8.7", "metascraper-image": "^5.8.10",
"metascraper-lang": "^5.8.9", "metascraper-lang": "^5.8.10",
"metascraper-lang-detector": "^4.10.2", "metascraper-lang-detector": "^4.10.2",
"metascraper-logo": "^5.8.7", "metascraper-logo": "^5.8.10",
"metascraper-publisher": "^5.8.7", "metascraper-publisher": "^5.8.7",
"metascraper-soundcloud": "^5.8.9", "metascraper-soundcloud": "^5.8.10",
"metascraper-title": "^5.8.7", "metascraper-title": "^5.8.10",
"metascraper-url": "^5.8.7", "metascraper-url": "^5.8.7",
"metascraper-video": "^5.8.9", "metascraper-video": "^5.8.10",
"metascraper-youtube": "^5.8.9", "metascraper-youtube": "^5.8.10",
"minimatch": "^3.0.4", "minimatch": "^3.0.4",
"mustache": "^3.1.0", "mustache": "^3.1.0",
"neo4j-driver": "~1.7.6", "neo4j-driver": "~1.7.6",
"neo4j-graphql-js": "^2.10.0", "neo4j-graphql-js": "^2.10.2",
"neode": "^0.3.3", "neode": "^0.3.6",
"node-fetch": "~2.6.0", "node-fetch": "~2.6.0",
"nodemailer": "^6.4.1", "nodemailer": "^6.4.2",
"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",
@ -97,11 +97,11 @@
"xregexp": "^4.2.4" "xregexp": "^4.2.4"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "~7.7.4", "@babel/cli": "~7.7.5",
"@babel/core": "~7.7.5", "@babel/core": "~7.7.5",
"@babel/node": "~7.7.4", "@babel/node": "~7.7.4",
"@babel/plugin-proposal-throw-expressions": "^7.7.4", "@babel/plugin-proposal-throw-expressions": "^7.7.4",
"@babel/preset-env": "~7.7.4", "@babel/preset-env": "~7.7.6",
"@babel/register": "~7.7.0", "@babel/register": "~7.7.0",
"apollo-server-testing": "~2.9.13", "apollo-server-testing": "~2.9.13",
"babel-core": "~7.0.0-0", "babel-core": "~7.0.0-0",
@ -112,14 +112,14 @@
"eslint": "~6.7.2", "eslint": "~6.7.2",
"eslint-config-prettier": "~6.7.0", "eslint-config-prettier": "~6.7.0",
"eslint-config-standard": "~14.1.0", "eslint-config-standard": "~14.1.0",
"eslint-plugin-import": "~2.18.2", "eslint-plugin-import": "~2.19.1",
"eslint-plugin-jest": "~23.1.1", "eslint-plugin-jest": "~23.1.1",
"eslint-plugin-node": "~10.0.0", "eslint-plugin-node": "~10.0.0",
"eslint-plugin-prettier": "~3.1.1", "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": "~24.9.0",
"nodemon": "~2.0.1", "nodemon": "~2.0.2",
"prettier": "~1.19.1", "prettier": "~1.19.1",
"supertest": "~4.0.2" "supertest": "~4.0.2"
} }

View File

@ -1,15 +1,17 @@
import { v1 as neo4j } from 'neo4j-driver' import { v1 as neo4j } from 'neo4j-driver'
import CONFIG from './../config' import CONFIG from './../config'
import setupNeode from './neode' import Neode from 'neode'
import models from '../models'
let driver let driver
const defaultOptions = {
uri: CONFIG.NEO4J_URI,
username: CONFIG.NEO4J_USERNAME,
password: CONFIG.NEO4J_PASSWORD,
}
export function getDriver(options = {}) { export function getDriver(options = {}) {
const { const { uri, username, password } = { ...defaultOptions, ...options }
uri = CONFIG.NEO4J_URI,
username = CONFIG.NEO4J_USERNAME,
password = CONFIG.NEO4J_PASSWORD,
} = options
if (!driver) { if (!driver) {
driver = neo4j.driver(uri, neo4j.auth.basic(username, password)) driver = neo4j.driver(uri, neo4j.auth.basic(username, password))
} }
@ -17,10 +19,11 @@ export function getDriver(options = {}) {
} }
let neodeInstance let neodeInstance
export function neode() { export function getNeode(options = {}) {
if (!neodeInstance) { if (!neodeInstance) {
const { NEO4J_URI: uri, NEO4J_USERNAME: username, NEO4J_PASSWORD: password } = CONFIG const { uri, username, password } = { ...defaultOptions, ...options }
neodeInstance = setupNeode({ uri, username, password }) neodeInstance = new Neode(uri, username, password).with(models)
return neodeInstance
} }
return neodeInstance return neodeInstance
} }

View File

@ -1,9 +0,0 @@
import Neode from 'neode'
import models from '../models'
export default function setupNeode(options) {
const { uri, username, password } = options
const neodeInstance = new Neode(uri, username, password)
neodeInstance.with(models)
return neodeInstance
}

View File

@ -11,27 +11,28 @@ export default async (driver, authorizationHeader) => {
} catch (err) { } catch (err) {
return null return null
} }
const query = ` const session = driver.session()
const writeTxResultPromise = session.writeTransaction(async transaction => {
const updateUserLastActiveTransactionResponse = await transaction.run(
`
MATCH (user:User {id: $id, deleted: false, disabled: false }) MATCH (user:User {id: $id, deleted: false, disabled: false })
SET user.lastActiveAt = toString(datetime()) SET user.lastActiveAt = toString(datetime())
RETURN user {.id, .slug, .name, .avatar, .email, .role, .disabled, .actorId} RETURN user {.id, .slug, .name, .avatar, .email, .role, .disabled, .actorId}
LIMIT 1 LIMIT 1
` `,
const session = driver.session() { id },
let result )
return updateUserLastActiveTransactionResponse.records.map(record => record.get('user'))
try {
result = await session.run(query, { id })
} finally {
session.close()
}
const [currentUser] = await result.records.map(record => {
return record.get('user')
}) })
try {
const [currentUser] = await writeTxResultPromise
if (!currentUser) return null if (!currentUser) return null
return { return {
token, token,
...currentUser, ...currentUser,
} }
} finally {
session.close()
}
} }

View File

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

View File

@ -2,30 +2,23 @@ import extractHashtags from '../hashtags/extractHashtags'
const updateHashtagsOfPost = async (postId, hashtags, context) => { const updateHashtagsOfPost = async (postId, hashtags, context) => {
if (!hashtags.length) return if (!hashtags.length) return
// We need two Cypher statements, because the 'MATCH' in the 'cypherDeletePreviousRelations' statement
// functions as an 'if'. In case there is no previous relation, the rest of the commands are omitted
// and no new Hashtags and relations will be created.
const cypherDeletePreviousRelations = `
MATCH (p: Post { id: $postId })-[previousRelations: TAGGED]->(t: Tag)
DELETE previousRelations
RETURN p, t
`
const cypherCreateNewTagsAndRelations = `
MATCH (p: Post { id: $postId})
UNWIND $hashtags AS tagName
MERGE (t: Tag { id: tagName, disabled: false, deleted: false })
MERGE (p)-[:TAGGED]->(t)
RETURN p, t
`
const session = context.driver.session() const session = context.driver.session()
try { try {
await session.run(cypherDeletePreviousRelations, { await session.writeTransaction(txc => {
postId, return txc.run(
}) `
await session.run(cypherCreateNewTagsAndRelations, { MATCH (post:Post { id: $postId})
postId, OPTIONAL MATCH (post)-[previousRelations:TAGGED]->(tag:Tag)
hashtags, DELETE previousRelations
WITH post
UNWIND $hashtags AS tagName
MERGE (tag:Tag {id: tagName, disabled: false, deleted: false })
MERGE (post)-[:TAGGED]->(tag)
RETURN post, tag
`,
{ postId, hashtags },
)
}) })
} finally { } finally {
session.close() session.close()

View File

@ -1,7 +1,7 @@
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import Factory from '../../seed/factories' import Factory from '../../seed/factories'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import { neode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../bootstrap/neo4j'
import createServer from '../../server' import createServer from '../../server'
let server let server
@ -11,7 +11,7 @@ let hashtagingUser
let authenticatedUser let authenticatedUser
const factory = Factory() const factory = Factory()
const driver = getDriver() const driver = getDriver()
const instance = neode() const neode = getNeode()
const categoryIds = ['cat9'] const categoryIds = ['cat9']
const createPostMutation = gql` const createPostMutation = gql`
mutation($id: ID, $title: String!, $postContent: String!, $categoryIds: [ID]!) { mutation($id: ID, $title: String!, $postContent: String!, $categoryIds: [ID]!) {
@ -36,7 +36,7 @@ beforeAll(() => {
context: () => { context: () => {
return { return {
user: authenticatedUser, user: authenticatedUser,
neode: instance, neode,
driver, driver,
} }
}, },
@ -48,14 +48,14 @@ beforeAll(() => {
}) })
beforeEach(async () => { beforeEach(async () => {
hashtagingUser = await instance.create('User', { hashtagingUser = await neode.create('User', {
id: 'you', id: 'you',
name: 'Al Capone', name: 'Al Capone',
slug: 'al-capone', slug: 'al-capone',
email: 'test@example.org', email: 'test@example.org',
password: '1234', password: '1234',
}) })
await instance.create('Category', { await neode.create('Category', {
id: 'cat9', id: 'cat9',
name: 'Democracy & Politics', name: 'Democracy & Politics',
icon: 'university', icon: 'university',

View File

@ -7,7 +7,7 @@ import sluggify from './sluggifyMiddleware'
import excerpt from './excerptMiddleware' import excerpt from './excerptMiddleware'
import xss from './xssMiddleware' import xss from './xssMiddleware'
import permissions from './permissionsMiddleware' import permissions from './permissionsMiddleware'
import user from './userMiddleware' import user from './user/userMiddleware'
import includedFields from './includedFieldsMiddleware' import includedFields from './includedFieldsMiddleware'
import orderBy from './orderByMiddleware' import orderBy from './orderByMiddleware'
import validation from './validation/validationMiddleware' import validation from './validation/validationMiddleware'

View File

@ -38,7 +38,7 @@ const createLocation = async (session, mapboxData) => {
lng: mapboxData.center && mapboxData.center.length ? mapboxData.center[1] : null, lng: mapboxData.center && mapboxData.center.length ? mapboxData.center[1] : null,
} }
let query = let mutation =
'MERGE (l:Location {id: $id}) ' + 'MERGE (l:Location {id: $id}) ' +
'SET l.name = $nameEN, ' + 'SET l.name = $nameEN, ' +
'l.nameEN = $nameEN, ' + 'l.nameEN = $nameEN, ' +
@ -53,19 +53,23 @@ const createLocation = async (session, mapboxData) => {
'l.type = $type' 'l.type = $type'
if (data.lat && data.lng) { if (data.lat && data.lng) {
query += ', l.lat = $lat, l.lng = $lng' mutation += ', l.lat = $lat, l.lng = $lng'
} }
query += ' RETURN l.id' mutation += ' RETURN l.id'
await session.run(query, data) try {
await session.writeTransaction(transaction => {
return transaction.run(mutation, data)
})
} finally {
session.close() session.close()
}
} }
const createOrUpdateLocations = async (userId, locationName, driver) => { const createOrUpdateLocations = async (userId, locationName, driver) => {
if (isEmpty(locationName)) { if (isEmpty(locationName)) {
return return
} }
const res = await fetch( const res = await fetch(
`https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent( `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(
locationName, locationName,
@ -106,33 +110,44 @@ const createOrUpdateLocations = async (userId, locationName, driver) => {
if (data.context) { if (data.context) {
await asyncForEach(data.context, async ctx => { await asyncForEach(data.context, async ctx => {
await createLocation(session, ctx) await createLocation(session, ctx)
try {
await session.run( await session.writeTransaction(transaction => {
'MATCH (parent:Location {id: $parentId}), (child:Location {id: $childId}) ' + return transaction.run(
'MERGE (child)<-[:IS_IN]-(parent) ' + `
'RETURN child.id, parent.id', MATCH (parent:Location {id: $parentId}), (child:Location {id: $childId})
MERGE (child)<-[:IS_IN]-(parent)
RETURN child.id, parent.id
`,
{ {
parentId: parent.id, parentId: parent.id,
childId: ctx.id, childId: ctx.id,
}, },
) )
})
parent = ctx parent = ctx
} finally {
session.close()
}
}) })
} }
// delete all current locations from user // delete all current locations from user and add new location
await session.run('MATCH (u:User {id: $userId})-[r:IS_IN]->(l:Location) DETACH DELETE r', { try {
userId: userId, await session.writeTransaction(transaction => {
}) return transaction.run(
// connect user with location `
await session.run( MATCH (user:User {id: $userId})-[relationship:IS_IN]->(location:Location)
'MATCH (u:User {id: $userId}), (l:Location {id: $locationId}) MERGE (u)-[:IS_IN]->(l) RETURN l.id, u.id', DETACH DELETE relationship
{ WITH user
userId: userId, MATCH (location:Location {id: $locationId})
locationId: data.id, MERGE (user)-[:IS_IN]->(location)
}, RETURN location.id, user.id
`,
{ userId: userId, locationId: data.id },
) )
})
} finally {
session.close() session.close()
}
} }
export default createOrUpdateLocations export default createOrUpdateLocations

View File

@ -1,66 +1,73 @@
import extractMentionedUsers from './mentions/extractMentionedUsers' import extractMentionedUsers from './mentions/extractMentionedUsers'
import { validateNotifyUsers } from '../validation/validationMiddleware'
const postAuthorOfComment = async (comment, { context }) => { const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => {
const cypherFindUser = ` const idsOfUsers = extractMentionedUsers(args.content)
MATCH (user: User)-[:WROTE]->(:Post)<-[:COMMENTS]-(:Comment { id: $commentId }) const post = await resolve(root, args, context, resolveInfo)
RETURN user { .id } if (post && idsOfUsers && idsOfUsers.length)
` await notifyUsersOfMention('Post', post.id, idsOfUsers, 'mentioned_in_post', context)
return post
}
const handleContentDataOfComment = async (resolve, root, args, context, resolveInfo) => {
const { content } = args
let idsOfUsers = extractMentionedUsers(content)
const comment = await resolve(root, args, context, resolveInfo)
const [postAuthor] = await postAuthorOfComment(comment.id, { context })
idsOfUsers = idsOfUsers.filter(id => id !== postAuthor.id)
if (idsOfUsers && idsOfUsers.length)
await notifyUsersOfMention('Comment', comment.id, idsOfUsers, 'mentioned_in_comment', context)
if (context.user.id !== postAuthor.id)
await notifyUsersOfComment('Comment', comment.id, postAuthor.id, 'commented_on_post', context)
return comment
}
const postAuthorOfComment = async (commentId, { context }) => {
const session = context.driver.session() const session = context.driver.session()
let result let postAuthorId
try { try {
result = await session.run(cypherFindUser, { postAuthorId = await session.readTransaction(transaction => {
commentId: comment.id, return transaction.run(
`
MATCH (author:User)-[:WROTE]->(:Post)<-[:COMMENTS]-(:Comment { id: $commentId })
RETURN author { .id } as authorId
`,
{ commentId },
)
}) })
return postAuthorId.records.map(record => record.get('authorId'))
} finally { } finally {
session.close() session.close()
} }
const [postAuthor] = await result.records.map(record => {
return record.get('user')
})
return postAuthor
} }
const notifyUsers = async (label, id, idsOfUsers, reason, context) => { const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => {
if (!idsOfUsers.length) return await validateNotifyUsers(label, reason)
let mentionedCypher
// Checked here, because it does not go through GraphQL checks at all in this file.
const reasonsAllowed = ['mentioned_in_post', 'mentioned_in_comment', 'commented_on_post']
if (!reasonsAllowed.includes(reason)) {
throw new Error('Notification reason is not allowed!')
}
if (
(label === 'Post' && reason !== 'mentioned_in_post') ||
(label === 'Comment' && !['mentioned_in_comment', 'commented_on_post'].includes(reason))
) {
throw new Error('Notification does not fit the reason!')
}
let cypher
switch (reason) { switch (reason) {
case 'mentioned_in_post': { case 'mentioned_in_post': {
cypher = ` mentionedCypher = `
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)
SET notification.read = FALSE
SET (
CASE
WHEN notification.createdAt IS NULL
THEN notification END ).createdAt = toString(datetime())
SET notification.updatedAt = toString(datetime())
` `
break break
} }
case 'mentioned_in_comment': { case 'mentioned_in_comment': {
cypher = ` mentionedCypher = `
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
}
}
mentionedCypher += `
SET notification.read = FALSE SET notification.read = FALSE
SET ( SET (
CASE CASE
@ -68,97 +75,47 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => {
THEN notification END ).createdAt = toString(datetime()) THEN notification END ).createdAt = toString(datetime())
SET notification.updatedAt = toString(datetime()) SET notification.updatedAt = toString(datetime())
` `
break const session = context.driver.session()
try {
await session.writeTransaction(transaction => {
return transaction.run(mentionedCypher, { id, idsOfUsers, reason })
})
} finally {
session.close()
} }
case 'commented_on_post': { }
cypher = `
MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(author: User) const notifyUsersOfComment = async (label, commentId, postAuthorId, reason, context) => {
MATCH (user: User) await validateNotifyUsers(label, reason)
WHERE user.id in $idsOfUsers const session = context.driver.session()
AND NOT (user)<-[:BLOCKED]-(author)
AND NOT (author)<-[:BLOCKED]-(user) try {
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user) await session.writeTransaction(async transaction => {
await transaction.run(
`
MATCH (postAuthor:User {id: $postAuthorId})-[:WROTE]->(post:Post)<-[:COMMENTS]-(comment:Comment { id: $commentId })<-[:WROTE]-(commenter:User)
WHERE NOT (postAuthor)-[:BLOCKED]-(commenter)
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(postAuthor)
SET notification.read = FALSE SET notification.read = FALSE
SET ( SET (
CASE CASE
WHEN notification.createdAt IS NULL WHEN notification.createdAt IS NULL
THEN notification END ).createdAt = toString(datetime()) THEN notification END ).createdAt = toString(datetime())
SET notification.updatedAt = toString(datetime()) SET notification.updatedAt = toString(datetime())
` `,
break { commentId, postAuthorId, reason },
} )
}
const session = context.driver.session()
try {
await session.run(cypher, {
id,
idsOfUsers,
reason,
}) })
} finally { } finally {
session.close() session.close()
} }
} }
const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => {
const idsOfUsers = extractMentionedUsers(args.content)
const post = await resolve(root, args, context, resolveInfo)
if (post) {
await notifyUsers('Post', post.id, idsOfUsers, 'mentioned_in_post', context)
}
return post
}
const handleContentDataOfComment = async (resolve, root, args, context, resolveInfo) => {
let idsOfUsers = extractMentionedUsers(args.content)
const comment = await resolve(root, args, context, resolveInfo)
if (comment) {
const postAuthor = await postAuthorOfComment(comment, { context })
idsOfUsers = idsOfUsers.filter(id => id !== postAuthor.id)
await notifyUsers('Comment', comment.id, idsOfUsers, 'mentioned_in_comment', context)
}
return comment
}
const handleCreateComment = async (resolve, root, args, context, resolveInfo) => {
const comment = await handleContentDataOfComment(resolve, root, args, context, resolveInfo)
if (comment) {
const cypherFindUser = `
MATCH (user: User)-[:WROTE]->(:Post)<-[:COMMENTS]-(:Comment { id: $commentId })
RETURN user { .id }
`
const session = context.driver.session()
let result
try {
result = await session.run(cypherFindUser, {
commentId: comment.id,
})
} finally {
session.close()
}
const [postAuthor] = await result.records.map(record => {
return record.get('user')
})
if (context.user.id !== postAuthor.id) {
await notifyUsers('Comment', comment.id, [postAuthor.id], 'commented_on_post', context)
}
}
return comment
}
export default { export default {
Mutation: { Mutation: {
CreatePost: handleContentDataOfPost, CreatePost: handleContentDataOfPost,
UpdatePost: handleContentDataOfPost, UpdatePost: handleContentDataOfPost,
CreateComment: handleCreateComment, CreateComment: handleContentDataOfComment,
UpdateComment: handleContentDataOfComment, UpdateComment: handleContentDataOfComment,
}, },
} }

View File

@ -1,17 +1,13 @@
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import Factory from '../../seed/factories' import Factory from '../../seed/factories'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import { neode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../bootstrap/neo4j'
import createServer from '../../server' import createServer from '../../server'
let server let server, query, mutate, notifiedUser, authenticatedUser
let query
let mutate
let notifiedUser
let authenticatedUser
const factory = Factory() const factory = Factory()
const driver = getDriver() const driver = getDriver()
const instance = neode() const neode = getNeode()
const categoryIds = ['cat9'] const categoryIds = ['cat9']
const createPostMutation = gql` const createPostMutation = gql`
mutation($id: ID, $title: String!, $postContent: String!, $categoryIds: [ID]!) { mutation($id: ID, $title: String!, $postContent: String!, $categoryIds: [ID]!) {
@ -39,12 +35,13 @@ const createCommentMutation = gql`
} }
` `
beforeAll(() => { beforeAll(async () => {
await factory.cleanDatabase()
const createServerResult = createServer({ const createServerResult = createServer({
context: () => { context: () => {
return { return {
user: authenticatedUser, user: authenticatedUser,
neode: instance, neode: neode,
driver, driver,
} }
}, },
@ -56,14 +53,14 @@ beforeAll(() => {
}) })
beforeEach(async () => { beforeEach(async () => {
notifiedUser = await instance.create('User', { notifiedUser = await neode.create('User', {
id: 'you', id: 'you',
name: 'Al Capone', name: 'Al Capone',
slug: 'al-capone', slug: 'al-capone',
email: 'test@example.org', email: 'test@example.org',
password: '1234', password: '1234',
}) })
await instance.create('Category', { await neode.create('Category', {
id: 'cat9', id: 'cat9',
name: 'Democracy & Politics', name: 'Democracy & Politics',
icon: 'university', icon: 'university',
@ -146,7 +143,7 @@ describe('notifications', () => {
describe('commenter is not me', () => { describe('commenter is not me', () => {
beforeEach(async () => { beforeEach(async () => {
commentContent = 'Commenters comment.' commentContent = 'Commenters comment.'
commentAuthor = await instance.create('User', { commentAuthor = await neode.create('User', {
id: 'commentAuthor', id: 'commentAuthor',
name: 'Mrs Comment', name: 'Mrs Comment',
slug: 'mrs-comment', slug: 'mrs-comment',
@ -173,7 +170,6 @@ describe('notifications', () => {
], ],
}, },
}) })
const { query } = createTestClient(server)
await expect( await expect(
query({ query({
query: notificationQuery, query: notificationQuery,
@ -190,7 +186,7 @@ describe('notifications', () => {
const expected = expect.objectContaining({ const expected = expect.objectContaining({
data: { notifications: [] }, data: { notifications: [] },
}) })
const { query } = createTestClient(server)
await expect( await expect(
query({ query({
query: notificationQuery, query: notificationQuery,
@ -214,7 +210,7 @@ describe('notifications', () => {
const expected = expect.objectContaining({ const expected = expect.objectContaining({
data: { notifications: [] }, data: { notifications: [] },
}) })
const { query } = createTestClient(server)
await expect( await expect(
query({ query({
query: notificationQuery, query: notificationQuery,
@ -228,7 +224,7 @@ describe('notifications', () => {
}) })
beforeEach(async () => { beforeEach(async () => {
postAuthor = await instance.create('User', { postAuthor = await neode.create('User', {
id: 'postAuthor', id: 'postAuthor',
name: 'Mrs Post', name: 'Mrs Post',
slug: 'mrs-post', slug: 'mrs-post',
@ -265,7 +261,7 @@ describe('notifications', () => {
], ],
}, },
}) })
const { query } = createTestClient(server)
await expect( await expect(
query({ query({
query: notificationQuery, query: notificationQuery,
@ -409,7 +405,7 @@ describe('notifications', () => {
const expected = expect.objectContaining({ const expected = expect.objectContaining({
data: { notifications: [] }, data: { notifications: [] },
}) })
const { query } = createTestClient(server)
await expect( await expect(
query({ query({
query: notificationQuery, query: notificationQuery,
@ -432,7 +428,7 @@ describe('notifications', () => {
beforeEach(async () => { beforeEach(async () => {
commentContent = commentContent =
'One mention about me with <a data-mention-id="you" class="mention" href="/profile/you" target="_blank">@al-capone</a>.' 'One mention about me with <a data-mention-id="you" class="mention" href="/profile/you" target="_blank">@al-capone</a>.'
commentAuthor = await instance.create('User', { commentAuthor = await neode.create('User', {
id: 'commentAuthor', id: 'commentAuthor',
name: 'Mrs Comment', name: 'Mrs Comment',
slug: 'mrs-comment', slug: 'mrs-comment',
@ -442,7 +438,7 @@ describe('notifications', () => {
}) })
it('sends only one notification with reason mentioned_in_comment', async () => { it('sends only one notification with reason mentioned_in_comment', async () => {
postAuthor = await instance.create('User', { postAuthor = await neode.create('User', {
id: 'MrPostAuthor', id: 'MrPostAuthor',
name: 'Mr Author', name: 'Mr Author',
slug: 'mr-author', slug: 'mr-author',
@ -467,7 +463,7 @@ describe('notifications', () => {
], ],
}, },
}) })
const { query } = createTestClient(server)
await expect( await expect(
query({ query({
query: notificationQuery, query: notificationQuery,
@ -501,7 +497,7 @@ describe('notifications', () => {
], ],
}, },
}) })
const { query } = createTestClient(server)
await expect( await expect(
query({ query({
query: notificationQuery, query: notificationQuery,
@ -518,7 +514,7 @@ describe('notifications', () => {
await postAuthor.relateTo(notifiedUser, 'blocked') await postAuthor.relateTo(notifiedUser, 'blocked')
commentContent = commentContent =
'One mention about me with <a data-mention-id="you" class="mention" href="/profile/you" target="_blank">@al-capone</a>.' 'One mention about me with <a data-mention-id="you" class="mention" href="/profile/you" target="_blank">@al-capone</a>.'
commentAuthor = await instance.create('User', { commentAuthor = await neode.create('User', {
id: 'commentAuthor', id: 'commentAuthor',
name: 'Mrs Comment', name: 'Mrs Comment',
slug: 'mrs-comment', slug: 'mrs-comment',
@ -532,7 +528,7 @@ describe('notifications', () => {
const expected = expect.objectContaining({ const expected = expect.objectContaining({
data: { notifications: [] }, data: { notifications: [] },
}) })
const { query } = createTestClient(server)
await expect( await expect(
query({ query({
query: notificationQuery, query: notificationQuery,

View File

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

View File

@ -1,11 +1,11 @@
import { rule, shield, deny, allow, or } from 'graphql-shield' import { rule, shield, deny, allow, or } from 'graphql-shield'
import { neode } from '../bootstrap/neo4j' import { getNeode } from '../bootstrap/neo4j'
import CONFIG from '../config' import CONFIG from '../config'
const debug = !!CONFIG.DEBUG const debug = !!CONFIG.DEBUG
const allowExternalErrors = true const allowExternalErrors = true
const instance = neode() const neode = getNeode()
const isAuthenticated = rule({ const isAuthenticated = rule({
cache: 'contextual', cache: 'contextual',
@ -36,7 +36,7 @@ const isMyOwn = rule({
const isMySocialMedia = rule({ const isMySocialMedia = rule({
cache: 'no_cache', cache: 'no_cache',
})(async (_, args, { user }) => { })(async (_, args, { user }) => {
let socialMedia = await instance.find('SocialMedia', args.id) let socialMedia = await neode.find('SocialMedia', args.id)
socialMedia = await socialMedia.toJson() socialMedia = await socialMedia.toJson()
return socialMedia.ownedBy.node.id === user.id return socialMedia.ownedBy.node.id === user.id
}) })
@ -47,17 +47,18 @@ const isAuthor = rule({
if (!user) return false if (!user) return false
const { id: resourceId } = args const { id: resourceId } = args
const session = driver.session() const session = driver.session()
try { const authorReadTxPromise = session.readTransaction(async transaction => {
const result = await session.run( const authorTransactionResponse = await transaction.run(
` `
MATCH (resource {id: $resourceId})<-[:WROTE]-(author {id: $userId}) MATCH (resource {id: $resourceId})<-[:WROTE]-(author {id: $userId})
RETURN author RETURN author
`, `,
{ resourceId, userId: user.id }, { resourceId, userId: user.id },
) )
const [author] = result.records.map(record => { return authorTransactionResponse.records.map(record => record.get('author'))
return record.get('author')
}) })
try {
const [author] = await authorReadTxPromise
return !!author return !!author
} finally { } finally {
session.close() session.close()

View File

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

View File

@ -4,10 +4,16 @@ const isUniqueFor = (context, type) => {
return async slug => { return async slug => {
const session = context.driver.session() const session = context.driver.session()
try { try {
const response = await session.run(`MATCH(p:${type} {slug: $slug }) return p.slug`, { const existingSlug = await session.readTransaction(transaction => {
slug, return transaction.run(
`
MATCH(p:${type} {slug: $slug })
RETURN p.slug
`,
{ slug },
)
}) })
return response.records.length === 0 return existingSlug.records.length === 0
} finally { } finally {
session.close() session.close()
} }

View File

@ -1,6 +1,6 @@
import Factory from '../seed/factories' import Factory from '../seed/factories'
import { gql } from '../helpers/jest' import { gql } from '../helpers/jest'
import { neode as getNeode, getDriver } from '../bootstrap/neo4j' import { getNeode, getDriver } from '../bootstrap/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 '../../seed/factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../bootstrap/neo4j'
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 createOrUpdateLocations from './nodes/locations' import createOrUpdateLocations from '../nodes/locations'
export default { export default {
Mutation: { Mutation: {
SignupVerification: async (resolve, root, args, context, info) => { SignupVerification: async (resolve, root, args, context, info) => {
const result = await resolve(root, args, context, info) const result = await resolve(root, args, context, info)
await createOrUpdateLocations(args.id, args.locationName, context.driver) await createOrUpdateLocations(result.id, args.locationName, context.driver)
return result return result
}, },
UpdateUser: async (resolve, root, args, context, info) => { UpdateUser: async (resolve, root, args, context, info) => {

View File

@ -0,0 +1,213 @@
import { gql } from '../../helpers/jest'
import Factory from '../../seed/factories'
import { getNeode, getDriver } from '../../bootstrap/neo4j'
import { createTestClient } from 'apollo-server-testing'
import createServer from '../../server'
const factory = Factory()
const neode = getNeode()
const driver = getDriver()
let authenticatedUser, mutate, variables
const signupVerificationMutation = gql`
mutation(
$name: String!
$password: String!
$email: String!
$nonce: String!
$termsAndConditionsAgreedVersion: String!
$locationName: String
) {
SignupVerification(
name: $name
password: $password
email: $email
nonce: $nonce
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
locationName: $locationName
) {
locationName
}
}
`
const updateUserMutation = gql`
mutation($id: ID!, $name: String!, $locationName: String) {
UpdateUser(id: $id, name: $name, locationName: $locationName) {
locationName
}
}
`
let newlyCreatedNodesWithLocales = [
{
city: {
lng: 41.1534,
nameES: 'Hamburg',
nameFR: 'Hamburg',
nameIT: 'Hamburg',
nameEN: 'Hamburg',
type: 'place',
namePT: 'Hamburg',
nameRU: 'Хамбург',
nameDE: 'Hamburg',
nameNL: 'Hamburg',
name: 'Hamburg',
namePL: 'Hamburg',
id: 'place.5977106083398860',
lat: -74.5763,
},
state: {
namePT: 'Nova Jérsia',
nameRU: 'Нью-Джерси',
nameDE: 'New Jersey',
nameNL: 'New Jersey',
nameES: 'Nueva Jersey',
name: 'New Jersey',
namePL: 'New Jersey',
nameFR: 'New Jersey',
nameIT: 'New Jersey',
id: 'region.14919479731700330',
nameEN: 'New Jersey',
type: 'region',
},
country: {
namePT: 'Estados Unidos',
nameRU: 'Соединённые Штаты Америки',
nameDE: 'Vereinigte Staaten',
nameNL: 'Verenigde Staten van Amerika',
nameES: 'Estados Unidos',
namePL: 'Stany Zjednoczone',
name: 'United States of America',
nameFR: 'États-Unis',
nameIT: "Stati Uniti d'America",
id: 'country.9053006287256050',
nameEN: 'United States of America',
type: 'country',
},
},
]
beforeAll(() => {
const { server } = createServer({
context: () => {
return {
user: authenticatedUser,
neode,
driver,
}
},
})
mutate = createTestClient(server).mutate
})
beforeEach(() => {
variables = {}
authenticatedUser = null
})
afterEach(() => {
factory.cleanDatabase()
})
describe('userMiddleware', () => {
describe('SignupVerification', () => {
beforeEach(async () => {
variables = {
...variables,
name: 'John Doe',
password: '123',
email: 'john@example.org',
nonce: '123456',
termsAndConditionsAgreedVersion: '0.1.0',
locationName: 'Hamburg, New Jersey, United States of America',
}
const args = {
email: 'john@example.org',
nonce: '123456',
}
await neode.model('EmailAddress').create(args)
})
it('creates a Location node with localised city/state/country names', async () => {
await mutate({ mutation: signupVerificationMutation, variables })
const locations = await neode.cypher(
`MATCH (city:Location)-[:IS_IN]->(state:Location)-[:IS_IN]->(country:Location) return city, state, country`,
)
expect(
locations.records.map(record => {
return {
city: record.get('city').properties,
state: record.get('state').properties,
country: record.get('country').properties,
}
}),
).toEqual(newlyCreatedNodesWithLocales)
})
})
describe('UpdateUser', () => {
let user, userParams
beforeEach(async () => {
newlyCreatedNodesWithLocales = [
{
city: {
lng: 53.55,
nameES: 'Hamburgo',
nameFR: 'Hambourg',
nameIT: 'Amburgo',
nameEN: 'Hamburg',
type: 'region',
namePT: 'Hamburgo',
nameRU: 'Гамбург',
nameDE: 'Hamburg',
nameNL: 'Hamburg',
namePL: 'Hamburg',
name: 'Hamburg',
id: 'region.10793468240398860',
lat: 10,
},
country: {
namePT: 'Alemanha',
nameRU: 'Германия',
nameDE: 'Deutschland',
nameNL: 'Duitsland',
nameES: 'Alemania',
name: 'Germany',
namePL: 'Niemcy',
nameFR: 'Allemagne',
nameIT: 'Germania',
id: 'country.10743216036480410',
nameEN: 'Germany',
type: 'country',
},
},
]
userParams = {
id: 'updating-user',
}
user = await factory.create('User', userParams)
authenticatedUser = await user.toJson()
})
it('creates a Location node with localised city/state/country names', async () => {
variables = {
...variables,
id: 'updating-user',
name: 'Updating user',
locationName: 'Hamburg, Germany',
}
await mutate({ mutation: updateUserMutation, variables })
const locations = await neode.cypher(
`MATCH (city:Location)-[:IS_IN]->(country:Location) return city, country`,
)
expect(
locations.records.map(record => {
return {
city: record.get('city').properties,
country: record.get('country').properties,
}
}),
).toEqual(newlyCreatedNodesWithLocales)
})
})
})

View File

@ -4,7 +4,7 @@ const COMMENT_MIN_LENGTH = 1
const NO_POST_ERR_MESSAGE = 'Comment cannot be created without a post!' const NO_POST_ERR_MESSAGE = 'Comment cannot be created without a post!'
const NO_CATEGORIES_ERR_MESSAGE = const NO_CATEGORIES_ERR_MESSAGE =
'You cannot save a post without at least one category or more than three' 'You cannot save a post without at least one category or more than three'
const USERNAME_MIN_LENGTH = 3
const validateCreateComment = async (resolve, root, args, context, info) => { const validateCreateComment = async (resolve, root, args, context, info) => {
const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim() const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim()
const { postId } = args const { postId } = args
@ -14,14 +14,15 @@ const validateCreateComment = async (resolve, root, args, context, info) => {
} }
const session = context.driver.session() const session = context.driver.session()
try { try {
const postQueryRes = await session.run( const postQueryRes = await session.readTransaction(transaction => {
return transaction.run(
` `
MATCH (post:Post {id: $postId}) MATCH (post:Post {id: $postId})
RETURN post`, RETURN post
{ `,
postId, { postId },
},
) )
})
const [post] = postQueryRes.records.map(record => { const [post] = postQueryRes.records.map(record => {
return record.get('post') return record.get('post')
}) })
@ -72,8 +73,8 @@ const validateReview = async (resolve, root, args, context, info) => {
const { user, driver } = context const { user, driver } = context
if (resourceId === user.id) throw new Error('You cannot review yourself!') if (resourceId === user.id) throw new Error('You cannot review yourself!')
const session = driver.session() const session = driver.session()
const reportReadTxPromise = session.writeTransaction(async txc => { const reportReadTxPromise = session.readTransaction(async transaction => {
const validateReviewTransactionResponse = await txc.run( const validateReviewTransactionResponse = await transaction.run(
` `
MATCH (resource {id: $resourceId}) MATCH (resource {id: $resourceId})
WHERE resource:User OR resource:Post OR resource:Comment WHERE resource:User OR resource:Post OR resource:Comment
@ -115,12 +116,31 @@ const validateReview = async (resolve, root, args, context, info) => {
return resolve(root, args, context, info) return resolve(root, args, context, info)
} }
export const validateNotifyUsers = async (label, reason) => {
const reasonsAllowed = ['mentioned_in_post', 'mentioned_in_comment', 'commented_on_post']
if (!reasonsAllowed.includes(reason)) throw new Error('Notification reason is not allowed!')
if (
(label === 'Post' && reason !== 'mentioned_in_post') ||
(label === 'Comment' && !['mentioned_in_comment', 'commented_on_post'].includes(reason))
) {
throw new Error('Notification does not fit the reason!')
}
}
const validateUpdateUser = async (resolve, root, params, context, info) => {
const { name } = params
if (typeof name === 'string' && name.trim().length < USERNAME_MIN_LENGTH)
throw new UserInputError(`Username must be at least ${USERNAME_MIN_LENGTH} character long!`)
return resolve(root, params, context, info)
}
export default { export default {
Mutation: { Mutation: {
CreateComment: validateCreateComment, CreateComment: validateCreateComment,
UpdateComment: validateUpdateComment, UpdateComment: validateUpdateComment,
CreatePost: validatePost, CreatePost: validatePost,
UpdatePost: validateUpdatePost, UpdatePost: validateUpdatePost,
UpdateUser: validateUpdateUser,
fileReport: validateReport, fileReport: validateReport,
review: validateReview, review: validateReview,
}, },

View File

@ -1,6 +1,6 @@
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import Factory from '../../seed/factories' import Factory from '../../seed/factories'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../bootstrap/neo4j'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import createServer from '../../server' import createServer from '../../server'
@ -71,6 +71,14 @@ const reviewMutation = gql`
} }
} }
` `
const updateUserMutation = gql`
mutation($id: ID!, $name: String) {
UpdateUser(id: $id, name: $name) {
name
}
}
`
beforeAll(() => { beforeAll(() => {
const { server } = createServer({ const { server } = createServer({
context: () => { context: () => {
@ -397,4 +405,33 @@ describe('validateReview', () => {
}) })
}) })
}) })
describe('validateUpdateUser', () => {
let userParams, variables, updatingUser
beforeEach(async () => {
userParams = {
id: 'updating-user',
name: 'John Doe',
}
variables = {
id: 'updating-user',
name: 'John Doughnut',
}
updatingUser = await factory.create('User', userParams)
authenticatedUser = await updatingUser.toJson()
})
it('with name too short', async () => {
variables = {
...variables,
name: ' ',
}
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject({
data: { UpdateUser: null },
errors: [{ message: 'Username must be at least 3 character long!' }],
})
})
})
}) })

View File

@ -1,8 +1,8 @@
import Factory from '../seed/factories' import Factory from '../seed/factories'
import { neode } from '../bootstrap/neo4j' import { getNeode } from '../bootstrap/neo4j'
const factory = Factory() const factory = Factory()
const instance = neode() const neode = getNeode()
afterEach(async () => { afterEach(async () => {
await factory.cleanDatabase() await factory.cleanDatabase()
@ -10,7 +10,7 @@ afterEach(async () => {
describe('role', () => { describe('role', () => {
it('defaults to `user`', async () => { it('defaults to `user`', async () => {
const user = await instance.create('User', { name: 'John' }) const user = await neode.create('User', { name: 'John' })
await expect(user.toJson()).resolves.toEqual( await expect(user.toJson()).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
role: 'user', role: 'user',
@ -21,7 +21,7 @@ describe('role', () => {
describe('slug', () => { describe('slug', () => {
it('normalizes to lowercase letters', async () => { it('normalizes to lowercase letters', async () => {
const user = await instance.create('User', { slug: 'Matt' }) const user = await neode.create('User', { slug: 'Matt' })
await expect(user.toJson()).resolves.toEqual( await expect(user.toJson()).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
slug: 'matt', slug: 'matt',
@ -30,9 +30,9 @@ describe('slug', () => {
}) })
it('must be unique', async done => { it('must be unique', async done => {
await instance.create('User', { slug: 'Matt' }) await neode.create('User', { slug: 'Matt' })
try { try {
await expect(instance.create('User', { slug: 'Matt' })).rejects.toThrow('already exists') await expect(neode.create('User', { slug: 'Matt' })).rejects.toThrow('already exists')
done() done()
} catch (error) { } catch (error) {
throw new Error(` throw new Error(`
@ -54,7 +54,7 @@ describe('slug', () => {
describe('characters', () => { describe('characters', () => {
const createUser = attrs => { const createUser = attrs => {
return instance.create('User', attrs).then(user => user.toJson()) return neode.create('User', attrs).then(user => user.toJson())
} }
it('-', async () => { it('-', async () => {
@ -70,15 +70,11 @@ describe('slug', () => {
}) })
it(' ', async () => { it(' ', async () => {
await expect(createUser({ slug: 'matt rider' })).rejects.toThrow( await expect(createUser({ slug: 'matt rider' })).rejects.toThrow('ERROR_VALIDATION')
/fails to match the required pattern/,
)
}) })
it('ä', async () => { it('ä', async () => {
await expect(createUser({ slug: 'mätt' })).rejects.toThrow( await expect(createUser({ slug: 'mätt' })).rejects.toThrow('ERROR_VALIDATION')
/fails to match the required pattern/,
)
}) })
}) })
}) })

View File

@ -5,6 +5,7 @@ export default {
Mutation: { Mutation: {
CreateComment: async (object, params, context, resolveInfo) => { CreateComment: async (object, params, context, resolveInfo) => {
const { postId } = params const { postId } = params
const { user, driver } = context
// Adding relationship from comment to post by passing in the postId, // Adding relationship from comment to post by passing in the postId,
// but we do not want to create the comment with postId as an attribute // but we do not want to create the comment with postId as an attribute
// because we use relationships for this. So, we are deleting it from params // because we use relationships for this. So, we are deleting it from params
@ -12,9 +13,11 @@ export default {
delete params.postId delete params.postId
params.id = params.id || uuid() params.id = params.id || uuid()
const session = context.driver.session() const session = driver.session()
try {
const createCommentCypher = ` const writeTxResultPromise = session.writeTransaction(async transaction => {
const createCommentTransactionResponse = await transaction.run(
`
MATCH (post:Post {id: $postId}) MATCH (post:Post {id: $postId})
MATCH (author:User {id: $userId}) MATCH (author:User {id: $userId})
WITH post, author WITH post, author
@ -23,15 +26,15 @@ export default {
SET comment.updatedAt = toString(datetime()) SET comment.updatedAt = toString(datetime())
MERGE (post)<-[:COMMENTS]-(comment)<-[:WROTE]-(author) MERGE (post)<-[:COMMENTS]-(comment)<-[:WROTE]-(author)
RETURN comment RETURN comment
` `,
const transactionRes = await session.run(createCommentCypher, { { userId: user.id, postId, params },
userId: context.user.id, )
postId, return createCommentTransactionResponse.records.map(
params, record => record.get('comment').properties,
)
}) })
try {
const [comment] = transactionRes.records.map(record => record.get('comment').properties) const [comment] = await writeTxResultPromise
return comment return comment
} finally { } finally {
session.close() session.close()
@ -39,15 +42,22 @@ export default {
}, },
UpdateComment: async (_parent, params, context, _resolveInfo) => { UpdateComment: async (_parent, params, context, _resolveInfo) => {
const session = context.driver.session() const session = context.driver.session()
try { const writeTxResultPromise = session.writeTransaction(async transaction => {
const updateCommentCypher = ` const updateCommentTransactionResponse = await transaction.run(
`
MATCH (comment:Comment {id: $params.id}) MATCH (comment:Comment {id: $params.id})
SET comment += $params SET comment += $params
SET comment.updatedAt = toString(datetime()) SET comment.updatedAt = toString(datetime())
RETURN comment RETURN comment
` `,
const transactionRes = await session.run(updateCommentCypher, { params }) { params },
const [comment] = transactionRes.records.map(record => record.get('comment').properties) )
return updateCommentTransactionResponse.records.map(
record => record.get('comment').properties,
)
})
try {
const [comment] = await writeTxResultPromise
return comment return comment
} finally { } finally {
session.close() session.close()
@ -55,8 +65,8 @@ export default {
}, },
DeleteComment: async (_parent, args, context, _resolveInfo) => { DeleteComment: async (_parent, args, context, _resolveInfo) => {
const session = context.driver.session() const session = context.driver.session()
try { const writeTxResultPromise = session.writeTransaction(async transaction => {
const transactionRes = await session.run( const deleteCommentTransactionResponse = await transaction.run(
` `
MATCH (comment:Comment {id: $commentId}) MATCH (comment:Comment {id: $commentId})
SET comment.deleted = TRUE SET comment.deleted = TRUE
@ -66,7 +76,12 @@ export default {
`, `,
{ commentId: args.id }, { commentId: args.id },
) )
const [comment] = transactionRes.records.map(record => record.get('comment').properties) return deleteCommentTransactionResponse.records.map(
record => record.get('comment').properties,
)
})
try {
const [comment] = await writeTxResultPromise
return comment return comment
} finally { } finally {
session.close() session.close()

View File

@ -2,7 +2,7 @@ import Factory from '../../seed/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 { neode as getNeode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../bootstrap/neo4j'
const driver = getDriver() const driver = getDriver()
const neode = getNeode() const neode = getNeode()
@ -10,7 +10,8 @@ const factory = Factory()
let variables, mutate, authenticatedUser, commentAuthor, newlyCreatedComment let variables, mutate, authenticatedUser, commentAuthor, newlyCreatedComment
beforeAll(() => { beforeAll(async () => {
await factory.cleanDatabase()
const { server } = createServer({ const { server } = createServer({
context: () => { context: () => {
return { return {
@ -19,8 +20,7 @@ beforeAll(() => {
} }
}, },
}) })
const client = createTestClient(server) mutate = createTestClient(server).mutate
mutate = client.mutate
}) })
beforeEach(async () => { beforeEach(async () => {
@ -100,6 +100,7 @@ describe('CreateComment', () => {
await expect(mutate({ mutation: createCommentMutation, variables })).resolves.toMatchObject( await expect(mutate({ mutation: createCommentMutation, variables })).resolves.toMatchObject(
{ {
data: { CreateComment: { content: "I'm authorised to comment" } }, data: { CreateComment: { content: "I'm authorised to comment" } },
errors: undefined,
}, },
) )
}) })
@ -108,6 +109,7 @@ describe('CreateComment', () => {
await expect(mutate({ mutation: createCommentMutation, variables })).resolves.toMatchObject( await expect(mutate({ mutation: createCommentMutation, variables })).resolves.toMatchObject(
{ {
data: { CreateComment: { author: { name: 'Author' } } }, data: { CreateComment: { author: { name: 'Author' } } },
errors: undefined,
}, },
) )
}) })
@ -157,6 +159,7 @@ describe('UpdateComment', () => {
it('updates the comment', async () => { it('updates the comment', async () => {
const expected = { const expected = {
data: { UpdateComment: { id: 'c456', content: 'The comment is updated' } }, data: { UpdateComment: { id: 'c456', content: 'The comment is updated' } },
errors: undefined,
} }
await expect(mutate({ mutation: updateCommentMutation, variables })).resolves.toMatchObject( await expect(mutate({ mutation: updateCommentMutation, variables })).resolves.toMatchObject(
expected, expected,
@ -172,6 +175,7 @@ describe('UpdateComment', () => {
createdAt: expect.any(String), createdAt: expect.any(String),
}, },
}, },
errors: undefined,
} }
await expect(mutate({ mutation: updateCommentMutation, variables })).resolves.toMatchObject( await expect(mutate({ mutation: updateCommentMutation, variables })).resolves.toMatchObject(
expected, expected,

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 '../../seed/factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../bootstrap/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 '../../seed/factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { getDriver, neode as getNeode } from '../../bootstrap/neo4j' import { getDriver, getNeode } from '../../bootstrap/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 { neode as getNeode } from '../../bootstrap/neo4j' import { getNeode } from '../../bootstrap/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 '../../seed/factories'
import { getDriver, neode as getNeode } from '../../bootstrap/neo4j' import { getDriver, getNeode } from '../../bootstrap/neo4j'
import createServer from '../../server' import createServer from '../../server'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'

View File

@ -1,9 +1,9 @@
import { neode } from '../../../bootstrap/neo4j' import log from './databaseLogger'
export const undefinedToNullResolver = list => { export const undefinedToNullResolver = list => {
const resolvers = {} const resolvers = {}
list.forEach(key => { list.forEach(key => {
resolvers[key] = async (parent, params, context, resolveInfo) => { resolvers[key] = async parent => {
return typeof parent[key] === 'undefined' ? null : parent[key] return typeof parent[key] === 'undefined' ? null : parent[key]
} }
}) })
@ -11,7 +11,6 @@ export const undefinedToNullResolver = list => {
} }
export default function Resolver(type, options = {}) { export default function Resolver(type, options = {}) {
const instance = neode()
const { const {
idAttribute = 'id', idAttribute = 'id',
undefinedToNull = [], undefinedToNull = [],
@ -22,32 +21,49 @@ export default function Resolver(type, options = {}) {
} = options } = options
const _hasResolver = (resolvers, { key, connection }, { returnType }) => { const _hasResolver = (resolvers, { key, connection }, { returnType }) => {
return async (parent, params, context, resolveInfo) => { return async (parent, params, { driver, cypherParams }, resolveInfo) => {
if (typeof parent[key] !== 'undefined') return parent[key] if (typeof parent[key] !== 'undefined') return parent[key]
const id = parent[idAttribute] const id = parent[idAttribute]
const statement = `MATCH(:${type} {${idAttribute}: {id}})${connection} RETURN related` const session = driver.session()
const result = await instance.cypher(statement, { id }) const readTxResultPromise = session.readTransaction(async txc => {
let response = result.records.map(r => r.get('related').properties) const cypher = `
MATCH(:${type} {${idAttribute}: $id})${connection}
RETURN related {.*} as related
`
const result = await txc.run(cypher, { id, cypherParams })
log(result)
return result.records.map(r => r.get('related'))
})
try {
let response = await readTxResultPromise
if (returnType === 'object') response = response[0] || null if (returnType === 'object') response = response[0] || null
return response return response
} finally {
session.close()
}
} }
} }
const booleanResolver = obj => { const booleanResolver = obj => {
const resolvers = {} const resolvers = {}
for (const [key, condition] of Object.entries(obj)) { for (const [key, condition] of Object.entries(obj)) {
resolvers[key] = async (parent, params, { cypherParams }, resolveInfo) => { resolvers[key] = async (parent, params, { cypherParams, driver }, resolveInfo) => {
if (typeof parent[key] !== 'undefined') return parent[key] if (typeof parent[key] !== 'undefined') return parent[key]
const result = await instance.cypher( const id = parent[idAttribute]
` const session = driver.session()
${condition.replace('this', 'this {id: $parent.id}')} as ${key}`, const readTxResultPromise = session.readTransaction(async txc => {
{ const nodeCondition = condition.replace('this', 'this {id: $id}')
parent, const cypher = `${nodeCondition} as ${key}`
cypherParams, const result = await txc.run(cypher, { id, cypherParams })
}, log(result)
) const [response] = result.records.map(r => r.get(key))
const [record] = result.records return response
return record.get(key) })
try {
return await readTxResultPromise
} finally {
session.close()
}
} }
} }
return resolvers return resolvers
@ -56,16 +72,25 @@ export default function Resolver(type, options = {}) {
const countResolver = obj => { const countResolver = obj => {
const resolvers = {} const resolvers = {}
for (const [key, connection] of Object.entries(obj)) { for (const [key, connection] of Object.entries(obj)) {
resolvers[key] = async (parent, params, context, resolveInfo) => { resolvers[key] = async (parent, params, { driver, cypherParams }, resolveInfo) => {
if (typeof parent[key] !== 'undefined') return parent[key] if (typeof parent[key] !== 'undefined') return parent[key]
const session = driver.session()
const readTxResultPromise = session.readTransaction(async txc => {
const id = parent[idAttribute] const id = parent[idAttribute]
const statement = ` const cypher = `
MATCH(u:${type} {${idAttribute}: {id}})${connection} MATCH(u:${type} {${idAttribute}: $id})${connection}
RETURN COUNT(DISTINCT(related)) as count RETURN COUNT(DISTINCT(related)) as count
` `
const result = await instance.cypher(statement, { id }) const result = await txc.run(cypher, { id, cypherParams })
log(result)
const [response] = result.records.map(r => r.get('count').toNumber()) const [response] = result.records.map(r => r.get('count').toNumber())
return response return response
})
try {
return await readTxResultPromise
} finally {
session.close()
}
} }
} }
return resolvers return resolvers

View File

@ -5,24 +5,29 @@ export default async function createPasswordReset(options) {
const normalizedEmail = normalizeEmail(email) const normalizedEmail = normalizeEmail(email)
const session = driver.session() const session = driver.session()
try { try {
const cypher = ` const createPasswordResetTxPromise = session.writeTransaction(async transaction => {
MATCH (u:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email:$email}) const createPasswordResetTransactionResponse = await transaction.run(
CREATE(pr:PasswordReset {nonce: $nonce, issuedAt: datetime($issuedAt), usedAt: NULL})
MERGE (u)-[:REQUESTED]->(pr)
RETURN e, pr, u
` `
const transactionRes = await session.run(cypher, { MATCH (user:User)-[:PRIMARY_EMAIL]->(email:EmailAddress {email:$email})
CREATE(passwordReset:PasswordReset {nonce: $nonce, issuedAt: datetime($issuedAt), usedAt: NULL})
MERGE (user)-[:REQUESTED]->(passwordReset)
RETURN email, passwordReset, user
`,
{
issuedAt: issuedAt.toISOString(), issuedAt: issuedAt.toISOString(),
nonce, nonce,
email: normalizedEmail, email: normalizedEmail,
}) },
const records = transactionRes.records.map(record => { )
const { email } = record.get('e').properties return createPasswordResetTransactionResponse.records.map(record => {
const { nonce } = record.get('pr').properties const { email } = record.get('email').properties
const { name } = record.get('u').properties const { nonce } = record.get('passwordReset').properties
const { name } = record.get('user').properties
return { email, nonce, name } return { email, nonce, name }
}) })
return records[0] || {} })
const [records] = await createPasswordResetTxPromise
return records || {}
} finally { } finally {
session.close() session.close()
} }

View File

@ -1,35 +0,0 @@
import createPasswordReset from './createPasswordReset'
describe('createPasswordReset', () => {
const issuedAt = new Date()
const nonce = 'abcdef'
describe('email lookup', () => {
let driver
let mockSession
beforeEach(() => {
mockSession = {
close() {},
run: jest.fn().mockReturnValue({
records: {
map: jest.fn(() => []),
},
}),
}
driver = { session: () => mockSession }
})
it('lowercases email address', async () => {
const email = 'stRaNGeCaSiNG@ExAmplE.ORG'
await createPasswordReset({ driver, email, issuedAt, nonce })
expect(mockSession.run.mock.calls).toEqual([
[
expect.any(String),
expect.objectContaining({
email: 'strangecasing@example.org',
}),
],
])
})
})
})

View File

@ -0,0 +1,15 @@
import Debug from 'debug'
const debugCypher = Debug('human-connection:neo4j:cypher')
const debugStats = Debug('human-connection:neo4j:stats')
export default function log(response) {
const { statement, counters, resultConsumedAfter, resultAvailableAfter } = response.summary
const { text, parameters } = statement
debugCypher('%s', text)
debugCypher('%o', parameters)
debugStats('%o', counters)
debugStats('%o', {
resultConsumedAfter: resultConsumedAfter.toNumber(),
resultAvailableAfter: resultAvailableAfter.toNumber(),
})
}

View File

@ -1,25 +1,29 @@
import { UserInputError } from 'apollo-server' import { UserInputError } from 'apollo-server'
export default async function alreadyExistingMail({ args, context }) { export default async function alreadyExistingMail({ args, context }) {
const cypher = ` const session = context.driver.session()
try {
const existingEmailAddressTxPromise = session.writeTransaction(async transaction => {
const existingEmailAddressTransactionResponse = await transaction.run(
`
MATCH (email:EmailAddress {email: $email}) MATCH (email:EmailAddress {email: $email})
OPTIONAL MATCH (email)-[:BELONGS_TO]-(user) OPTIONAL MATCH (email)-[:BELONGS_TO]-(user)
RETURN email, user RETURN email, user
` `,
let transactionRes { email: args.email },
const session = context.driver.session() )
try { return existingEmailAddressTransactionResponse.records.map(record => {
transactionRes = await session.run(cypher, { email: args.email })
} finally {
session.close()
}
const [result] = transactionRes.records.map(record => {
return { return {
alreadyExistingEmail: record.get('email').properties, alreadyExistingEmail: record.get('email').properties,
user: record.get('user') && record.get('user').properties, user: record.get('user') && record.get('user').properties,
} }
}) })
const { alreadyExistingEmail, user } = result || {} })
const [emailBelongsToUser] = await existingEmailAddressTxPromise
const { alreadyExistingEmail, user } = emailBelongsToUser || {}
if (user) throw new UserInputError('A user account with this email already exists.') if (user) throw new UserInputError('A user account with this email already exists.')
return alreadyExistingEmail return alreadyExistingEmail
} finally {
session.close()
}
} }

View File

@ -1,6 +1,6 @@
import Factory from '../../seed/factories' import Factory from '../../seed/factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../bootstrap/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 '../../seed/factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../bootstrap/neo4j'
import createServer from '../../server' import createServer from '../../server'
const factory = Factory() const factory = Factory()

View File

@ -1,3 +1,5 @@
import log from './helpers/databaseLogger'
const resourceTypes = ['Post', 'Comment'] const resourceTypes = ['Post', 'Comment']
const transformReturnType = record => { const transformReturnType = record => {
@ -42,16 +44,29 @@ export default {
} }
const offset = args.offset && typeof args.offset === 'number' ? `SKIP ${args.offset}` : '' const offset = args.offset && typeof args.offset === 'number' ? `SKIP ${args.offset}` : ''
const limit = args.first && typeof args.first === 'number' ? `LIMIT ${args.first}` : '' const limit = args.first && typeof args.first === 'number' ? `LIMIT ${args.first}` : ''
const cypher = `
const readTxResultPromise = session.readTransaction(async transaction => {
const notificationsTransactionResponse = await transaction.run(
`
MATCH (resource {deleted: false, disabled: false})-[notification:NOTIFIED]->(user:User {id:$id}) MATCH (resource {deleted: false, disabled: false})-[notification:NOTIFIED]->(user:User {id:$id})
${whereClause} ${whereClause}
RETURN resource, notification, user WITH user, notification, resource,
[(resource)<-[:WROTE]-(author:User) | author {.*}] as authors,
[(resource)-[:COMMENTS]->(post:Post)<-[:WROTE]-(author:User) | post{.*, author: properties(author)} ] as posts
WITH resource, user, notification, authors, posts,
resource {.*, __typename: labels(resource)[0], author: authors[0], post: posts[0]} as finalResource
RETURN notification {.*, from: finalResource, to: properties(user)}
${orderByClause} ${orderByClause}
${offset} ${limit} ${offset} ${limit}
` `,
{ id: currentUser.id },
)
log(notificationsTransactionResponse)
return notificationsTransactionResponse.records.map(record => record.get('notification'))
})
try { try {
const result = await session.run(cypher, { id: currentUser.id }) const notifications = await readTxResultPromise
return result.records.map(transformReturnType) return notifications
} finally { } finally {
session.close() session.close()
} }
@ -61,15 +76,21 @@ export default {
markAsRead: async (parent, args, context, resolveInfo) => { markAsRead: async (parent, args, context, resolveInfo) => {
const { user: currentUser } = context const { user: currentUser } = context
const session = context.driver.session() const session = context.driver.session()
try { const writeTxResultPromise = session.writeTransaction(async transaction => {
const cypher = ` const markNotificationAsReadTransactionResponse = await transaction.run(
`
MATCH (resource {id: $resourceId})-[notification:NOTIFIED {read: FALSE}]->(user:User {id:$id}) MATCH (resource {id: $resourceId})-[notification:NOTIFIED {read: FALSE}]->(user:User {id:$id})
SET notification.read = TRUE SET notification.read = TRUE
RETURN resource, notification, user RETURN resource, notification, user
` `,
const result = await session.run(cypher, { resourceId: args.id, id: currentUser.id }) { resourceId: args.id, id: currentUser.id },
const notifications = await result.records.map(transformReturnType) )
return notifications[0] log(markNotificationAsReadTransactionResponse)
return markNotificationAsReadTransactionResponse.records.map(transformReturnType)
})
try {
const [notifications] = await writeTxResultPromise
return notifications
} finally { } finally {
session.close() session.close()
} }

View File

@ -184,6 +184,7 @@ describe('given some notifications', () => {
data: { data: {
notifications: expect.arrayContaining(expected), notifications: expect.arrayContaining(expected),
}, },
errors: undefined,
}) })
}) })
}) })
@ -233,7 +234,10 @@ describe('given some notifications', () => {
` `
await expect( await expect(
mutate({ mutation: deletePostMutation, variables: { id: 'p3' } }), mutate({ mutation: deletePostMutation, variables: { id: 'p3' } }),
).resolves.toMatchObject({ data: { DeletePost: { id: 'p3', deleted: true } } }) ).resolves.toMatchObject({
data: { DeletePost: { id: 'p3', deleted: true } },
errors: undefined,
})
authenticatedUser = await user.toJson() authenticatedUser = await user.toJson()
} }
@ -242,11 +246,12 @@ describe('given some notifications', () => {
query({ query: notificationQuery, variables: { ...variables, read: false } }), query({ query: notificationQuery, variables: { ...variables, read: false } }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
data: { notifications: [expect.any(Object), expect.any(Object)] }, data: { notifications: [expect.any(Object), expect.any(Object)] },
errors: undefined,
}) })
await deletePostAction() await deletePostAction()
await expect( await expect(
query({ query: notificationQuery, variables: { ...variables, read: false } }), query({ query: notificationQuery, variables: { ...variables, read: false } }),
).resolves.toMatchObject({ data: { notifications: [] } }) ).resolves.toMatchObject({ data: { notifications: [] }, errors: undefined })
}) })
}) })
}) })

View File

@ -12,25 +12,29 @@ export default {
const stillValid = new Date() const stillValid = new Date()
stillValid.setDate(stillValid.getDate() - 1) stillValid.setDate(stillValid.getDate() - 1)
const encryptedNewPassword = await bcrypt.hashSync(newPassword, 10) const encryptedNewPassword = await bcrypt.hashSync(newPassword, 10)
const cypher = `
MATCH (pr:PasswordReset {nonce: $nonce})
MATCH (e:EmailAddress {email: $email})<-[:PRIMARY_EMAIL]-(u:User)-[:REQUESTED]->(pr)
WHERE duration.between(pr.issuedAt, datetime()).days <= 0 AND pr.usedAt IS NULL
SET pr.usedAt = datetime()
SET u.encryptedPassword = $encryptedNewPassword
RETURN pr
`
const session = driver.session() const session = driver.session()
try { try {
const transactionRes = await session.run(cypher, { const passwordResetTxPromise = session.writeTransaction(async transaction => {
const passwordResetTransactionResponse = await transaction.run(
`
MATCH (passwordReset:PasswordReset {nonce: $nonce})
MATCH (email:EmailAddress {email: $email})<-[:PRIMARY_EMAIL]-(user:User)-[:REQUESTED]->(passwordReset)
WHERE duration.between(passwordReset.issuedAt, datetime()).days <= 0 AND passwordReset.usedAt IS NULL
SET passwordReset.usedAt = datetime()
SET user.encryptedPassword = $encryptedNewPassword
RETURN passwordReset
`,
{
stillValid, stillValid,
email, email,
nonce, nonce,
encryptedNewPassword, encryptedNewPassword,
},
)
return passwordResetTransactionResponse.records.map(record => record.get('passwordReset'))
}) })
const [reset] = transactionRes.records.map(record => record.get('pr')) const [reset] = await passwordResetTxPromise
const response = !!(reset && reset.properties.usedAt) return !!(reset && reset.properties.usedAt)
return response
} finally { } finally {
session.close() session.close()
} }

View File

@ -1,6 +1,6 @@
import Factory from '../../seed/factories' import Factory from '../../seed/factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../bootstrap/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'
@ -14,14 +14,11 @@ let authenticatedUser
let variables let variables
const getAllPasswordResets = async () => { const getAllPasswordResets = async () => {
const session = driver.session() const passwordResetQuery = await neode.cypher(
try { 'MATCH (passwordReset:PasswordReset) RETURN passwordReset',
const transactionRes = await session.run('MATCH (r:PasswordReset) RETURN r') )
const resets = transactionRes.records.map(record => record.get('r')) const resets = passwordResetQuery.records.map(record => record.get('passwordReset'))
return resets return resets
} finally {
session.close()
}
} }
beforeEach(() => { beforeEach(() => {

View File

@ -57,17 +57,20 @@ export default {
PostsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => { PostsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => {
const { postId, data } = params const { postId, data } = params
const session = context.driver.session() const session = context.driver.session()
try { const readTxResultPromise = session.readTransaction(async transaction => {
const transactionRes = await session.run( const emotionsCountTransactionResponse = await transaction.run(
`MATCH (post:Post {id: $postId})<-[emoted:EMOTED {emotion: $data.emotion}]-() `
MATCH (post:Post {id: $postId})<-[emoted:EMOTED {emotion: $data.emotion}]-()
RETURN COUNT(DISTINCT emoted) as emotionsCount RETURN COUNT(DISTINCT emoted) as emotionsCount
`, `,
{ postId, data }, { postId, data },
) )
return emotionsCountTransactionResponse.records.map(
const [emotionsCount] = transactionRes.records.map(record => { record => record.get('emotionsCount').low,
return record.get('emotionsCount').low )
}) })
try {
const [emotionsCount] = await readTxResultPromise
return emotionsCount return emotionsCount
} finally { } finally {
session.close() session.close()
@ -76,16 +79,18 @@ export default {
PostsEmotionsByCurrentUser: async (object, params, context, resolveInfo) => { PostsEmotionsByCurrentUser: async (object, params, context, resolveInfo) => {
const { postId } = params const { postId } = params
const session = context.driver.session() const session = context.driver.session()
try { const readTxResultPromise = session.readTransaction(async transaction => {
const transactionRes = await session.run( const emotionsTransactionResponse = await transaction.run(
`MATCH (user:User {id: $userId})-[emoted:EMOTED]->(post:Post {id: $postId}) `
RETURN collect(emoted.emotion) as emotion`, MATCH (user:User {id: $userId})-[emoted:EMOTED]->(post:Post {id: $postId})
RETURN collect(emoted.emotion) as emotion
`,
{ userId: context.user.id, postId }, { userId: context.user.id, postId },
) )
return emotionsTransactionResponse.records.map(record => record.get('emotion'))
const [emotions] = transactionRes.records.map(record => {
return record.get('emotion')
}) })
try {
const [emotions] = await readTxResultPromise
return emotions return emotions
} finally { } finally {
session.close() session.close()
@ -98,7 +103,11 @@ export default {
delete params.categoryIds delete params.categoryIds
params = await fileUpload(params, { file: 'imageUpload', url: 'image' }) params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
params.id = params.id || uuid() params.id = params.id || uuid()
const createPostCypher = `CREATE (post:Post {params}) const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async transaction => {
const createPostTransactionResponse = await transaction.run(
`
CREATE (post:Post {params})
SET post.createdAt = toString(datetime()) SET post.createdAt = toString(datetime())
SET post.updatedAt = toString(datetime()) SET post.updatedAt = toString(datetime())
WITH post WITH post
@ -108,15 +117,15 @@ export default {
UNWIND $categoryIds AS categoryId UNWIND $categoryIds AS categoryId
MATCH (category:Category {id: categoryId}) MATCH (category:Category {id: categoryId})
MERGE (post)-[:CATEGORIZED]->(category) MERGE (post)-[:CATEGORIZED]->(category)
RETURN post` RETURN post
`,
const createPostVariables = { userId: context.user.id, categoryIds, params } { userId: context.user.id, categoryIds, params },
)
const session = context.driver.session() return createPostTransactionResponse.records.map(record => record.get('post').properties)
})
try { try {
const transactionRes = await session.run(createPostCypher, createPostVariables) const [post] = await writeTxResultPromise
const posts = transactionRes.records.map(record => record.get('post').properties) return post
return posts[0]
} catch (e) { } catch (e) {
if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed') if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
throw new UserInputError('Post with this slug already exists!') throw new UserInputError('Post with this slug already exists!')
@ -129,14 +138,14 @@ export default {
const { categoryIds } = params const { categoryIds } = params
delete params.categoryIds delete params.categoryIds
params = await fileUpload(params, { file: 'imageUpload', url: 'image' }) params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
let updatePostCypher = `MATCH (post:Post {id: $params.id}) const session = context.driver.session()
let updatePostCypher = `
MATCH (post:Post {id: $params.id})
SET post += $params SET post += $params
SET post.updatedAt = toString(datetime()) SET post.updatedAt = toString(datetime())
WITH post WITH post
` `
const session = context.driver.session()
try {
if (categoryIds && categoryIds.length) { if (categoryIds && categoryIds.length) {
const cypherDeletePreviousRelations = ` const cypherDeletePreviousRelations = `
MATCH (post:Post { id: $params.id })-[previousRelations:CATEGORIZED]->(category:Category) MATCH (post:Post { id: $params.id })-[previousRelations:CATEGORIZED]->(category:Category)
@ -144,7 +153,9 @@ export default {
RETURN post, category RETURN post, category
` `
await session.run(cypherDeletePreviousRelations, { params }) await session.writeTransaction(transaction => {
return transaction.run(cypherDeletePreviousRelations, { params })
})
updatePostCypher += ` updatePostCypher += `
UNWIND $categoryIds AS categoryId UNWIND $categoryIds AS categoryId
@ -156,11 +167,15 @@ export default {
updatePostCypher += `RETURN post` updatePostCypher += `RETURN post`
const updatePostVariables = { categoryIds, params } const updatePostVariables = { categoryIds, params }
try {
const transactionRes = await session.run(updatePostCypher, updatePostVariables) const writeTxResultPromise = session.writeTransaction(async transaction => {
const [post] = transactionRes.records.map(record => { const updatePostTransactionResponse = await transaction.run(
return record.get('post').properties updatePostCypher,
updatePostVariables,
)
return updatePostTransactionResponse.records.map(record => record.get('post').properties)
}) })
const [post] = await writeTxResultPromise
return post return post
} finally { } finally {
session.close() session.close()
@ -169,9 +184,8 @@ export default {
DeletePost: async (object, args, context, resolveInfo) => { DeletePost: async (object, args, context, resolveInfo) => {
const session = context.driver.session() const session = context.driver.session()
try { const writeTxResultPromise = session.writeTransaction(async transaction => {
// we cannot set slug to 'UNAVAILABE' because of unique constraints const deletePostTransactionResponse = await transaction.run(
const transactionRes = await session.run(
` `
MATCH (post:Post {id: $postId}) MATCH (post:Post {id: $postId})
OPTIONAL MATCH (post)<-[:COMMENTS]-(comment:Comment) OPTIONAL MATCH (post)<-[:COMMENTS]-(comment:Comment)
@ -185,7 +199,10 @@ export default {
`, `,
{ postId: args.id }, { postId: args.id },
) )
const [post] = transactionRes.records.map(record => record.get('post').properties) return deletePostTransactionResponse.records.map(record => record.get('post').properties)
})
try {
const [post] = await writeTxResultPromise
return post return post
} finally { } finally {
session.close() session.close()
@ -195,21 +212,24 @@ export default {
const { to, data } = params const { to, data } = params
const { user } = context const { user } = context
const session = context.driver.session() const session = context.driver.session()
try { const writeTxResultPromise = session.writeTransaction(async transaction => {
const transactionRes = await session.run( const addPostEmotionsTransactionResponse = await transaction.run(
`MATCH (userFrom:User {id: $user.id}), (postTo:Post {id: $to.id}) `
MATCH (userFrom:User {id: $user.id}), (postTo:Post {id: $to.id})
MERGE (userFrom)-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo) MERGE (userFrom)-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo)
RETURN userFrom, postTo, emotedRelation`, RETURN userFrom, postTo, emotedRelation`,
{ user, to, data }, { user, to, data },
) )
return addPostEmotionsTransactionResponse.records.map(record => {
const [emoted] = transactionRes.records.map(record => {
return { return {
from: { ...record.get('userFrom').properties }, from: { ...record.get('userFrom').properties },
to: { ...record.get('postTo').properties }, to: { ...record.get('postTo').properties },
...record.get('emotedRelation').properties, ...record.get('emotedRelation').properties,
} }
}) })
})
try {
const [emoted] = await writeTxResultPromise
return emoted return emoted
} finally { } finally {
session.close() session.close()
@ -219,20 +239,25 @@ export default {
const { to, data } = params const { to, data } = params
const { id: from } = context.user const { id: from } = context.user
const session = context.driver.session() const session = context.driver.session()
try { const writeTxResultPromise = session.writeTransaction(async transaction => {
const transactionRes = await session.run( const removePostEmotionsTransactionResponse = await transaction.run(
`MATCH (userFrom:User {id: $from})-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo:Post {id: $to.id}) `
MATCH (userFrom:User {id: $from})-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo:Post {id: $to.id})
DELETE emotedRelation DELETE emotedRelation
RETURN userFrom, postTo`, RETURN userFrom, postTo
`,
{ from, to, data }, { from, to, data },
) )
const [emoted] = transactionRes.records.map(record => { return removePostEmotionsTransactionResponse.records.map(record => {
return { return {
from: { ...record.get('userFrom').properties }, from: { ...record.get('userFrom').properties },
to: { ...record.get('postTo').properties }, to: { ...record.get('postTo').properties },
emotion: data.emotion, emotion: data.emotion,
} }
}) })
})
try {
const [emoted] = await writeTxResultPromise
return emoted return emoted
} finally { } finally {
session.close() session.close()
@ -344,21 +369,28 @@ export default {
relatedContributions: async (parent, params, context, resolveInfo) => { relatedContributions: async (parent, params, context, resolveInfo) => {
if (typeof parent.relatedContributions !== 'undefined') return parent.relatedContributions if (typeof parent.relatedContributions !== 'undefined') return parent.relatedContributions
const { id } = parent const { id } = parent
const statement = ` const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async transaction => {
const relatedContributionsTransactionResponse = await transaction.run(
`
MATCH (p:Post {id: $id})-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post) MATCH (p:Post {id: $id})-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post)
WHERE NOT post.deleted AND NOT post.disabled WHERE NOT post.deleted AND NOT post.disabled
RETURN DISTINCT post RETURN DISTINCT post
LIMIT 10 LIMIT 10
` `,
let relatedContributions { id },
const session = context.driver.session() )
return relatedContributionsTransactionResponse.records.map(
record => record.get('post').properties,
)
})
try { try {
const result = await session.run(statement, { id }) const relatedContributions = await writeTxResultPromise
relatedContributions = result.records.map(r => r.get('post').properties) return relatedContributions
} finally { } finally {
session.close() session.close()
} }
return relatedContributions
}, },
}, },
} }

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 '../../seed/factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../bootstrap/neo4j'
import createServer from '../../server' import createServer from '../../server'
const driver = getDriver() const driver = getDriver()
@ -383,7 +383,10 @@ describe('UpdatePost', () => {
}) })
it('updates a post', async () => { it('updates a post', async () => {
const expected = { data: { UpdatePost: { id: 'p9876', content: 'New content' } } } const expected = {
data: { UpdatePost: { id: 'p9876', content: 'New content' } },
errors: undefined,
}
await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject( await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject(
expected, expected,
) )
@ -394,6 +397,7 @@ describe('UpdatePost', () => {
data: { data: {
UpdatePost: { id: 'p9876', content: 'New content', createdAt: expect.any(String) }, UpdatePost: { id: 'p9876', content: 'New content', createdAt: expect.any(String) },
}, },
errors: undefined,
} }
await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject( await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject(
expected, expected,
@ -421,6 +425,7 @@ describe('UpdatePost', () => {
categories: expect.arrayContaining([{ id: 'cat9' }, { id: 'cat4' }, { id: 'cat15' }]), categories: expect.arrayContaining([{ id: 'cat9' }, { id: 'cat4' }, { id: 'cat15' }]),
}, },
}, },
errors: undefined,
} }
await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject( await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject(
expected, expected,
@ -441,6 +446,7 @@ describe('UpdatePost', () => {
categories: expect.arrayContaining([{ id: 'cat27' }]), categories: expect.arrayContaining([{ id: 'cat27' }]),
}, },
}, },
errors: undefined,
} }
await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject( await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject(
expected, expected,
@ -722,6 +728,7 @@ describe('UpdatePost', () => {
}, },
], ],
}, },
errors: undefined,
} }
variables = { orderBy: ['pinned_desc', 'createdAt_desc'] } variables = { orderBy: ['pinned_desc', 'createdAt_desc'] }
await expect(query({ query: postOrderingQuery, variables })).resolves.toMatchObject( await expect(query({ query: postOrderingQuery, variables })).resolves.toMatchObject(

View File

@ -1,12 +1,12 @@
import { UserInputError } from 'apollo-server' import { UserInputError } from 'apollo-server'
import { neode } from '../../bootstrap/neo4j' import { getNeode } from '../../bootstrap/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'
import existingEmailAddress from './helpers/existingEmailAddress' import existingEmailAddress from './helpers/existingEmailAddress'
import normalizeEmail from './helpers/normalizeEmail' import normalizeEmail from './helpers/normalizeEmail'
const instance = neode() const neode = getNeode()
export default { export default {
Mutation: { Mutation: {
@ -16,7 +16,7 @@ export default {
let emailAddress = await existingEmailAddress({ args, context }) let emailAddress = await existingEmailAddress({ args, context })
if (emailAddress) return emailAddress if (emailAddress) return emailAddress
try { try {
emailAddress = await instance.create('EmailAddress', args) emailAddress = await neode.create('EmailAddress', args)
return emailAddress.toJson() return emailAddress.toJson()
} catch (e) { } catch (e) {
throw new UserInputError(e.message) throw new UserInputError(e.message)
@ -32,7 +32,7 @@ export default {
let { nonce, email } = args let { nonce, email } = args
email = normalizeEmail(email) email = normalizeEmail(email)
const result = await instance.cypher( const result = await neode.cypher(
` `
MATCH(email:EmailAddress {nonce: {nonce}, email: {email}}) MATCH(email:EmailAddress {nonce: {nonce}, email: {email}})
WHERE NOT (email)-[:BELONGS_TO]->() WHERE NOT (email)-[:BELONGS_TO]->()
@ -40,12 +40,12 @@ export default {
`, `,
{ nonce, email }, { nonce, email },
) )
const emailAddress = await instance.hydrateFirst(result, 'email', instance.model('Email')) const emailAddress = await neode.hydrateFirst(result, 'email', neode.model('EmailAddress'))
if (!emailAddress) throw new UserInputError('Invalid email or nonce') if (!emailAddress) throw new UserInputError('Invalid email or nonce')
args = await fileUpload(args, { file: 'avatarUpload', url: 'avatar' }) args = await fileUpload(args, { file: 'avatarUpload', url: 'avatar' })
args = await encryptPassword(args) args = await encryptPassword(args)
try { try {
const user = await instance.create('User', args) const user = await neode.create('User', args)
await Promise.all([ await Promise.all([
user.relateTo(emailAddress, 'primaryEmail'), user.relateTo(emailAddress, 'primaryEmail'),
emailAddress.relateTo(user, 'belongsTo'), emailAddress.relateTo(user, 'belongsTo'),

View File

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

View File

@ -1,3 +1,5 @@
import log from './helpers/databaseLogger'
const transformReturnType = record => { const transformReturnType = record => {
return { return {
...record.get('report').properties, ...record.get('report').properties,
@ -11,12 +13,11 @@ const transformReturnType = record => {
export default { export default {
Mutation: { Mutation: {
fileReport: async (_parent, params, context, _resolveInfo) => { fileReport: async (_parent, params, context, _resolveInfo) => {
let createdRelationshipWithNestedAttributes
const { resourceId, reasonCategory, reasonDescription } = params const { resourceId, reasonCategory, reasonDescription } = params
const { driver, user } = context const { driver, user } = context
const session = driver.session() const session = driver.session()
const reportWriteTxResultPromise = session.writeTransaction(async txc => { const reportWriteTxResultPromise = session.writeTransaction(async transaction => {
const reportTransactionResponse = await txc.run( const reportTransactionResponse = await transaction.run(
` `
MATCH (submitter:User {id: $submitterId}) MATCH (submitter:User {id: $submitterId})
MATCH (resource {id: $resourceId}) MATCH (resource {id: $resourceId})
@ -36,23 +37,23 @@ export default {
reasonDescription, reasonDescription,
}, },
) )
log(reportTransactionResponse)
return reportTransactionResponse.records.map(transformReturnType) return reportTransactionResponse.records.map(transformReturnType)
}) })
try { try {
const txResult = await reportWriteTxResultPromise const [createdRelationshipWithNestedAttributes] = await reportWriteTxResultPromise
if (!txResult[0]) return null if (!createdRelationshipWithNestedAttributes) return null
createdRelationshipWithNestedAttributes = txResult[0] return createdRelationshipWithNestedAttributes
} finally { } finally {
session.close() session.close()
} }
return createdRelationshipWithNestedAttributes
}, },
}, },
Query: { Query: {
reports: async (_parent, params, context, _resolveInfo) => { reports: async (_parent, params, context, _resolveInfo) => {
const { driver } = context const { driver } = context
const session = driver.session() const session = driver.session()
let reports, orderByClause, filterClause let orderByClause, filterClause
switch (params.orderBy) { switch (params.orderBy) {
case 'createdAt_asc': case 'createdAt_asc':
orderByClause = 'ORDER BY report.createdAt ASC' orderByClause = 'ORDER BY report.createdAt ASC'
@ -81,8 +82,8 @@ export default {
params.offset && typeof params.offset === 'number' ? `SKIP ${params.offset}` : '' params.offset && typeof params.offset === 'number' ? `SKIP ${params.offset}` : ''
const limit = params.first && typeof params.first === 'number' ? `LIMIT ${params.first}` : '' const limit = params.first && typeof params.first === 'number' ? `LIMIT ${params.first}` : ''
const reportReadTxPromise = session.readTransaction(async tx => { const reportReadTxPromise = session.readTransaction(async transaction => {
const allReportsTransactionResponse = await tx.run( const allReportsTransactionResponse = await transaction.run(
` `
MATCH (report:Report)-[:BELONGS_TO]->(resource) MATCH (report:Report)-[:BELONGS_TO]->(resource)
WHERE (resource:User OR resource:Post OR resource:Comment) WHERE (resource:User OR resource:Post OR resource:Comment)
@ -100,16 +101,15 @@ export default {
${offset} ${limit} ${offset} ${limit}
`, `,
) )
log(allReportsTransactionResponse)
return allReportsTransactionResponse.records.map(record => record.get('report')) return allReportsTransactionResponse.records.map(record => record.get('report'))
}) })
try { try {
const txResult = await reportReadTxPromise const reports = await reportReadTxPromise
if (!txResult[0]) return null return reports
reports = txResult
} finally { } finally {
session.close() session.close()
} }
return reports
}, },
}, },
Report: { Report: {
@ -118,23 +118,23 @@ export default {
const session = context.driver.session() const session = context.driver.session()
const { id } = parent const { id } = parent
let filed let filed
const readTxPromise = session.readTransaction(async tx => { const readTxPromise = session.readTransaction(async transaction => {
const allReportsTransactionResponse = await tx.run( const filedReportsTransactionResponse = await transaction.run(
` `
MATCH (submitter:User)-[filed:FILED]->(report:Report {id: $id}) MATCH (submitter:User)-[filed:FILED]->(report:Report {id: $id})
RETURN filed, submitter RETURN filed, submitter
`, `,
{ id }, { id },
) )
return allReportsTransactionResponse.records.map(record => ({ log(filedReportsTransactionResponse)
return filedReportsTransactionResponse.records.map(record => ({
submitter: record.get('submitter').properties, submitter: record.get('submitter').properties,
filed: record.get('filed').properties, filed: record.get('filed').properties,
})) }))
}) })
try { try {
const txResult = await readTxPromise const filedReports = await readTxPromise
if (!txResult[0]) return null filed = filedReports.map(reportedRecord => {
filed = txResult.map(reportedRecord => {
const { submitter, filed } = reportedRecord const { submitter, filed } = reportedRecord
const relationshipWithNestedAttributes = { const relationshipWithNestedAttributes = {
...filed, ...filed,
@ -152,8 +152,8 @@ export default {
const session = context.driver.session() const session = context.driver.session()
const { id } = parent const { id } = parent
let reviewed let reviewed
const readTxPromise = session.readTransaction(async tx => { const readTxPromise = session.readTransaction(async transaction => {
const allReportsTransactionResponse = await tx.run( const reviewedReportsTransactionResponse = await transaction.run(
` `
MATCH (resource)<-[:BELONGS_TO]-(report:Report {id: $id})<-[review:REVIEWED]-(moderator:User) MATCH (resource)<-[:BELONGS_TO]-(report:Report {id: $id})<-[review:REVIEWED]-(moderator:User)
RETURN moderator, review RETURN moderator, review
@ -161,14 +161,15 @@ export default {
`, `,
{ id }, { id },
) )
return allReportsTransactionResponse.records.map(record => ({ log(reviewedReportsTransactionResponse)
return reviewedReportsTransactionResponse.records.map(record => ({
review: record.get('review').properties, review: record.get('review').properties,
moderator: record.get('moderator').properties, moderator: record.get('moderator').properties,
})) }))
}) })
try { try {
const txResult = await readTxPromise const reviewedReports = await readTxPromise
reviewed = txResult.map(reportedRecord => { reviewed = reviewedReports.map(reportedRecord => {
const { review, moderator } = reportedRecord const { review, moderator } = reportedRecord
const relationshipWithNestedAttributes = { const relationshipWithNestedAttributes = {
...review, ...review,

View File

@ -2,7 +2,7 @@ import { createTestClient } from 'apollo-server-testing'
import createServer from '../.././server' import createServer from '../.././server'
import Factory from '../../seed/factories' import Factory from '../../seed/factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { getDriver, neode as getNeode } from '../../bootstrap/neo4j' import { getDriver, getNeode } from '../../bootstrap/neo4j'
const factory = Factory() const factory = Factory()
const instance = getNeode() const instance = getNeode()
@ -21,7 +21,6 @@ describe('file a report on a resource', () => {
id id
createdAt createdAt
updatedAt updatedAt
disable
closed closed
rule rule
resource { resource {
@ -489,7 +488,6 @@ describe('file a report on a resource', () => {
id id
createdAt createdAt
updatedAt updatedAt
disable
closed closed
resource { resource {
__typename __typename
@ -624,7 +622,6 @@ describe('file a report on a resource', () => {
id: expect.any(String), id: expect.any(String),
createdAt: expect.any(String), createdAt: expect.any(String),
updatedAt: expect.any(String), updatedAt: expect.any(String),
disable: false,
closed: false, closed: false,
resource: { resource: {
__typename: 'User', __typename: 'User',
@ -645,7 +642,6 @@ describe('file a report on a resource', () => {
id: expect.any(String), id: expect.any(String),
createdAt: expect.any(String), createdAt: expect.any(String),
updatedAt: expect.any(String), updatedAt: expect.any(String),
disable: false,
closed: false, closed: false,
resource: { resource: {
__typename: 'Post', __typename: 'Post',
@ -666,7 +662,6 @@ describe('file a report on a resource', () => {
id: expect.any(String), id: expect.any(String),
createdAt: expect.any(String), createdAt: expect.any(String),
updatedAt: expect.any(String), updatedAt: expect.any(String),
disable: false,
closed: false, closed: false,
resource: { resource: {
__typename: 'Comment', __typename: 'Comment',

View File

@ -1,11 +1,11 @@
import { neode } from '../../bootstrap/neo4j' import { getNeode } from '../../bootstrap/neo4j'
import { UserInputError } from 'apollo-server' import { UserInputError } from 'apollo-server'
const instance = neode() const neode = getNeode()
const getUserAndBadge = async ({ badgeKey, userId }) => { const getUserAndBadge = async ({ badgeKey, userId }) => {
const user = await instance.first('User', 'id', userId) const user = await neode.first('User', 'id', userId)
const badge = await instance.first('Badge', 'id', badgeKey) const badge = await neode.first('Badge', 'id', badgeKey)
if (!user) throw new UserInputError("Couldn't find a user with that id") if (!user) throw new UserInputError("Couldn't find a user with that id")
if (!badge) throw new UserInputError("Couldn't find a badge with that id") if (!badge) throw new UserInputError("Couldn't find a badge with that id")
return { user, badge } return { user, badge }
@ -24,8 +24,8 @@ export default {
const { user } = await getUserAndBadge(params) const { user } = await getUserAndBadge(params)
const session = context.driver.session() const session = context.driver.session()
try { try {
// silly neode cannot remove relationships await session.writeTransaction(transaction => {
await session.run( return transaction.run(
` `
MATCH (badge:Badge {id: $badgeKey})-[reward:REWARDED]->(rewardedUser:User {id: $userId}) MATCH (badge:Badge {id: $badgeKey})-[reward:REWARDED]->(rewardedUser:User {id: $userId})
DELETE reward DELETE reward
@ -36,6 +36,7 @@ export default {
userId, userId,
}, },
) )
})
} finally { } finally {
session.close() session.close()
} }

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 '../../seed/factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../bootstrap/neo4j'
import createServer from '../../server' import createServer from '../../server'
const factory = Factory() const factory = Factory()

View File

@ -1,3 +1,5 @@
import log from './helpers/databaseLogger'
export default { export default {
Mutation: { Mutation: {
shout: async (_object, params, context, _resolveInfo) => { shout: async (_object, params, context, _resolveInfo) => {
@ -5,22 +7,24 @@ export default {
const session = context.driver.session() const session = context.driver.session()
try { try {
const transactionRes = await session.run( const shoutWriteTxResultPromise = session.writeTransaction(async transaction => {
`MATCH (node {id: $id})<-[:WROTE]-(userWritten:User), (user:User {id: $userId}) const shoutTransactionResponse = await transaction.run(
`
MATCH (node {id: $id})<-[:WROTE]-(userWritten:User), (user:User {id: $userId})
WHERE $type IN labels(node) AND NOT userWritten.id = $userId WHERE $type IN labels(node) AND NOT userWritten.id = $userId
MERGE (user)-[relation:SHOUTED{createdAt:toString(datetime())}]->(node) MERGE (user)-[relation:SHOUTED{createdAt:toString(datetime())}]->(node)
RETURN COUNT(relation) > 0 as isShouted`, RETURN COUNT(relation) > 0 as isShouted
`,
{ {
id, id,
type, type,
userId: context.user.id, userId: context.user.id,
}, },
) )
log(shoutTransactionResponse)
const [isShouted] = transactionRes.records.map(record => { return shoutTransactionResponse.records.map(record => record.get('isShouted'))
return record.get('isShouted')
}) })
const [isShouted] = await shoutWriteTxResultPromise
return isShouted return isShouted
} finally { } finally {
session.close() session.close()
@ -31,20 +35,24 @@ export default {
const { id, type } = params const { id, type } = params
const session = context.driver.session() const session = context.driver.session()
try { try {
const transactionRes = await session.run( const unshoutWriteTxResultPromise = session.writeTransaction(async transaction => {
`MATCH (user:User {id: $userId})-[relation:SHOUTED]->(node {id: $id}) const unshoutTransactionResponse = await transaction.run(
`
MATCH (user:User {id: $userId})-[relation:SHOUTED]->(node {id: $id})
WHERE $type IN labels(node) WHERE $type IN labels(node)
DELETE relation DELETE relation
RETURN COUNT(relation) > 0 as isShouted`, RETURN COUNT(relation) > 0 as isShouted
`,
{ {
id, id,
type, type,
userId: context.user.id, userId: context.user.id,
}, },
) )
const [isShouted] = transactionRes.records.map(record => { log(unshoutTransactionResponse)
return record.get('isShouted') return unshoutTransactionResponse.records.map(record => record.get('isShouted'))
}) })
const [isShouted] = await unshoutWriteTxResultPromise
return isShouted return isShouted
} finally { } finally {
session.close() session.close()

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 '../../seed/factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../bootstrap/neo4j'
import createServer from '../../server' import createServer from '../../server'
let mutate, query, authenticatedUser, variables let mutate, query, authenticatedUser, variables

View File

@ -1,14 +1,14 @@
import { neode } from '../../bootstrap/neo4j' import { getNeode } from '../../bootstrap/neo4j'
import Resolver from './helpers/Resolver' import Resolver from './helpers/Resolver'
const instance = neode() const neode = getNeode()
export default { export default {
Mutation: { Mutation: {
CreateSocialMedia: async (object, params, context, resolveInfo) => { CreateSocialMedia: async (object, params, context, resolveInfo) => {
const [user, socialMedia] = await Promise.all([ const [user, socialMedia] = await Promise.all([
instance.find('User', context.user.id), neode.find('User', context.user.id),
instance.create('SocialMedia', params), neode.create('SocialMedia', params),
]) ])
await socialMedia.relateTo(user, 'ownedBy') await socialMedia.relateTo(user, 'ownedBy')
const response = await socialMedia.toJson() const response = await socialMedia.toJson()
@ -16,14 +16,14 @@ export default {
return response return response
}, },
UpdateSocialMedia: async (object, params, context, resolveInfo) => { UpdateSocialMedia: async (object, params, context, resolveInfo) => {
const socialMedia = await instance.find('SocialMedia', params.id) const socialMedia = await neode.find('SocialMedia', params.id)
await socialMedia.update({ url: params.url }) await socialMedia.update({ url: params.url })
const response = await socialMedia.toJson() const response = await socialMedia.toJson()
return response return response
}, },
DeleteSocialMedia: async (object, { id }, context, resolveInfo) => { DeleteSocialMedia: async (object, { id }, context, resolveInfo) => {
const socialMedia = await instance.find('SocialMedia', id) const socialMedia = await neode.find('SocialMedia', id)
if (!socialMedia) return null if (!socialMedia) return null
await socialMedia.delete() await socialMedia.delete()
return socialMedia.toJson() return socialMedia.toJson()

View File

@ -2,11 +2,11 @@ import { createTestClient } from 'apollo-server-testing'
import createServer from '../../server' import createServer from '../../server'
import Factory from '../../seed/factories' import Factory from '../../seed/factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { neode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../bootstrap/neo4j'
const driver = getDriver() const driver = getDriver()
const factory = Factory() const factory = Factory()
const instance = neode() const neode = getNeode()
describe('SocialMedia', () => { describe('SocialMedia', () => {
let socialMediaAction, someUser, ownerNode, owner let socialMediaAction, someUser, ownerNode, owner
@ -27,15 +27,15 @@ describe('SocialMedia', () => {
const newUrl = 'https://twitter.com/bullerby' const newUrl = 'https://twitter.com/bullerby'
const setUpSocialMedia = async () => { const setUpSocialMedia = async () => {
const socialMediaNode = await instance.create('SocialMedia', { url }) const socialMediaNode = await neode.create('SocialMedia', { url })
await socialMediaNode.relateTo(ownerNode, 'ownedBy') await socialMediaNode.relateTo(ownerNode, 'ownedBy')
return socialMediaNode.toJson() return socialMediaNode.toJson()
} }
beforeEach(async () => { beforeEach(async () => {
const someUserNode = await instance.create('User', userParams) const someUserNode = await neode.create('User', userParams)
someUser = await someUserNode.toJson() someUser = await someUserNode.toJson()
ownerNode = await instance.create('User', ownerParams) ownerNode = await neode.create('User', ownerParams)
owner = await ownerNode.toJson() owner = await ownerNode.toJson()
socialMediaAction = async (user, mutation, variables) => { socialMediaAction = async (user, mutation, variables) => {

View File

@ -1,8 +1,10 @@
import log from './helpers/databaseLogger'
export default { export default {
Query: { Query: {
statistics: async (_parent, _args, { driver }) => { statistics: async (_parent, _args, { driver }) => {
const session = driver.session() const session = driver.session()
const response = {} const counts = {}
try { try {
const mapping = { const mapping = {
countUsers: 'User', countUsers: 'User',
@ -13,27 +15,28 @@ export default {
countFollows: 'FOLLOWS', countFollows: 'FOLLOWS',
countShouts: 'SHOUTED', countShouts: 'SHOUTED',
} }
const cypher = ` const statisticsReadTxResultPromise = session.readTransaction(async transaction => {
const statisticsTransactionResponse = await transaction.run(
`
CALL apoc.meta.stats() YIELD labels, relTypesCount CALL apoc.meta.stats() YIELD labels, relTypesCount
RETURN labels, relTypesCount RETURN labels, relTypesCount
` `,
const result = await session.run(cypher) )
const [statistics] = await result.records.map(record => { log(statisticsTransactionResponse)
return statisticsTransactionResponse.records.map(record => {
return { return {
...record.get('labels'), ...record.get('labels'),
...record.get('relTypesCount'), ...record.get('relTypesCount'),
} }
}) })
})
const [statistics] = await statisticsReadTxResultPromise
Object.keys(mapping).forEach(key => { Object.keys(mapping).forEach(key => {
const stat = statistics[mapping[key]] const stat = statistics[mapping[key]]
response[key] = stat ? stat.toNumber() : 0 counts[key] = stat ? stat.toNumber() : 0
}) })
counts.countInvites = counts.countEmails - counts.countUsers
/* return counts
* Note: invites count is calculated this way because invitation codes are not in use yet
*/
response.countInvites = response.countEmails - response.countUsers
return response
} finally { } finally {
session.close() session.close()
} }

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 '../../seed/factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../bootstrap/neo4j'
import createServer from '../../server' import createServer from '../../server'
let query, authenticatedUser let query, authenticatedUser

View File

@ -1,10 +1,11 @@
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 { neode } from '../../bootstrap/neo4j' import { getNeode } from '../../bootstrap/neo4j'
import normalizeEmail from './helpers/normalizeEmail' import normalizeEmail from './helpers/normalizeEmail'
import log from './helpers/databaseLogger'
const instance = neode() const neode = getNeode()
export default { export default {
Query: { Query: {
@ -13,7 +14,7 @@ export default {
}, },
currentUser: async (object, params, ctx, resolveInfo) => { currentUser: async (object, params, ctx, resolveInfo) => {
if (!ctx.user) return null if (!ctx.user) return null
const user = await instance.find('User', ctx.user.id) const user = await neode.find('User', ctx.user.id)
return user.toJson() return user.toJson()
}, },
}, },
@ -25,17 +26,18 @@ export default {
email = normalizeEmail(email) email = normalizeEmail(email)
const session = driver.session() const session = driver.session()
try { try {
const result = await session.run( const loginReadTxResultPromise = session.readTransaction(async transaction => {
const loginTransactionResponse = await transaction.run(
` `
MATCH (user:User {deleted: false})-[:PRIMARY_EMAIL]->(e:EmailAddress {email: $userEmail}) MATCH (user:User {deleted: false})-[:PRIMARY_EMAIL]->(e:EmailAddress {email: $userEmail})
RETURN user {.id, .slug, .name, .avatar, .encryptedPassword, .role, .disabled, email:e.email} as user LIMIT 1 RETURN user {.id, .slug, .name, .avatar, .encryptedPassword, .role, .disabled, email:e.email} as user LIMIT 1
`, `,
{ userEmail: email }, { userEmail: email },
) )
const [currentUser] = await result.records.map(record => { log(loginTransactionResponse)
return record.get('user') return loginTransactionResponse.records.map(record => record.get('user'))
}) })
const [currentUser] = await loginReadTxResultPromise
if ( if (
currentUser && currentUser &&
(await bcrypt.compareSync(password, currentUser.encryptedPassword)) && (await bcrypt.compareSync(password, currentUser.encryptedPassword)) &&
@ -53,7 +55,7 @@ export default {
} }
}, },
changePassword: async (_, { oldPassword, newPassword }, { driver, user }) => { changePassword: async (_, { oldPassword, newPassword }, { driver, user }) => {
const currentUser = await instance.find('User', user.id) const currentUser = await neode.find('User', user.id)
const encryptedPassword = currentUser.get('encryptedPassword') const encryptedPassword = currentUser.get('encryptedPassword')
if (!(await bcrypt.compareSync(oldPassword, encryptedPassword))) { if (!(await bcrypt.compareSync(oldPassword, encryptedPassword))) {

View File

@ -5,7 +5,7 @@ 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 { neode as getNeode } from '../../bootstrap/neo4j' import { getNeode } from '../../bootstrap/neo4j'
const factory = Factory() const factory = Factory()
const neode = getNeode() const neode = getNeode()

View File

@ -1,10 +1,11 @@
import { neo4jgraphql } from 'neo4j-graphql-js' import { neo4jgraphql } from 'neo4j-graphql-js'
import fileUpload from './fileUpload' import fileUpload from './fileUpload'
import { neode } from '../../bootstrap/neo4j' import { getNeode } from '../../bootstrap/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'
const instance = neode() const neode = getNeode()
export const getBlockedUsers = async context => { export const getBlockedUsers = async context => {
const { neode } = context const { neode } = context
@ -73,7 +74,7 @@ export default {
block: async (object, args, context, resolveInfo) => { block: 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 instance.cypher( await neode.cypher(
` `
MATCH(u:User {id: $currentUser.id})-[r:FOLLOWS]->(b:User {id: $args.id}) MATCH(u:User {id: $currentUser.id})-[r:FOLLOWS]->(b:User {id: $args.id})
DELETE r DELETE r
@ -81,8 +82,8 @@ export default {
{ currentUser, args }, { currentUser, args },
) )
const [user, blockedUser] = await Promise.all([ const [user, blockedUser] = await Promise.all([
instance.find('User', currentUser.id), neode.find('User', currentUser.id),
instance.find('User', args.id), neode.find('User', args.id),
]) ])
await user.relateTo(blockedUser, 'blocked') await user.relateTo(blockedUser, 'blocked')
return blockedUser.toJson() return blockedUser.toJson()
@ -90,45 +91,57 @@ export default {
unblock: async (object, args, context, resolveInfo) => { unblock: 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 instance.cypher( await neode.cypher(
` `
MATCH(u:User {id: $currentUser.id})-[r:BLOCKED]->(b:User {id: $args.id}) MATCH(u:User {id: $currentUser.id})-[r:BLOCKED]->(b:User {id: $args.id})
DELETE r DELETE r
`, `,
{ currentUser, args }, { currentUser, args },
) )
const blockedUser = await instance.find('User', args.id) const blockedUser = await neode.find('User', args.id)
return blockedUser.toJson() return blockedUser.toJson()
}, },
UpdateUser: async (object, args, context, resolveInfo) => { UpdateUser: async (_parent, params, context, _resolveInfo) => {
const { termsAndConditionsAgreedVersion } = args const { termsAndConditionsAgreedVersion } = params
if (termsAndConditionsAgreedVersion) { if (termsAndConditionsAgreedVersion) {
const regEx = new RegExp(/^[0-9]+\.[0-9]+\.[0-9]+$/g) const regEx = new RegExp(/^[0-9]+\.[0-9]+\.[0-9]+$/g)
if (!regEx.test(termsAndConditionsAgreedVersion)) { if (!regEx.test(termsAndConditionsAgreedVersion)) {
throw new ForbiddenError('Invalid version format!') throw new ForbiddenError('Invalid version format!')
} }
args.termsAndConditionsAgreedAt = new Date().toISOString() params.termsAndConditionsAgreedAt = new Date().toISOString()
} }
args = await fileUpload(args, { file: 'avatarUpload', url: 'avatar' }) params = await fileUpload(params, { file: 'avatarUpload', url: 'avatar' })
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async transaction => {
const updateUserTransactionResponse = await transaction.run(
`
MATCH (user:User {id: $params.id})
SET user += $params
SET user.updatedAt = toString(datetime())
RETURN user
`,
{ params },
)
return updateUserTransactionResponse.records.map(record => record.get('user').properties)
})
try { try {
const user = await instance.find('User', args.id) const [user] = await writeTxResultPromise
if (!user) return null return user
await user.update({ ...args, updatedAt: new Date().toISOString() }) } catch (error) {
return user.toJson() throw new UserInputError(error.message)
} catch (e) { } finally {
throw new UserInputError(e.message) session.close()
} }
}, },
DeleteUser: async (object, params, context, resolveInfo) => { DeleteUser: async (object, params, context, resolveInfo) => {
const { resource } = params const { resource } = params
const session = context.driver.session() const session = context.driver.session()
let user
try { try {
if (resource && resource.length) { if (resource && resource.length) {
await Promise.all( await session.writeTransaction(transaction => {
resource.map(async node => { resource.map(node => {
await session.run( return transaction.run(
` `
MATCH (resource:${node})<-[:WROTE]-(author:User {id: $userId}) MATCH (resource:${node})<-[:WROTE]-(author:User {id: $userId})
OPTIONAL MATCH (resource)<-[:COMMENTS]-(comment:Comment) OPTIONAL MATCH (resource)<-[:COMMENTS]-(comment:Comment)
@ -136,17 +149,18 @@ export default {
SET resource.content = 'UNAVAILABLE' SET resource.content = 'UNAVAILABLE'
SET resource.contentExcerpt = 'UNAVAILABLE' SET resource.contentExcerpt = 'UNAVAILABLE'
SET comment.deleted = true SET comment.deleted = true
RETURN author`, RETURN author
`,
{ {
userId: context.user.id, userId: context.user.id,
}, },
) )
}), })
) })
} }
// we cannot set slug to 'UNAVAILABE' because of unique constraints const deleteUserTxResultPromise = session.writeTransaction(async transaction => {
const transactionResult = await session.run( const deleteUserTransactionResponse = await transaction.run(
` `
MATCH (user:User {id: $userId}) MATCH (user:User {id: $userId})
SET user.deleted = true SET user.deleted = true
@ -158,14 +172,18 @@ export default {
WITH user WITH user
OPTIONAL MATCH (user)<-[:OWNED_BY]-(socialMedia:SocialMedia) OPTIONAL MATCH (user)<-[:OWNED_BY]-(socialMedia:SocialMedia)
DETACH DELETE socialMedia DETACH DELETE socialMedia
RETURN user`, RETURN user
`,
{ userId: context.user.id }, { userId: context.user.id },
) )
user = transactionResult.records.map(r => r.get('user').properties)[0] log(deleteUserTransactionResponse)
return deleteUserTransactionResponse.records.map(record => record.get('user').properties)
})
const [user] = await deleteUserTxResultPromise
return user
} finally { } finally {
session.close() session.close()
} }
return user
}, },
}, },
User: { User: {
@ -173,7 +191,7 @@ export default {
if (typeof parent.email !== 'undefined') return parent.email if (typeof parent.email !== 'undefined') return parent.email
const { id } = parent const { id } = parent
const statement = `MATCH(u:User {id: {id}})-[:PRIMARY_EMAIL]->(e:EmailAddress) RETURN e` const statement = `MATCH(u:User {id: {id}})-[:PRIMARY_EMAIL]->(e:EmailAddress) RETURN e`
const result = await instance.cypher(statement, { id }) const result = await neode.cypher(statement, { id })
const [{ email }] = result.records.map(r => r.get('e').properties) const [{ email }] = result.records.map(r => r.get('e').properties)
return email return email
}, },

View File

@ -1,6 +1,6 @@
import Factory from '../../seed/factories' import Factory from '../../seed/factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' import { getNeode, getDriver } from '../../bootstrap/neo4j'
import createServer from '../../server' import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
@ -68,6 +68,7 @@ describe('User', () => {
it('is permitted', async () => { it('is permitted', async () => {
await expect(query({ query: userQuery, variables })).resolves.toMatchObject({ await expect(query({ query: userQuery, variables })).resolves.toMatchObject({
data: { User: [{ name: 'Johnny' }] }, data: { User: [{ name: 'Johnny' }] },
errors: undefined,
}) })
}) })
@ -90,8 +91,7 @@ describe('User', () => {
}) })
describe('UpdateUser', () => { describe('UpdateUser', () => {
let userParams let userParams, variables
let variables
beforeEach(async () => { beforeEach(async () => {
userParams = { userParams = {
@ -111,16 +111,23 @@ describe('UpdateUser', () => {
}) })
const updateUserMutation = gql` const updateUserMutation = gql`
mutation($id: ID!, $name: String, $termsAndConditionsAgreedVersion: String) { mutation(
$id: ID!
$name: String
$termsAndConditionsAgreedVersion: String
$locationName: String
) {
UpdateUser( UpdateUser(
id: $id id: $id
name: $name name: $name
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
locationName: $locationName
) { ) {
id id
name name
termsAndConditionsAgreedVersion termsAndConditionsAgreedVersion
termsAndConditionsAgreedAt termsAndConditionsAgreedAt
locationName
} }
} }
` `
@ -152,7 +159,7 @@ describe('UpdateUser', () => {
authenticatedUser = await user.toJson() authenticatedUser = await user.toJson()
}) })
it('name within specifications', async () => { it('updates the name', async () => {
const expected = { const expected = {
data: { data: {
UpdateUser: { UpdateUser: {
@ -160,36 +167,13 @@ describe('UpdateUser', () => {
name: 'John Doughnut', name: 'John Doughnut',
}, },
}, },
errors: undefined,
} }
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject( await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject(
expected, expected,
) )
}) })
it('with `null` as name', async () => {
const variables = {
id: 'u47',
name: null,
}
const { errors } = await mutate({ mutation: updateUserMutation, variables })
expect(errors[0]).toHaveProperty(
'message',
'child "name" fails because ["name" contains an invalid value, "name" must be a string]',
)
})
it('with too short name', async () => {
const variables = {
id: 'u47',
name: ' ',
}
const { errors } = await mutate({ mutation: updateUserMutation, variables })
expect(errors[0]).toHaveProperty(
'message',
'child "name" fails because ["name" length must be at least 3 characters long]',
)
})
describe('given a new agreed version of terms and conditions', () => { describe('given a new agreed version of terms and conditions', () => {
beforeEach(async () => { beforeEach(async () => {
variables = { ...variables, termsAndConditionsAgreedVersion: '0.0.2' } variables = { ...variables, termsAndConditionsAgreedVersion: '0.0.2' }
@ -202,6 +186,7 @@ describe('UpdateUser', () => {
termsAndConditionsAgreedAt: expect.any(String), termsAndConditionsAgreedAt: expect.any(String),
}), }),
}, },
errors: undefined,
} }
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject( await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject(
@ -222,6 +207,7 @@ describe('UpdateUser', () => {
termsAndConditionsAgreedAt: null, termsAndConditionsAgreedAt: null,
}), }),
}, },
errors: undefined,
} }
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject( await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject(
@ -238,6 +224,14 @@ describe('UpdateUser', () => {
const { errors } = await mutate({ mutation: updateUserMutation, variables }) const { errors } = await mutate({ mutation: updateUserMutation, variables })
expect(errors[0]).toHaveProperty('message', 'Invalid version format!') expect(errors[0]).toHaveProperty('message', 'Invalid version format!')
}) })
it('supports updating location', async () => {
variables = { ...variables, locationName: 'Hamburg, New Jersey, United States of America' }
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject({
data: { UpdateUser: { locationName: 'Hamburg, New Jersey, United States of America' } },
errors: undefined,
})
})
}) })
}) })
@ -372,6 +366,7 @@ describe('DeleteUser', () => {
], ],
}, },
}, },
errors: undefined,
} }
await expect(mutate({ mutation: deleteUserMutation, variables })).resolves.toMatchObject( await expect(mutate({ mutation: deleteUserMutation, variables })).resolves.toMatchObject(
expectedResponse, expectedResponse,
@ -418,6 +413,7 @@ describe('DeleteUser', () => {
], ],
}, },
}, },
errors: undefined,
} }
await expect( await expect(
mutate({ mutation: deleteUserMutation, variables }), mutate({ mutation: deleteUserMutation, variables }),
@ -465,6 +461,7 @@ describe('DeleteUser', () => {
], ],
}, },
}, },
errors: undefined,
} }
await expect( await expect(
mutate({ mutation: deleteUserMutation, variables }), mutate({ mutation: deleteUserMutation, variables }),
@ -511,6 +508,7 @@ describe('DeleteUser', () => {
], ],
}, },
}, },
errors: undefined,
} }
await expect( await expect(
mutate({ mutation: deleteUserMutation, variables }), mutate({ mutation: deleteUserMutation, variables }),

View File

@ -2,11 +2,11 @@ import { createTestClient } from 'apollo-server-testing'
import createServer from '../../../server' import createServer from '../../../server'
import Factory from '../../../seed/factories' import Factory from '../../../seed/factories'
import { gql } from '../../../helpers/jest' import { gql } from '../../../helpers/jest'
import { neode, getDriver } from '../../../bootstrap/neo4j' import { getNeode, getDriver } from '../../../bootstrap/neo4j'
const driver = getDriver() const driver = getDriver()
const factory = Factory() const factory = Factory()
const instance = neode() const neode = getNeode()
let currentUser let currentUser
let blockedUser let blockedUser
@ -20,7 +20,7 @@ beforeEach(() => {
return { return {
user: authenticatedUser, user: authenticatedUser,
driver, driver,
neode: instance, neode,
cypherParams: { cypherParams: {
currentUserId: authenticatedUser ? authenticatedUser.id : null, currentUserId: authenticatedUser ? authenticatedUser.id : null,
}, },
@ -55,11 +55,11 @@ describe('blockedUsers', () => {
describe('authenticated and given a blocked user', () => { describe('authenticated and given a blocked user', () => {
beforeEach(async () => { beforeEach(async () => {
currentUser = await instance.create('User', { currentUser = await neode.create('User', {
name: 'Current User', name: 'Current User',
id: 'u1', id: 'u1',
}) })
blockedUser = await instance.create('User', { blockedUser = await neode.create('User', {
name: 'Blocked User', name: 'Blocked User',
id: 'u2', id: 'u2',
}) })
@ -113,7 +113,7 @@ describe('block', () => {
describe('authenticated', () => { describe('authenticated', () => {
beforeEach(async () => { beforeEach(async () => {
currentUser = await instance.create('User', { currentUser = await neode.create('User', {
name: 'Current User', name: 'Current User',
id: 'u1', id: 'u1',
}) })
@ -138,7 +138,7 @@ describe('block', () => {
describe('given a to-be-blocked user', () => { describe('given a to-be-blocked user', () => {
beforeEach(async () => { beforeEach(async () => {
blockedUser = await instance.create('User', { blockedUser = await neode.create('User', {
name: 'Blocked User', name: 'Blocked User',
id: 'u2', id: 'u2',
}) })
@ -181,11 +181,11 @@ describe('block', () => {
let postQuery let postQuery
beforeEach(async () => { beforeEach(async () => {
const post1 = await instance.create('Post', { const post1 = await neode.create('Post', {
id: 'p12', id: 'p12',
title: 'A post written by the current user', title: 'A post written by the current user',
}) })
const post2 = await instance.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 blocked user',
}) })
@ -323,7 +323,7 @@ describe('unblock', () => {
describe('authenticated', () => { describe('authenticated', () => {
beforeEach(async () => { beforeEach(async () => {
currentUser = await instance.create('User', { currentUser = await neode.create('User', {
name: 'Current User', name: 'Current User',
id: 'u1', id: 'u1',
}) })
@ -348,7 +348,7 @@ describe('unblock', () => {
describe('given another user', () => { describe('given another user', () => {
beforeEach(async () => { beforeEach(async () => {
blockedUser = await instance.create('User', { blockedUser = await neode.create('User', {
name: 'Blocked User', name: 'Blocked User',
id: 'u2', id: 'u2',
}) })

View File

@ -1,4 +1,4 @@
import { getDriver, neode } from '../../bootstrap/neo4j' import { getDriver, getNeode } from '../../bootstrap/neo4j'
import createBadge from './badges.js' import createBadge from './badges.js'
import createUser from './users.js' import createUser from './users.js'
import createPost from './posts.js' import createPost from './posts.js'
@ -29,17 +29,23 @@ const factories = {
export const cleanDatabase = async (options = {}) => { export const cleanDatabase = async (options = {}) => {
const { driver = getDriver() } = options const { driver = getDriver() } = options
const cypher = 'MATCH (n) DETACH DELETE n'
const session = driver.session() const session = driver.session()
try { try {
return await session.run(cypher) await session.writeTransaction(transaction => {
return transaction.run(
`
MATCH (everything)
DETACH DELETE everything
`,
)
})
} finally { } finally {
session.close() session.close()
} }
} }
export default function Factory(options = {}) { export default function Factory(options = {}) {
const { neo4jDriver = getDriver(), neodeInstance = neode() } = options const { neo4jDriver = getDriver(), neodeInstance = getNeode() } = options
const result = { const result = {
neo4jDriver, neo4jDriver,

View File

@ -3,7 +3,7 @@ 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 { neode as getNeode, getDriver } from '../bootstrap/neo4j' import { getNeode, getDriver } from '../bootstrap/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']

View File

@ -3,7 +3,7 @@ import helmet from 'helmet'
import { ApolloServer } from 'apollo-server-express' import { ApolloServer } from 'apollo-server-express'
import CONFIG, { requiredConfigs } from './config' import CONFIG, { requiredConfigs } from './config'
import middleware from './middleware' import middleware from './middleware'
import { neode as getNeode, getDriver } from './bootstrap/neo4j' import { getNeode, getDriver } from './bootstrap/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'
@ -38,6 +38,12 @@ const createServer = options => {
schema: middleware(schema), schema: middleware(schema),
debug: !!CONFIG.DEBUG, debug: !!CONFIG.DEBUG,
tracing: !!CONFIG.DEBUG, tracing: !!CONFIG.DEBUG,
formatError: error => {
if (error.message === 'ERROR_VALIDATION') {
return new Error(error.originalError.details.map(d => d.message))
}
return error
},
} }
const server = new ApolloServer(Object.assign({}, defaults, options)) const server = new ApolloServer(Object.assign({}, defaults, options))

View File

@ -33,10 +33,10 @@
resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.24.tgz#3ce939cb127fb8aaa3ffc1e90dff9b8af9f2e3dc" resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.24.tgz#3ce939cb127fb8aaa3ffc1e90dff9b8af9f2e3dc"
integrity sha512-8GqG48m1XqyXh4mIZrtB5xOhUwSsh1WsrrsaZQOEYYql3YN9DEu9OOSg0ILzXHZo/h2Q74777YE4YzlArQzQEQ== integrity sha512-8GqG48m1XqyXh4mIZrtB5xOhUwSsh1WsrrsaZQOEYYql3YN9DEu9OOSg0ILzXHZo/h2Q74777YE4YzlArQzQEQ==
"@babel/cli@~7.7.4": "@babel/cli@~7.7.5":
version "7.7.4" version "7.7.5"
resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.7.4.tgz#38804334c8db40209f88c69a5c90998e60cca18b" resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.7.5.tgz#25702cc65418efc06989af3727897b9f4c8690b6"
integrity sha512-O7mmzaWdm+VabWQmxuM8hqNrWGGihN83KfhPUzp2lAW4kzIMwBxujXkZbD4fMwKMYY9FXTbDvXsJqU+5XHXi4A== integrity sha512-y2YrMGXM3NUyu1Myg0pxg+Lx6g8XhEyvLHYNRwTBV6fDek3H7Io6b7N/LXscLs4HWn4HxMdy7f2rM1rTMp2mFg==
dependencies: dependencies:
commander "^4.0.1" commander "^4.0.1"
convert-source-map "^1.1.0" convert-source-map "^1.1.0"
@ -184,6 +184,18 @@
"@babel/types" "^7.7.4" "@babel/types" "^7.7.4"
lodash "^4.17.13" lodash "^4.17.13"
"@babel/helper-module-transforms@^7.7.5":
version "7.7.5"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.7.5.tgz#d044da7ffd91ec967db25cd6748f704b6b244835"
integrity sha512-A7pSxyJf1gN5qXVcidwLWydjftUN878VkalhXX5iQDuGyiGK3sOrrKKHF4/A4fwHtnsotv/NipwAeLzY4KQPvw==
dependencies:
"@babel/helper-module-imports" "^7.7.4"
"@babel/helper-simple-access" "^7.7.4"
"@babel/helper-split-export-declaration" "^7.7.4"
"@babel/template" "^7.7.4"
"@babel/types" "^7.7.4"
lodash "^4.17.13"
"@babel/helper-optimise-call-expression@^7.7.4": "@babel/helper-optimise-call-expression@^7.7.4":
version "7.7.4" version "7.7.4"
resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.7.4.tgz#034af31370d2995242aa4df402c3b7794b2dcdf2" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.7.4.tgz#034af31370d2995242aa4df402c3b7794b2dcdf2"
@ -502,21 +514,21 @@
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-transform-modules-amd@^7.7.4": "@babel/plugin-transform-modules-amd@^7.7.5":
version "7.7.4" version "7.7.5"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.7.4.tgz#276b3845ca2b228f2995e453adc2e6f54d72fb71" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.7.5.tgz#39e0fb717224b59475b306402bb8eedab01e729c"
integrity sha512-/542/5LNA18YDtg1F+QHvvUSlxdvjZoD/aldQwkq+E3WCkbEjNSN9zdrOXaSlfg3IfGi22ijzecklF/A7kVZFQ== integrity sha512-CT57FG4A2ZUNU1v+HdvDSDrjNWBrtCmSH6YbbgN3Lrf0Di/q/lWRxZrE72p3+HCCz9UjfZOEBdphgC0nzOS6DQ==
dependencies: dependencies:
"@babel/helper-module-transforms" "^7.7.4" "@babel/helper-module-transforms" "^7.7.5"
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
babel-plugin-dynamic-import-node "^2.3.0" babel-plugin-dynamic-import-node "^2.3.0"
"@babel/plugin-transform-modules-commonjs@^7.7.4": "@babel/plugin-transform-modules-commonjs@^7.7.5":
version "7.7.4" version "7.7.5"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.7.4.tgz#bee4386e550446343dd52a571eda47851ff857a3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.7.5.tgz#1d27f5eb0bcf7543e774950e5b2fa782e637b345"
integrity sha512-k8iVS7Jhc367IcNF53KCwIXtKAH7czev866ThsTgy8CwlXjnKZna2VHwChglzLleYrcHz1eQEIJlGRQxB53nqA== integrity sha512-9Cq4zTFExwFhQI6MT1aFxgqhIsMWQWDVwOgLzl7PTWJHsNaqFvklAU+Oz6AQLAS0dJKTwZSOCo20INwktxpi3Q==
dependencies: dependencies:
"@babel/helper-module-transforms" "^7.7.4" "@babel/helper-module-transforms" "^7.7.5"
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/helper-simple-access" "^7.7.4" "@babel/helper-simple-access" "^7.7.4"
babel-plugin-dynamic-import-node "^2.3.0" babel-plugin-dynamic-import-node "^2.3.0"
@ -576,10 +588,10 @@
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-transform-regenerator@^7.7.4": "@babel/plugin-transform-regenerator@^7.7.5":
version "7.7.4" version "7.7.5"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.7.4.tgz#d18eac0312a70152d7d914cbed2dc3999601cfc0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.7.5.tgz#3a8757ee1a2780f390e89f246065ecf59c26fce9"
integrity sha512-e7MWl5UJvmPEwFJTwkBlPmqixCtr9yAASBqff4ggXTNicZiwbF8Eefzm6NVgfiBp7JdAGItecnctKTgH44q2Jw== integrity sha512-/8I8tPvX2FkuEyWbjRCt4qTAgZK0DVy8QRguhA524UH48RfGJy94On2ri+dCuwOpcerPRl9O4ebQkRcVzIaGBw==
dependencies: dependencies:
regenerator-transform "^0.14.0" regenerator-transform "^0.14.0"
@ -635,10 +647,10 @@
"@babel/helper-create-regexp-features-plugin" "^7.7.4" "@babel/helper-create-regexp-features-plugin" "^7.7.4"
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
"@babel/preset-env@~7.7.4": "@babel/preset-env@~7.7.6":
version "7.7.4" version "7.7.6"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.7.4.tgz#ccaf309ae8d1ee2409c85a4e2b5e280ceee830f8" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.7.6.tgz#39ac600427bbb94eec6b27953f1dfa1d64d457b2"
integrity sha512-Dg+ciGJjwvC1NIe/DGblMbcGq1HOtKbw8RLl4nIjlfcILKEOkWT/vRqPpumswABEBVudii6dnVwrBtzD7ibm4g== integrity sha512-k5hO17iF/Q7tR9Jv8PdNBZWYW6RofxhnxKjBMc0nG4JTaWvOTiPoO/RLFwAKcA4FpmuBFm6jkoqaRJLGi0zdaQ==
dependencies: dependencies:
"@babel/helper-module-imports" "^7.7.4" "@babel/helper-module-imports" "^7.7.4"
"@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0"
@ -668,8 +680,8 @@
"@babel/plugin-transform-function-name" "^7.7.4" "@babel/plugin-transform-function-name" "^7.7.4"
"@babel/plugin-transform-literals" "^7.7.4" "@babel/plugin-transform-literals" "^7.7.4"
"@babel/plugin-transform-member-expression-literals" "^7.7.4" "@babel/plugin-transform-member-expression-literals" "^7.7.4"
"@babel/plugin-transform-modules-amd" "^7.7.4" "@babel/plugin-transform-modules-amd" "^7.7.5"
"@babel/plugin-transform-modules-commonjs" "^7.7.4" "@babel/plugin-transform-modules-commonjs" "^7.7.5"
"@babel/plugin-transform-modules-systemjs" "^7.7.4" "@babel/plugin-transform-modules-systemjs" "^7.7.4"
"@babel/plugin-transform-modules-umd" "^7.7.4" "@babel/plugin-transform-modules-umd" "^7.7.4"
"@babel/plugin-transform-named-capturing-groups-regex" "^7.7.4" "@babel/plugin-transform-named-capturing-groups-regex" "^7.7.4"
@ -677,7 +689,7 @@
"@babel/plugin-transform-object-super" "^7.7.4" "@babel/plugin-transform-object-super" "^7.7.4"
"@babel/plugin-transform-parameters" "^7.7.4" "@babel/plugin-transform-parameters" "^7.7.4"
"@babel/plugin-transform-property-literals" "^7.7.4" "@babel/plugin-transform-property-literals" "^7.7.4"
"@babel/plugin-transform-regenerator" "^7.7.4" "@babel/plugin-transform-regenerator" "^7.7.5"
"@babel/plugin-transform-reserved-words" "^7.7.4" "@babel/plugin-transform-reserved-words" "^7.7.4"
"@babel/plugin-transform-shorthand-properties" "^7.7.4" "@babel/plugin-transform-shorthand-properties" "^7.7.4"
"@babel/plugin-transform-spread" "^7.7.4" "@babel/plugin-transform-spread" "^7.7.4"
@ -687,7 +699,7 @@
"@babel/plugin-transform-unicode-regex" "^7.7.4" "@babel/plugin-transform-unicode-regex" "^7.7.4"
"@babel/types" "^7.7.4" "@babel/types" "^7.7.4"
browserslist "^4.6.0" browserslist "^4.6.0"
core-js-compat "^3.1.1" core-js-compat "^3.4.7"
invariant "^2.2.2" invariant "^2.2.2"
js-levenshtein "^1.1.3" js-levenshtein "^1.1.3"
semver "^5.5.0" semver "^5.5.0"
@ -1022,10 +1034,10 @@
url-regex "~4.1.1" url-regex "~4.1.1"
video-extensions "~1.1.0" video-extensions "~1.1.0"
"@metascraper/helpers@^5.8.7": "@metascraper/helpers@^5.8.10", "@metascraper/helpers@^5.8.7":
version "5.8.7" version "5.8.10"
resolved "https://registry.yarnpkg.com/@metascraper/helpers/-/helpers-5.8.7.tgz#b05f83f2a90001f7753c18a8b1bb978bd7c2f9d9" resolved "https://registry.yarnpkg.com/@metascraper/helpers/-/helpers-5.8.10.tgz#efaae1d57afca6db1f0846852fe88d1608601f13"
integrity sha512-gDErMAA3d1CdkGxvAG4cDi7D2+fReZpD6lzYNJ/gsq45U3Pdz7ltsAvbp4amK92bGKYYPZtnUq85Wrr+Q+e06Q== integrity sha512-o7vrlNC+wzfArTkQcQfHKT4iHUYEQYs6hoORTWN7A1dj5v8P1wl5oOs0oAc7MNGJ3nWnex3/bq/5SUWV301Arg==
dependencies: dependencies:
audio-extensions "0.0.0" audio-extensions "0.0.0"
chrono-node "~1.3.11" chrono-node "~1.3.11"
@ -1101,56 +1113,56 @@
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=
"@sentry/apm@5.10.1": "@sentry/apm@5.10.2":
version "5.10.1" version "5.10.2"
resolved "https://registry.yarnpkg.com/@sentry/apm/-/apm-5.10.1.tgz#2ec20cef0f87f9f638ff78dd5092e1e9d36c4b7d" resolved "https://registry.yarnpkg.com/@sentry/apm/-/apm-5.10.2.tgz#41a401b3964b68514439f8a595b12c6fd05ab21a"
integrity sha512-VSFK8giRG5/lN0YSaOw8+Cru/8MVevmoHZ5JC9iDIt0H6sGTUjOBKIqTZ0eq2Y99Vn0N9dkxjeT0rOIvsrg0gA== integrity sha512-rPeAFsD/6ontvs7bsuHh+XAg1ohWo04ms08SNWqEvLRQJx7WfiWnjziyC0S3dXIYZDGdhruSsqQJPJN8r6Aj5g==
dependencies: dependencies:
"@sentry/hub" "5.10.1" "@sentry/hub" "5.10.2"
"@sentry/minimal" "5.10.1" "@sentry/minimal" "5.10.2"
"@sentry/types" "5.10.0" "@sentry/types" "5.10.0"
"@sentry/utils" "5.10.1" "@sentry/utils" "5.10.2"
tslib "^1.9.3" tslib "^1.9.3"
"@sentry/core@5.10.1": "@sentry/core@5.10.2":
version "5.10.1" version "5.10.2"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.10.1.tgz#356551f111d4df38e60852607cc8cde0ed8ccc76" resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.10.2.tgz#1cb64489e6f8363c3249415b49d3f1289814825f"
integrity sha512-MbiasA/cuMB0+9zVBGi5YLWRj7CdFQJOM29Vp8rm3xMaQDH0KHarpny1gOgMiLu/O/r8itjiZwKu+9pxOWGbeA== integrity sha512-sKVeFH3v8K8xw2vM5MKMnnyAAwih+JSE3pbNL0CcCCA+/SwX+3jeAo2BhgXev2SAR/TjWW+wmeC9TdIW7KyYbg==
dependencies: dependencies:
"@sentry/hub" "5.10.1" "@sentry/hub" "5.10.2"
"@sentry/minimal" "5.10.1" "@sentry/minimal" "5.10.2"
"@sentry/types" "5.10.0" "@sentry/types" "5.10.0"
"@sentry/utils" "5.10.1" "@sentry/utils" "5.10.2"
tslib "^1.9.3" tslib "^1.9.3"
"@sentry/hub@5.10.1": "@sentry/hub@5.10.2":
version "5.10.1" version "5.10.2"
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.10.1.tgz#3be4a0705cd0cd074be0aab0dc418ecb72885989" resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.10.2.tgz#25d9f36b8f7c5cb65cf486737fa61dc9bf69b7e3"
integrity sha512-g+P+0cj6vKdf6Ct4S47MxHwSMIjtIadOwBhb4Lqwij5YPtQ4LpVr10peKbE+FMMvCNQSvQnJEhTDko+AE7AoYw== integrity sha512-hSlZIiu3hcR/I5yEhlpN9C0nip+U7hiRzRzUQaBiHO4YG4TC58NqnOPR89D/ekiuHIXzFpjW9OQmqtAMRoSUYA==
dependencies: dependencies:
"@sentry/types" "5.10.0" "@sentry/types" "5.10.0"
"@sentry/utils" "5.10.1" "@sentry/utils" "5.10.2"
tslib "^1.9.3" tslib "^1.9.3"
"@sentry/minimal@5.10.1": "@sentry/minimal@5.10.2":
version "5.10.1" version "5.10.2"
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.10.1.tgz#37104f81ef3b333c0f9e77ac94bfed348070dea3" resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.10.2.tgz#267c2f3aa6877a0fe7a86971942e83f3ee616580"
integrity sha512-oKrLvKaah0xGVIYbS1I7dVbo73aWssfiT2ypl9DYt8MAFiwfiiXz68FlG4z9dPZ2jSz9Jm2SAYHFaYLvU26TBQ== integrity sha512-GalixiM9sckYfompH5HHTp9XT2BcjawBkcl1DMEKUBEi37+kUq0bivOBmnN1G/I4/wWOUdnAI/kagDWaWpbZPg==
dependencies: dependencies:
"@sentry/hub" "5.10.1" "@sentry/hub" "5.10.2"
"@sentry/types" "5.10.0" "@sentry/types" "5.10.0"
tslib "^1.9.3" tslib "^1.9.3"
"@sentry/node@^5.10.1": "@sentry/node@^5.10.2":
version "5.10.1" version "5.10.2"
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-5.10.1.tgz#cafbf3b0918c98fb9f99803ffe50056e32194bef" resolved "https://registry.yarnpkg.com/@sentry/node/-/node-5.10.2.tgz#1f5d6deefb2c1549ddb542c10952cccf5f9a4ac2"
integrity sha512-kard7OXQDvYqmQD93bOkYhznqrbsiFNZ6+dIi13eo/kc2Au+v1Th1mGvr9JDRE/X07z6vJMYMiorKd351G3p/A== integrity sha512-1ib1hAhVtmfXOThpcCfR4S6wFopd6lHqgOMrAUPo9saHy8zseZPRC7iTWGoSPy2RMwjrURAk54VvFnLe7G+PdQ==
dependencies: dependencies:
"@sentry/apm" "5.10.1" "@sentry/apm" "5.10.2"
"@sentry/core" "5.10.1" "@sentry/core" "5.10.2"
"@sentry/hub" "5.10.1" "@sentry/hub" "5.10.2"
"@sentry/types" "5.10.0" "@sentry/types" "5.10.0"
"@sentry/utils" "5.10.1" "@sentry/utils" "5.10.2"
cookie "^0.3.1" cookie "^0.3.1"
https-proxy-agent "^3.0.0" https-proxy-agent "^3.0.0"
lru_map "^0.3.3" lru_map "^0.3.3"
@ -1161,10 +1173,10 @@
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.10.0.tgz#4f0ba31b6e4d5371112c38279f11f66c73b43746" resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.10.0.tgz#4f0ba31b6e4d5371112c38279f11f66c73b43746"
integrity sha512-TW20GzkCWsP6uAxR2JIpIkiitCKyIOfkyDsKBeLqYj4SaZjfvBPnzgNCcYR0L0UsP1/Es6oHooZfIGSkp6GGxQ== integrity sha512-TW20GzkCWsP6uAxR2JIpIkiitCKyIOfkyDsKBeLqYj4SaZjfvBPnzgNCcYR0L0UsP1/Es6oHooZfIGSkp6GGxQ==
"@sentry/utils@5.10.1": "@sentry/utils@5.10.2":
version "5.10.1" version "5.10.2"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.10.1.tgz#eeb3ede85a9b5b1cd1aad7e3157052bee0d42551" resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.10.2.tgz#261f575079d30aaf604e59f5f4de0aa21db22252"
integrity sha512-zdv03sINfJ8QXSHP49845qhkbdNUrX20AagUY+Arq2zxmM4XxnRVA7dtWDkyy55bTt0ziRuSikBxR3266t8mDg== integrity sha512-UcbbaFpYrGSV448lQ16Cr+W/MPuKUflQQUdrMCt5vgaf5+M7kpozlcji4GGGZUCXIA7oRP93ABoXj55s1OM9zw==
dependencies: dependencies:
"@sentry/types" "5.10.0" "@sentry/types" "5.10.0"
tslib "^1.9.3" tslib "^1.9.3"
@ -1602,37 +1614,37 @@ apollo-cache-control@^0.8.8:
apollo-server-env "^2.4.3" apollo-server-env "^2.4.3"
graphql-extensions "^0.10.7" graphql-extensions "^0.10.7"
apollo-cache-inmemory@~1.6.3: apollo-cache-inmemory@~1.6.5:
version "1.6.3" version "1.6.5"
resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.3.tgz#826861d20baca4abc45f7ca7a874105905b8525d" resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.5.tgz#2ccaa3827686f6ed7fb634203dbf2b8d7015856a"
integrity sha512-S4B/zQNSuYc0M/1Wq8dJDTIO9yRgU0ZwDGnmlqxGGmFombOZb9mLjylewSfQKmjNpciZ7iUIBbJ0mHlPJTzdXg== integrity sha512-koB76JUDJaycfejHmrXBbWIN9pRKM0Z9CJGQcBzIOtmte1JhEBSuzsOUu7NQgiXKYI4iGoMREcnaWffsosZynA==
dependencies: dependencies:
apollo-cache "^1.3.2" apollo-cache "^1.3.4"
apollo-utilities "^1.3.2" apollo-utilities "^1.3.3"
optimism "^0.10.0" optimism "^0.10.0"
ts-invariant "^0.4.0" ts-invariant "^0.4.0"
tslib "^1.9.3" tslib "^1.10.0"
apollo-cache@1.3.2, apollo-cache@^1.3.2: apollo-cache@1.3.4, apollo-cache@^1.3.4:
version "1.3.2" version "1.3.4"
resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.3.2.tgz#df4dce56240d6c95c613510d7e409f7214e6d26a" resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.3.4.tgz#0c9f63c793e1cd6e34c450f7668e77aff58c9a42"
integrity sha512-+KA685AV5ETEJfjZuviRTEImGA11uNBp/MJGnaCvkgr+BYRrGLruVKBv6WvyFod27WEB2sp7SsG8cNBKANhGLg== integrity sha512-7X5aGbqaOWYG+SSkCzJNHTz2ZKDcyRwtmvW4mGVLRqdQs+HxfXS4dUS2CcwrAj449se6tZ6NLUMnjko4KMt3KA==
dependencies: dependencies:
apollo-utilities "^1.3.2" apollo-utilities "^1.3.3"
tslib "^1.9.3" tslib "^1.10.0"
apollo-client@~2.6.4: apollo-client@~2.6.8:
version "2.6.4" version "2.6.8"
resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.6.4.tgz#872c32927263a0d34655c5ef8a8949fbb20b6140" resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.6.8.tgz#01cebc18692abf90c6b3806414e081696b0fa537"
integrity sha512-oWOwEOxQ9neHHVZrQhHDbI6bIibp9SHgxaLRVPoGvOFy7OH5XUykZE7hBQAVxq99tQjBzgytaZffQkeWo1B4VQ== integrity sha512-0zvJtAcONiozpa5z5zgou83iEKkBaXhhSSXJebFHRXs100SecDojyUWKjwTtBPn9HbM6o5xrvC5mo9VQ5fgAjw==
dependencies: dependencies:
"@types/zen-observable" "^0.8.0" "@types/zen-observable" "^0.8.0"
apollo-cache "1.3.2" apollo-cache "1.3.4"
apollo-link "^1.0.0" apollo-link "^1.0.0"
apollo-utilities "1.3.2" apollo-utilities "1.3.3"
symbol-observable "^1.0.2" symbol-observable "^1.0.2"
ts-invariant "^0.4.0" ts-invariant "^0.4.0"
tslib "^1.9.3" tslib "^1.10.0"
zen-observable "^0.8.0" zen-observable "^0.8.0"
apollo-datasource@^0.6.3: apollo-datasource@^0.6.3:
@ -1835,15 +1847,15 @@ apollo-tracing@^0.8.8:
apollo-server-env "^2.4.3" apollo-server-env "^2.4.3"
graphql-extensions "^0.10.7" graphql-extensions "^0.10.7"
apollo-utilities@1.3.2, apollo-utilities@^1.0.1, apollo-utilities@^1.3.0, apollo-utilities@^1.3.2: apollo-utilities@1.3.3, apollo-utilities@^1.0.1, apollo-utilities@^1.3.0, apollo-utilities@^1.3.3:
version "1.3.2" version "1.3.3"
resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.2.tgz#8cbdcf8b012f664cd6cb5767f6130f5aed9115c9" resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.3.tgz#f1854715a7be80cd810bc3ac95df085815c0787c"
integrity sha512-JWNHj8XChz7S4OZghV6yc9FNnzEXj285QYp/nLNh943iObycI5GTDO3NGR9Dth12LRrSFMeDOConPfPln+WGfg== integrity sha512-F14aX2R/fKNYMvhuP2t9GD9fggID7zp5I96MF5QeKYWDWTrkRdHRp4+SVfXUVN+cXOaB/IebfvRtzPf25CM0zw==
dependencies: dependencies:
"@wry/equality" "^0.1.2" "@wry/equality" "^0.1.2"
fast-json-stable-stringify "^2.0.0" fast-json-stable-stringify "^2.0.0"
ts-invariant "^0.4.0" ts-invariant "^0.4.0"
tslib "^1.9.3" tslib "^1.10.0"
aproba@^1.0.3: aproba@^1.0.3:
version "1.2.0" version "1.2.0"
@ -1918,6 +1930,15 @@ array-unique@^0.3.2:
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
array.prototype.flat@^1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.2.tgz#8f3c71d245ba349b6b64b4078f76f5576f1fd723"
integrity sha512-VXjh7lAL4KXKF2hY4FnEW9eRW6IhdvFW1sN/JwLbmECbCgACCnBHNyP3lFiYuttr0jxRN9Bsc5+G27dMseSWqQ==
dependencies:
define-properties "^1.1.3"
es-abstract "^1.15.0"
function-bind "^1.1.1"
arrify@^2.0.1: arrify@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
@ -2233,14 +2254,14 @@ browser-resolve@^1.11.3:
dependencies: dependencies:
resolve "1.1.7" resolve "1.1.7"
browserslist@^4.6.0, browserslist@^4.6.6: browserslist@^4.6.0, browserslist@^4.8.2:
version "4.6.6" version "4.8.2"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.6.6.tgz#6e4bf467cde520bc9dbdf3747dafa03531cec453" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.8.2.tgz#b45720ad5fbc8713b7253c20766f701c9a694289"
integrity sha512-D2Nk3W9JL9Fp/gIcWei8LrERCS+eXu9AM5cfXA8WEZ84lFks+ARnZ0q/R69m2SV3Wjma83QDDPxsNKXUwdIsyA== integrity sha512-+M4oeaTplPm/f1pXDw84YohEv7B1i/2Aisei8s4s6k3QsoSHa7i5sz8u/cGQkkatCPxMASKxPualR4wwYgVboA==
dependencies: dependencies:
caniuse-lite "^1.0.30000984" caniuse-lite "^1.0.30001015"
electron-to-chromium "^1.3.191" electron-to-chromium "^1.3.322"
node-releases "^1.1.25" node-releases "^1.1.42"
bser@^2.0.0: bser@^2.0.0:
version "2.1.0" version "2.1.0"
@ -2319,10 +2340,10 @@ camelize@1.0.0:
resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b" resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b"
integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs= integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=
caniuse-lite@^1.0.30000984: caniuse-lite@^1.0.30001015:
version "1.0.30000989" version "1.0.30001015"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000989.tgz#b9193e293ccf7e4426c5245134b8f2a56c0ac4b9" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001015.tgz#15a7ddf66aba786a71d99626bc8f2b91c6f0f5f0"
integrity sha512-vrMcvSuMz16YY6GSVZ0dWDTJP8jqk3iFQ/Aq5iqblPwxSVVZI+zxDyTX0VPqtQsDnfdrBDcsmhgTEOh5R8Lbpw== integrity sha512-/xL2AbW/XWHNu1gnIrO8UitBGoFthcsDgU9VLK1/dpsoxbaD5LscHozKze05R6WLsBvLhqv78dAPozMFQBYLbQ==
capture-exit@^2.0.0: capture-exit@^2.0.0:
version "2.0.0" version "2.0.0"
@ -2683,12 +2704,12 @@ copy-descriptor@^0.1.0:
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
core-js-compat@^3.1.1: core-js-compat@^3.4.7:
version "3.2.1" version "3.4.8"
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.2.1.tgz#0cbdbc2e386e8e00d3b85dc81c848effec5b8150" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.4.8.tgz#f72e6a4ed76437ea710928f44615f926a81607d5"
integrity sha512-MwPZle5CF9dEaMYdDeWm73ao/IflDH+FjeJCWEADcEgFSE9TLimFKwJsfmkwzI8eC0Aj0mgvMDjeQjrElkz4/A== integrity sha512-l3WTmnXHV2Sfu5VuD7EHE2w7y+K68+kULKt5RJg8ZJk3YhHF1qLD4O8v8AmNq+8vbOwnPFFDvds25/AoEvMqlQ==
dependencies: dependencies:
browserslist "^4.6.6" browserslist "^4.8.2"
semver "^6.3.0" semver "^6.3.0"
core-js@^2.4.0, core-js@^2.6.5: core-js@^2.4.0, core-js@^2.6.5:
@ -2874,7 +2895,7 @@ date-fns@2.8.1:
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.8.1.tgz#2109362ccb6c87c3ca011e9e31f702bc09e4123b" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.8.1.tgz#2109362ccb6c87c3ca011e9e31f702bc09e4123b"
integrity sha512-EL/C8IHvYRwAHYgFRse4MGAPSqlJVlOrhVYZ75iQBKrnv+ZedmYsgwH3t+BCDuZDXpoo07+q9j4qgSSOa7irJg== integrity sha512-EL/C8IHvYRwAHYgFRse4MGAPSqlJVlOrhVYZ75iQBKrnv+ZedmYsgwH3t+BCDuZDXpoo07+q9j4qgSSOa7irJg==
debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9:
version "2.6.9" version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
@ -3156,10 +3177,10 @@ ee-first@1.1.1:
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
electron-to-chromium@^1.3.191: electron-to-chromium@^1.3.322:
version "1.3.237" version "1.3.322"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.237.tgz#39c5d1da59d6fd16ff705b97e772bb3b5dfda7e4" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.322.tgz#a6f7e1c79025c2b05838e8e344f6e89eb83213a8"
integrity sha512-SPAFjDr/7iiVK2kgTluwxela6eaWjjFkS9rO/iYpB/KGXgccUom5YC7OIf19c8m8GGptWxLU0Em8xM64A/N7Fg== integrity sha512-Tc8JQEfGQ1MzfSzI/bTlSr7btJv/FFO7Yh6tanqVmIWOuNCu6/D1MilIEgLtmWqIrsv+o4IjpLAhgMBr/ncNAA==
emoji-regex@^7.0.1: emoji-regex@^7.0.1:
version "7.0.3" version "7.0.3"
@ -3219,6 +3240,22 @@ es-abstract@^1.12.0, es-abstract@^1.4.3, es-abstract@^1.5.1, es-abstract@^1.7.0:
is-regex "^1.0.4" is-regex "^1.0.4"
object-keys "^1.0.12" object-keys "^1.0.12"
es-abstract@^1.15.0:
version "1.16.3"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.16.3.tgz#52490d978f96ff9f89ec15b5cf244304a5bca161"
integrity sha512-WtY7Fx5LiOnSYgF5eg/1T+GONaGmpvpPdCpSnYij+U2gDTL0UPfWrhDw7b2IYb+9NQJsYpCA0wOQvZfsd6YwRw==
dependencies:
es-to-primitive "^1.2.1"
function-bind "^1.1.1"
has "^1.0.3"
has-symbols "^1.0.1"
is-callable "^1.1.4"
is-regex "^1.0.4"
object-inspect "^1.7.0"
object-keys "^1.1.1"
string.prototype.trimleft "^2.1.0"
string.prototype.trimright "^2.1.0"
es-to-primitive@^1.2.0: es-to-primitive@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377"
@ -3228,6 +3265,15 @@ es-to-primitive@^1.2.0:
is-date-object "^1.0.1" is-date-object "^1.0.1"
is-symbol "^1.0.2" is-symbol "^1.0.2"
es-to-primitive@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
dependencies:
is-callable "^1.1.4"
is-date-object "^1.0.1"
is-symbol "^1.0.2"
es5-ext@^0.10.35, es5-ext@^0.10.50, es5-ext@~0.10.14, es5-ext@~0.10.46: es5-ext@^0.10.35, es5-ext@^0.10.50, es5-ext@~0.10.14, es5-ext@~0.10.46:
version "0.10.50" version "0.10.50"
resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.50.tgz#6d0e23a0abdb27018e5ac4fd09b412bc5517a778" resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.50.tgz#6d0e23a0abdb27018e5ac4fd09b412bc5517a778"
@ -3313,12 +3359,12 @@ eslint-import-resolver-node@^0.3.2:
debug "^2.6.9" debug "^2.6.9"
resolve "^1.5.0" resolve "^1.5.0"
eslint-module-utils@^2.4.0: eslint-module-utils@^2.4.1:
version "2.4.1" version "2.5.0"
resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.4.1.tgz#7b4675875bf96b0dbf1b21977456e5bb1f5e018c" resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.5.0.tgz#cdf0b40d623032274ccd2abd7e64c4e524d6e19c"
integrity sha512-H6DOj+ejw7Tesdgbfs4jeS4YMFrT8uI8xwd1gtQqXssaR0EQ26L+2O/w6wkYFy2MymON0fTwHmXBvvfLNZVZEw== integrity sha512-kCo8pZaNz2dsAW7nCUjuVoI11EBXXpIzfNxmaoLhXoRDOnqXLC4iSGVRdZPhOitfbdEfMEfKOiENaK6wDPZEGw==
dependencies: dependencies:
debug "^2.6.8" debug "^2.6.9"
pkg-dir "^2.0.0" pkg-dir "^2.0.0"
eslint-plugin-es@^2.0.0: eslint-plugin-es@^2.0.0:
@ -3329,22 +3375,23 @@ eslint-plugin-es@^2.0.0:
eslint-utils "^1.4.2" eslint-utils "^1.4.2"
regexpp "^3.0.0" regexpp "^3.0.0"
eslint-plugin-import@~2.18.2: eslint-plugin-import@~2.19.1:
version "2.18.2" version "2.19.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz#02f1180b90b077b33d447a17a2326ceb400aceb6" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.19.1.tgz#5654e10b7839d064dd0d46cd1b88ec2133a11448"
integrity sha512-5ohpsHAiUBRNaBWAF08izwUGlbrJoJJ+W9/TBwsGoR1MnlgfwMIKrFeSjWbt6moabiXW9xNvtFz+97KHRfI4HQ== integrity sha512-x68131aKoCZlCae7rDXKSAQmbT5DQuManyXo2sK6fJJ0aK5CWAkv6A6HJZGgqC8IhjQxYPgo6/IY4Oz8AFsbBw==
dependencies: dependencies:
array-includes "^3.0.3" array-includes "^3.0.3"
array.prototype.flat "^1.2.1"
contains-path "^0.1.0" contains-path "^0.1.0"
debug "^2.6.9" debug "^2.6.9"
doctrine "1.5.0" doctrine "1.5.0"
eslint-import-resolver-node "^0.3.2" eslint-import-resolver-node "^0.3.2"
eslint-module-utils "^2.4.0" eslint-module-utils "^2.4.1"
has "^1.0.3" has "^1.0.3"
minimatch "^3.0.4" minimatch "^3.0.4"
object.values "^1.1.0" object.values "^1.1.0"
read-pkg-up "^2.0.0" read-pkg-up "^2.0.0"
resolve "^1.11.0" resolve "^1.12.0"
eslint-plugin-jest@~23.1.1: eslint-plugin-jest@~23.1.1:
version "23.1.1" version "23.1.1"
@ -3365,10 +3412,10 @@ eslint-plugin-node@~10.0.0:
resolve "^1.10.1" resolve "^1.10.1"
semver "^6.1.0" semver "^6.1.0"
eslint-plugin-prettier@~3.1.1: eslint-plugin-prettier@~3.1.2:
version "3.1.1" version "3.1.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.1.tgz#507b8562410d02a03f0ddc949c616f877852f2ba" resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.2.tgz#432e5a667666ab84ce72f945c72f77d996a5c9ba"
integrity sha512-A+TZuHZ0KU0cnn56/9mfR7/KjUJ9QNVXUhwvRFSR7PGPe0zQR6PTkmyqg1AtUUEOzTqeRsUwyKFh0oVZKVCrtA== integrity sha512-GlolCC9y3XZfv3RQfwGew7NnuFDKsfI4lbvRK+PIIo23SFH+LemGs4cKwzAaRa+Mdb+lQO/STaIayno8T5sJJA==
dependencies: dependencies:
prettier-linter-helpers "^1.0.0" prettier-linter-helpers "^1.0.0"
@ -4205,6 +4252,11 @@ has-symbols@^1.0.0:
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44"
integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=
has-symbols@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
has-unicode@^2.0.0: has-unicode@^2.0.0:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
@ -5758,12 +5810,12 @@ merge-stream@^2.0.0:
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
metascraper-audio@^5.8.7: metascraper-audio@^5.8.10:
version "5.8.7" version "5.8.10"
resolved "https://registry.yarnpkg.com/metascraper-audio/-/metascraper-audio-5.8.7.tgz#ce27b1f4056c1d1cbaa2cec0e819c3704f38fff4" resolved "https://registry.yarnpkg.com/metascraper-audio/-/metascraper-audio-5.8.10.tgz#bc7bc0471ee178ab747baec4fb9bf7443078980d"
integrity sha512-ew9KZKOIl3u0500j7qIR/ZNiVtSohuyyiIWSxJVEeeguEOwAhMpOrpYAEkvKRo5CB89F2PNBIsXJIzMC4BWFrw== integrity sha512-uR4PCG7mxz7GLZ3I3x83sTCAaD/+MMTSf5rtP+shfdGJCm6h3mNmUpZm6hlBunmBx/PpDpwdI34rkl2A8SUjnQ==
dependencies: dependencies:
"@metascraper/helpers" "^5.8.7" "@metascraper/helpers" "^5.8.10"
metascraper-author@^5.8.7: metascraper-author@^5.8.7:
version "5.8.7" version "5.8.7"
@ -5787,19 +5839,19 @@ metascraper-date@^5.8.7:
dependencies: dependencies:
"@metascraper/helpers" "^5.8.7" "@metascraper/helpers" "^5.8.7"
metascraper-description@^5.8.7: metascraper-description@^5.8.10:
version "5.8.7" version "5.8.10"
resolved "https://registry.yarnpkg.com/metascraper-description/-/metascraper-description-5.8.7.tgz#e85ce218daf33b74813b1523ad7dc7dc3fb128af" resolved "https://registry.yarnpkg.com/metascraper-description/-/metascraper-description-5.8.10.tgz#1b69f59fa76263fcd2c15f8ce73052b81900177a"
integrity sha512-KOv5gnQVvGF1CgpUczu7KJm76rWJ7SH5UFcqFST60hRNgR9xy0y3aHbVDOhZkjNN4UKqnxMF6XTS/WaQxCK/AA== integrity sha512-0stYkl5OPpM0yM6Dl3WcXxLjl2gY5k77E4seeHOqHAUx1EKXNgrSrtO0I3PX9p6vcxP+WBtK6zlqHYU4qAMlSA==
dependencies: dependencies:
"@metascraper/helpers" "^5.8.7" "@metascraper/helpers" "^5.8.10"
metascraper-image@^5.8.7: metascraper-image@^5.8.10:
version "5.8.7" version "5.8.10"
resolved "https://registry.yarnpkg.com/metascraper-image/-/metascraper-image-5.8.7.tgz#d24697c5b5a6ba688948c48fadcb5fffeb6c703d" resolved "https://registry.yarnpkg.com/metascraper-image/-/metascraper-image-5.8.10.tgz#fe21811ca88eef13e64812462fb5a21ee48933dc"
integrity sha512-OMK+PFnHeavCSuEJY5tFkG5tdl/luYmPys7PKkJIwC8A8q5qoAC0InIUu+c0SDrdf4nzOj083DZTp32YQxYF5A== integrity sha512-WOPnTupaDEl58iZp0M6kFlUcRSRQFSPWATPUi3AeW31VJM2sepxmJlqc5qVFTen/Lm+kI23firrvEg5N8tFUVA==
dependencies: dependencies:
"@metascraper/helpers" "^5.8.7" "@metascraper/helpers" "^5.8.10"
metascraper-lang-detector@^4.10.2: metascraper-lang-detector@^4.10.2:
version "4.10.2" version "4.10.2"
@ -5810,19 +5862,19 @@ metascraper-lang-detector@^4.10.2:
franc "~4.0.0" franc "~4.0.0"
iso-639-3 "~1.1.0" iso-639-3 "~1.1.0"
metascraper-lang@^5.8.9: metascraper-lang@^5.8.10:
version "5.8.9" version "5.8.10"
resolved "https://registry.yarnpkg.com/metascraper-lang/-/metascraper-lang-5.8.9.tgz#589bac0fdc523b5b6e6317a7b6295474eedfb872" resolved "https://registry.yarnpkg.com/metascraper-lang/-/metascraper-lang-5.8.10.tgz#b8827282dea500b68e49ebbe8b0081fb6b6584d5"
integrity sha512-VMiU+T9LFsra/bBc0w0+fw6lk8Snb/ULoIvHUF0+5wvkv4KzQicc0z1lTAL/28Et2Xa+R5Km5A9Ts7LYuQRqVw== integrity sha512-qydko4UkLGqTimKzT+AkcIaXOo7/GkHGtclGiLae80lHeKzI5NG7kYN4eMv1r4BfBkcluSNeJ/P532T6ZD2Y1Q==
dependencies: dependencies:
"@metascraper/helpers" "^5.8.7" "@metascraper/helpers" "^5.8.10"
metascraper-logo@^5.8.7: metascraper-logo@^5.8.10:
version "5.8.7" version "5.8.10"
resolved "https://registry.yarnpkg.com/metascraper-logo/-/metascraper-logo-5.8.7.tgz#5efb7e6c5f91ccad812e2d9ec3facfef179f40b6" resolved "https://registry.yarnpkg.com/metascraper-logo/-/metascraper-logo-5.8.10.tgz#8e0dc0296d71db03307584ecdb57cd3fcbad1d4b"
integrity sha512-QudGVJBBeXLWU54Xw2PmnsTf+qPUnbyYaOl4aFLg2wkLLza1GbuvOYGMiH9Y8k0WcRoesi9sQk+P0a/611blew== integrity sha512-l5LkzZcVzrKclzf3JGx2cnCtPI/8Rf+EQV/SfXUqz7FUwgfT3uzRw9wBbqP25056ukh6aOuywGClTdnEu2PJcw==
dependencies: dependencies:
"@metascraper/helpers" "^5.8.7" "@metascraper/helpers" "^5.8.10"
metascraper-publisher@^5.8.7: metascraper-publisher@^5.8.7:
version "5.8.7" version "5.8.7"
@ -5831,20 +5883,20 @@ metascraper-publisher@^5.8.7:
dependencies: dependencies:
"@metascraper/helpers" "^5.8.7" "@metascraper/helpers" "^5.8.7"
metascraper-soundcloud@^5.8.9: metascraper-soundcloud@^5.8.10:
version "5.8.9" version "5.8.10"
resolved "https://registry.yarnpkg.com/metascraper-soundcloud/-/metascraper-soundcloud-5.8.9.tgz#5d02538078114c5ab25c46df4afc3f45a94b3d7c" resolved "https://registry.yarnpkg.com/metascraper-soundcloud/-/metascraper-soundcloud-5.8.10.tgz#c281a35e2e7289006bd304dfb4074f01451e7f26"
integrity sha512-0otAe2E4N/KN2UqopJAM9NFZfSMyll2Q0XKhicfV/d+6Q1ERT7LWA/vwhBmxFwQzzX2mxZ8JFKeXUf6OZqEvVg== integrity sha512-IBGGBFrzRiS1bTyR9+eJwv+fPvC8KoggpAZnGPABep4ZhfajblI3B+8U1kIXHMaFR4b1BaD4d+tWh3gNLZCjwQ==
dependencies: dependencies:
"@metascraper/helpers" "^5.8.7" "@metascraper/helpers" "^5.8.10"
tldts "~5.6.2" tldts "~5.6.2"
metascraper-title@^5.8.7: metascraper-title@^5.8.10:
version "5.8.7" version "5.8.10"
resolved "https://registry.yarnpkg.com/metascraper-title/-/metascraper-title-5.8.7.tgz#aecbbd9515bd74d2aeafa587c83447d926508ba0" resolved "https://registry.yarnpkg.com/metascraper-title/-/metascraper-title-5.8.10.tgz#c25dc8e8ad7073c18c8d25db0b855f62d3d986bd"
integrity sha512-u+5KeJbsFKpi+pMnG71Gd49OLDQpkjiGIRTddhCZQhb45qHoTlGKN1nZuQ8nqJI6+ARWicFqtquomkaRXfBEnw== integrity sha512-CauBJmLYtS+AZ9KJfnfJHp/tzUTo9yup56P/7aaOBcfVA5PWg3xdI1lVXJegmiTsBCyDEzWRVJ41f/ZlMjbAsg==
dependencies: dependencies:
"@metascraper/helpers" "^5.8.7" "@metascraper/helpers" "^5.8.10"
lodash "~4.17.15" lodash "~4.17.15"
metascraper-url@^5.8.7: metascraper-url@^5.8.7:
@ -5854,20 +5906,20 @@ metascraper-url@^5.8.7:
dependencies: dependencies:
"@metascraper/helpers" "^5.8.7" "@metascraper/helpers" "^5.8.7"
metascraper-video@^5.8.9: metascraper-video@^5.8.10:
version "5.8.9" version "5.8.10"
resolved "https://registry.yarnpkg.com/metascraper-video/-/metascraper-video-5.8.9.tgz#23c0fe71fae5088bc8e11bfa537eff80658aa6d9" resolved "https://registry.yarnpkg.com/metascraper-video/-/metascraper-video-5.8.10.tgz#c43bdc3d4dc7ff97b94d45e0050fb50091da27be"
integrity sha512-xaimkGz1Txsd9qHUN2U5HyFMP8tkrb5LuW8bCo+0kdTu5c00HGurvs0/BpWrTW/CzUQBNl/uEybeDXm8J++03g== integrity sha512-ofO7OLt73iMZM6IkA3iHtD1EzbEeiTYJK/xKBp+Awyl/dLUWKfsFjOAjkz9XDzLANRT+7+rwzqQmc+a2/rBVVg==
dependencies: dependencies:
"@metascraper/helpers" "^5.8.7" "@metascraper/helpers" "^5.8.10"
lodash "~4.17.15" lodash "~4.17.15"
metascraper-youtube@^5.8.9: metascraper-youtube@^5.8.10:
version "5.8.9" version "5.8.10"
resolved "https://registry.yarnpkg.com/metascraper-youtube/-/metascraper-youtube-5.8.9.tgz#595f5e384e0db519378ca2023bd8aa6603866c9d" resolved "https://registry.yarnpkg.com/metascraper-youtube/-/metascraper-youtube-5.8.10.tgz#c2b84b9faf8d4bd326a0a048e61cdbeefc7263ab"
integrity sha512-Zuew1tLSC14ceL9ZaNvlQ4GmFopbYDalr8gL+Ofo4ha4jKyX58VaPQtmIgASAJv/jlOXd9zCwEdhNw8/YyZZWw== integrity sha512-2QLqIqc8FWGJmGEwvoWDdEZnSCLg5lzH/3qZm0P9joFGA6WWrfpaONCVW4M72xfVHv/WwEblKZERzlbJNEhGVg==
dependencies: dependencies:
"@metascraper/helpers" "^5.8.7" "@metascraper/helpers" "^5.8.10"
get-video-id "~3.1.4" get-video-id "~3.1.4"
is-reachable "~4.0.0" is-reachable "~4.0.0"
p-locate "~4.1.0" p-locate "~4.1.0"
@ -6100,7 +6152,7 @@ neo-async@^2.6.0:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
neo4j-driver@^1.7.3, neo4j-driver@^1.7.5, neo4j-driver@~1.7.6: neo4j-driver@^1.7.3, neo4j-driver@^1.7.6, neo4j-driver@~1.7.6:
version "1.7.6" version "1.7.6"
resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-1.7.6.tgz#eccb135a71eba9048c68717444593a6424cffc49" resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-1.7.6.tgz#eccb135a71eba9048c68717444593a6424cffc49"
integrity sha512-6c3ALO3vYDfUqNoCy8OFzq+fQ7q/ab3LCuJrmm8P04M7RmyRCCnUtJ8IzSTGbiZvyhcehGK+azNDAEJhxPV/hA== integrity sha512-6c3ALO3vYDfUqNoCy8OFzq+fQ7q/ab3LCuJrmm8P04M7RmyRCCnUtJ8IzSTGbiZvyhcehGK+azNDAEJhxPV/hA==
@ -6109,10 +6161,10 @@ neo4j-driver@^1.7.3, neo4j-driver@^1.7.5, neo4j-driver@~1.7.6:
text-encoding-utf-8 "^1.0.2" text-encoding-utf-8 "^1.0.2"
uri-js "^4.2.2" uri-js "^4.2.2"
neo4j-graphql-js@^2.10.0: neo4j-graphql-js@^2.10.2:
version "2.10.0" version "2.10.2"
resolved "https://registry.yarnpkg.com/neo4j-graphql-js/-/neo4j-graphql-js-2.10.0.tgz#4298793756d839dedb98bc3e50a2bd40a311874d" resolved "https://registry.yarnpkg.com/neo4j-graphql-js/-/neo4j-graphql-js-2.10.2.tgz#e67d1aab6441b28f276adf0f6d655720983b9b84"
integrity sha512-jRdIyw+DHg9gfB6pWKb1ZHMR9rXIl7qf51efjUHIRHRbVR3RCcw1cKyONkq4LE8v2bHc7QDrKwJs+GQ1SRxDug== integrity sha512-CgtKEgrWgSJBjuKQ5CEPt4tcG1z14oAB3UWQjX8scDlUag0iWofgzpPlrc3brn+RitfeEc3FuMSru8E9dVDJPg==
dependencies: dependencies:
"@babel/runtime" "^7.5.5" "@babel/runtime" "^7.5.5"
"@babel/runtime-corejs2" "^7.5.5" "@babel/runtime-corejs2" "^7.5.5"
@ -6122,14 +6174,14 @@ neo4j-graphql-js@^2.10.0:
lodash "^4.17.15" lodash "^4.17.15"
neo4j-driver "^1.7.3" neo4j-driver "^1.7.3"
neode@^0.3.3: neode@^0.3.6:
version "0.3.3" version "0.3.6"
resolved "https://registry.yarnpkg.com/neode/-/neode-0.3.3.tgz#a539830cce6f6e4825462f6cb03f2969a0003f1b" resolved "https://registry.yarnpkg.com/neode/-/neode-0.3.6.tgz#7daf791eff6d170e52c338ea2e5cca6fdc6bfbe3"
integrity sha512-pArHG1hD2kVwrzLlz6B1+IgdOJRQj/BgR6KzH6DlVzSA6geoZRe68fbpvmOJtzyPU7iuUYxXVk87PpPM1A7dlg== integrity sha512-jCskCPobtHpsIIYQD72h5lRjMJEX70KwIeqgpt1VOLI+d1zJZvUlDkcOKgarAW0fmwtHIrPOP6mLPe5G/ZG9+g==
dependencies: dependencies:
"@hapi/joi" "^15.1.0" "@hapi/joi" "^15.1.0"
dotenv "^4.0.0" dotenv "^4.0.0"
neo4j-driver "^1.7.5" neo4j-driver "^1.7.6"
uuid "^3.3.2" uuid "^3.3.2"
next-tick@^1.0.0: next-tick@^1.0.0:
@ -6204,12 +6256,12 @@ node-pre-gyp@^0.12.0:
semver "^5.3.0" semver "^5.3.0"
tar "^4" tar "^4"
node-releases@^1.1.25: node-releases@^1.1.42:
version "1.1.28" version "1.1.42"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.28.tgz#503c3c70d0e4732b84e7aaa2925fbdde10482d4a" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.42.tgz#a999f6a62f8746981f6da90627a8d2fc090bbad7"
integrity sha512-AQw4emh6iSXnCpDiFe0phYcThiccmkNWMZnFZ+lDJjAP8J0m2fVd59duvUUyuTirQOhIAajTFkzG6FHCLBO59g== integrity sha512-OQ/ESmUqGawI2PRX+XIRao44qWYBBfN54ImQYdWVTQqUckuejOg76ysSqDBK8NG3zwySRVnX36JwDQ6x+9GxzA==
dependencies: dependencies:
semver "^5.3.0" semver "^6.3.0"
nodemailer-html-to-text@^3.1.0: nodemailer-html-to-text@^3.1.0:
version "3.1.0" version "3.1.0"
@ -6218,15 +6270,15 @@ nodemailer-html-to-text@^3.1.0:
dependencies: dependencies:
html-to-text "^5.1.1" html-to-text "^5.1.1"
nodemailer@^6.4.1: nodemailer@^6.4.2:
version "6.4.1" version "6.4.2"
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.4.1.tgz#f70b40355b7b08f1f80344b353970a4f8f664370" resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.4.2.tgz#7147550e32cdc37453380ab78d2074533966090a"
integrity sha512-mSQAzMim8XIC1DemK9TifDTIgASfoJEllG5aC1mEtZeZ+FQyrSOdGBRth6JRA1ERzHQCET3QHVSd9Kc6mh356g== integrity sha512-g0n4nH1ONGvqYo1v72uSWvF/MRNnnq1LzmSzXb/6EPF3LFb51akOhgG3K2+aETAsJx90/Q5eFNTntu4vBCwyQQ==
nodemon@~2.0.1: nodemon@~2.0.2:
version "2.0.1" version "2.0.2"
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.1.tgz#cec436f8153ad5d3e6c27c304849a06cabea71cc" resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.2.tgz#9c7efeaaf9b8259295a97e5d4585ba8f0cbe50b0"
integrity sha512-UC6FVhNLXjbbV4UzaXA3wUdbEkUZzLGgMGzmxvWAex5nzib/jhcSHVFlQODdbuUHq8SnnZ4/EABBAbC3RplvPg== integrity sha512-GWhYPMfde2+M0FsHnggIHXTqPDHXia32HRhh6H0d75Mt9FKUoCBvumNHr7LdrpPBTKxsWmIEOjoN+P4IU6Hcaw==
dependencies: dependencies:
chokidar "^3.2.2" chokidar "^3.2.2"
debug "^3.2.6" debug "^3.2.6"
@ -6382,7 +6434,12 @@ object-hash@^2.0.0:
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.0.0.tgz#7c4cc341eb8b53367312a7c546142f00c9e0ea20" resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.0.0.tgz#7c4cc341eb8b53367312a7c546142f00c9e0ea20"
integrity sha512-I7zGBH0rDKwVGeGZpZoFaDhIwvJa3l1CZE+8VchylXbInNiCj7sxxea9P5dTM4ftKR5//nrqxrdeGSTWL2VpBA== integrity sha512-I7zGBH0rDKwVGeGZpZoFaDhIwvJa3l1CZE+8VchylXbInNiCj7sxxea9P5dTM4ftKR5//nrqxrdeGSTWL2VpBA==
object-keys@^1.0.11, object-keys@^1.0.12: object-inspect@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==
object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
@ -7259,7 +7316,7 @@ resolve@1.1.7:
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=
resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.0, resolve@^1.12.0, resolve@^1.3.2, resolve@^1.3.3, resolve@^1.5.0: resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.3.2, resolve@^1.3.3, resolve@^1.5.0:
version "1.12.0" version "1.12.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6"
integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w== integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==
@ -7452,6 +7509,11 @@ serve-static@1.14.1:
version "1.14.1" version "1.14.1"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
dependencies:
encodeurl "~1.0.2"
escape-html "~1.0.3"
parseurl "~1.3.3"
send "0.17.1"
set-blocking@^2.0.0, set-blocking@~2.0.0: set-blocking@^2.0.0, set-blocking@~2.0.0:
version "2.0.0" version "2.0.0"
@ -7788,6 +7850,22 @@ string.prototype.padend@^3.0.0:
es-abstract "^1.4.3" es-abstract "^1.4.3"
function-bind "^1.0.2" function-bind "^1.0.2"
string.prototype.trimleft@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz#6cc47f0d7eb8d62b0f3701611715a3954591d634"
integrity sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==
dependencies:
define-properties "^1.1.3"
function-bind "^1.1.1"
string.prototype.trimright@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz#669d164be9df9b6f7559fa8e89945b168a5a6c58"
integrity sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==
dependencies:
define-properties "^1.1.3"
function-bind "^1.1.1"
string_decoder@^1.1.1: string_decoder@^1.1.1:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
@ -8172,7 +8250,7 @@ ts-invariant@^0.4.0:
dependencies: dependencies:
tslib "^1.9.3" tslib "^1.9.3"
tslib@1.10.0, tslib@^1.9.0, tslib@^1.9.3: tslib@1.10.0, tslib@^1.10.0, tslib@^1.9.0, tslib@^1.9.3:
version "1.10.0" version "1.10.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==

View File

@ -1,6 +0,0 @@
{
"BACKEND_HOST": "http://localhost:4000",
"NEO4J_URI": "bolt://localhost:7687",
"NEO4J_USERNAME": "neo4j",
"NEO4J_PASSWORD": "letmein"
}

View File

@ -16,12 +16,7 @@ First, you have to tell cypress how to connect to your local neo4j database
among other things. You can copy our template configuration and change the new among other things. You can copy our template configuration and change the new
file according to your needs. file according to your needs.
Make sure you are at the root level of the project. Then: To start the services that are required for cypress testing, run:
```bash
# in the top level folder Human-Connection/
$ cp cypress.env.template.json cypress.env.json
```
To start the services that are required for cypress testing, run this:
```bash ```bash
# in the top level folder Human-Connection/ # in the top level folder Human-Connection/

View File

@ -3,6 +3,14 @@ import { When, Then } from "cypress-cucumber-preprocessor/steps";
const narratorAvatar = const narratorAvatar =
"https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg"; "https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg";
When("I type in a comment with {int} characters", size => {
var c="";
for (var i = 0; i < size; i++) {
c += "c"
}
cy.get(".editor .ProseMirror").type(c);
});
Then("I click on the {string} button", text => { Then("I click on the {string} button", text => {
cy.get("button") cy.get("button")
.contains(text) .contains(text)
@ -23,6 +31,16 @@ Then("I should see my comment", () => {
.should("contain", "today at"); .should("contain", "today at");
}); });
Then("I should see the entirety of my comment", () => {
cy.get("div.comment")
.should("not.contain", "show more")
});
Then("I should see an abreviated version of my comment", () => {
cy.get("div.comment")
.should("contain", "show more")
});
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");
}); });

View File

@ -1,5 +1,6 @@
import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps' import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps'
import { VERSION } from '../../constants/terms-and-conditions-version.js' import { VERSION } from '../../constants/terms-and-conditions-version.js'
import { gql } from '../../../backend/src/helpers/jest'
/* global cy */ /* global cy */
@ -128,7 +129,7 @@ Given('somebody reported the following posts:', table => {
cy.factory() cy.factory()
.create('User', submitter) .create('User', submitter)
.authenticateAs(submitter) .authenticateAs(submitter)
.mutate(`mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) { .mutate(gql`mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
fileReport(resourceId: $resourceId, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription) { fileReport(resourceId: $resourceId, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription) {
id id
} }

View File

@ -20,3 +20,19 @@ Feature: Post Comment
Then my comment should be successfully created Then my comment should be successfully created
And I should see my comment And I should see my comment
And the editor should be cleared And the editor should be cleared
Scenario: View medium length comments
Given I visit "post/bWBjpkTKZp/101-essays"
And I type in a comment with 305 characters
And I click on the "Comment" button
Then my comment should be successfully created
And I should see the entirety of my comment
And the editor should be cleared
Scenario: View long comments
Given I visit "post/bWBjpkTKZp/101-essays"
And I type in a comment with 1205 characters
And I click on the "Comment" button
Then my comment should be successfully created
And I should see an abreviated version of my comment
And the editor should be cleared

View File

@ -18,8 +18,8 @@ import helpers from "./helpers";
import users from "../fixtures/users.json"; import users from "../fixtures/users.json";
import { GraphQLClient, request } from 'graphql-request' import { GraphQLClient, request } from 'graphql-request'
import { gql } from '../../backend/src/helpers/jest' import { gql } from '../../backend/src/helpers/jest'
import config from '../../backend/src/config'
const backendHost = Cypress.env('BACKEND_HOST')
const switchLang = name => { const switchLang = name => {
cy.get(".locale-menu").click(); cy.get(".locale-menu").click();
cy.contains(".locale-menu-popover a", name).click(); cy.contains(".locale-menu-popover a", name).click();
@ -31,7 +31,7 @@ const authenticatedHeaders = async (variables) => {
login(email: $email, password: $password) login(email: $email, password: $password)
} }
` `
const response = await request(backendHost, mutation, variables) const response = await request(config.GRAPHQL_URI, mutation, variables)
return { authorization: `Bearer ${response.login}` } return { authorization: `Bearer ${response.login}` }
} }
@ -100,8 +100,7 @@ Cypress.Commands.add(
'authenticateAs', 'authenticateAs',
async ({email, password}) => { async ({email, password}) => {
const headers = await authenticatedHeaders({ email, password }) const headers = await authenticatedHeaders({ email, password })
console.log(headers) return new GraphQLClient(config.GRAPHQL_URI, { headers })
return new GraphQLClient(backendHost, { headers })
} }
) )

View File

@ -1,16 +1,10 @@
import Factory from '../../backend/src/seed/factories' import Factory from '../../backend/src/seed/factories'
import { getDriver, neode as getNeode } from '../../backend/src/bootstrap/neo4j' import { getDriver, getNeode } from '../../backend/src/bootstrap/neo4j'
import setupNeode from '../../backend/src/bootstrap/neode'
import neode from 'neode' import neode from 'neode'
const backendHost = Cypress.env('SEED_SERVER_HOST') const neo4jDriver = getDriver()
const neo4jConfigs = { const neodeInstance = getNeode()
uri: Cypress.env('NEO4J_URI'), const factoryOptions = { neo4jDriver, neodeInstance }
username: Cypress.env('NEO4J_USERNAME'),
password: Cypress.env('NEO4J_PASSWORD')
}
const neo4jDriver = getDriver(neo4jConfigs)
const factoryOptions = { seedServerHost: backendHost, neo4jDriver, neodeInstance: setupNeode(neo4jConfigs)}
const factory = Factory(factoryOptions) const factory = Factory(factoryOptions)
beforeEach(async () => { beforeEach(async () => {
@ -18,7 +12,7 @@ beforeEach(async () => {
}) })
Cypress.Commands.add('neode', () => { Cypress.Commands.add('neode', () => {
return setupNeode(neo4jConfigs) return neodeInstance
}) })
Cypress.Commands.add( Cypress.Commands.add(
'first', 'first',

814
locale/ru.json Normal file
View File

@ -0,0 +1,814 @@
{
"actions": {
"cancel": "Отменить",
"create": "Создать",
"delete": "Удалить",
"edit": "Редактировать",
"loading": "загрузка",
"loadMore": "Загрузить ещё",
"save": "Сохранить"
},
"admin": {
"categories": {
"categoryName": "Имя",
"name": "Категории",
"postCount": "Посты"
},
"dashboard": {
"comments": "Комментарии",
"follows": "Подписки",
"invites": "Приглашения",
"name": "Панель управления",
"notifications": "Уведомления",
"organizations": "Организации",
"posts": "Посты",
"projects": "Проекты",
"shouts": "Выкрики",
"users": "Пользователи"
},
"donations": {
"goal": "Необходимы ежемесячные пожертвования",
"name": "Информация о пожертвованиях",
"progress": "Пожертвования собраны",
"successfulUpdate": "Информация о пожертвованиях успешно обновлена!"
},
"hashtags": {
"name": "Хэштеги",
"nameOfHashtag": "Имя",
"number": "№",
"tagCount": "Посты",
"tagCountUnique": "Пользователи"
},
"invites": {
"description": "Приглашения — это замечательный способ завести друзей в своей сети ...",
"name": "Пригласить пользователей",
"title": "Пригласить людей"
},
"name": "Администрирование",
"notifications": {
"name": "Уведомления"
},
"organizations": {
"name": "Организации"
},
"pages": {
"name": "Страницы"
},
"settings": {
"name": "Настройки"
},
"tags": {
"name": "Теги",
"tagCount": "Посты",
"tagCountUnique": "Пользователи"
},
"users": {
"empty": "Пользователи не найдены",
"form": {
"placeholder": "Электронная почта, имя или описание"
},
"name": "Пользователи",
"table": {
"columns": {
"createdAt": "Дата создания",
"email": "Эл. почта",
"name": "Имя",
"number": "№",
"role": "Роль",
"slug": "Slug"
}
}
}
},
"code-of-conduct": {
"consequences": {
"description": "Если участник сообщества проявляет неприемлемое поведение, ответственные операторы, модераторы и администраторы сети могут принять соответствующие меры, включая, но не ограничиваясь:",
"list": {
"0": "Просьба о немедленном прекращении неприемлемого поведения",
"1": "Блокирование или удаление комментариев",
"2": "Временное исключение из соответствующего поста или другого контента",
"3": "Блокирование или удаление контента",
"4": "Временный запрет на добавление контента",
"5": "Временное исключение из сети",
"6": "Окончательное исключение из сети",
"7": "Передача сведений о нарушениях немецкого законодательства.",
"8": "Пропаганда или поощрение такого поведения."
},
"title": "Последствия неприемлемого поведения"
},
"expected-behaviour": {
"description": "Мы ожидаем и требуем от всех членов сообщества предерживаться следующих правил поведения:",
"list": {
"0": "Будьте внимательны и уважительны к тому, что пишете и делаете.",
"1": "Пытайтесь сотрудничать, прежде чем возникнет конфликт.",
"2": "Воздерживайтесь от поведения и высказываний, унижающих достоинство, дискриминационного или преследующего характера.",
"3": "Будьте внимательны к своему окружению и другим участникам. Информируйте лидеров сообщества об опасных ситуациях, когда кто-либо попал в беду или нарушает настоящий Кодекс поведения, даже если они кажутся незначительными."
},
"title": "Ожидаемое поведение"
},
"get-help": "Если вы стали жертвой или свидетелем неприемлемого поведения или у вас возникли какие-либо другие проблемы, пожалуйста, как можно скорее сообщите об этом организатору сообщества и укажите ссылку на соответствующий контент:",
"preamble": {
"description": "Human Connection - это некоммерческая социальная сеть знаний и действий следующего поколения. Создана людьми для людей. С открытым исходным кодом, справедливая и прозрачная. Для позитивных локальных и глобальных изменений во всех сферах жизни. Мы полностью перестраиваем публичный обмен знаниями, идеями и проектами. Функции Human Connection объединяют людей офлайн и онлайн так что мы можем сделать мир лучше.",
"title": "Преамбула"
},
"purpose": {
"description": "С помощью этих правил поведения мы регулируем основные принципы поведения в нашей социальной сети. При этом Устав ООН по правам человека является нашей ориентацией и лежит в основе нашего понимания ценностей. Правила поведения служат руководящими принципами для личного выступления и общения друг с другом. Любой, кто является активным пользователем в сети Human Connection, публикует сообщения, комментирует или контактирует с другими пользователями, в том числе за пределами сети, признает эти правила поведения обязательными.",
"title": "Цель"
},
"subheader": "социальной сети \"Human Connection gGmbH\"",
"unacceptable-behaviour": {
"description": "В нашем сообществе неприемлемо следующее поведение:",
"list": {
"0": "Дискриминационные посты, комментарии, высказывания или оскорбления, в частности, касающиеся пола, сексуальной ориентации, расы, религии, политической или мировоззренческой ориентации, или инвалидности.",
"1": "Публикация или ссылка на явно порнографические материалы.",
"2": "Прославление или умаление жестоких, или бесчеловечных актов насилия.",
"3": "Публикация персональных данных других лиц без их согласия или угрозы (\"Доксинг\").",
"4": "Преднамеренное запугивание или преследование.",
"5": "Рекламировать продукты и услуги с коммерческим намерением.",
"6": "Преступное поведение или нарушение немецкого права.",
"7": "Одобрение или поощрение недопустимого поведения."
},
"title": "Недопустимое поведение"
}
},
"comment": {
"content": {
"unavailable-placeholder": "...этот комментарий больше не доступен"
},
"delete": "Удалить комментарий",
"edit": "Редактировать комментарий",
"edited": "Изменен",
"menu": {
"delete": "Удалить комментарий",
"edit": "Редактировать комментарий"
},
"show": {
"less": "показать меньше",
"more": "показать больше"
}
},
"common": {
"category": "Категория ::: Категории ::: Категории",
"comment": "Комментарий::: Комментарии::: Комментарии",
"letsTalk": "Давай поговорим",
"loading": "загрузка",
"loadMore": "Загрузить ещё",
"moreInfo": "Больше информации",
"name": "Имя",
"organization": "Организация ::: Организации ::: Организации",
"post": "Пост ::: Посты ::: Посты",
"project": "Проект ::: Проекты ::: Проекты",
"reportContent": "Отчет",
"shout": "Выкрик ::: Выкрики ::: Выкрики",
"tag": "Тег ::: Теги ::: Теги",
"takeAction": "Принять меры",
"user": "Пользователь ::: Пользователи ::: Пользователи",
"validations": {
"categories": "Выберите от одной то трех категорий",
"email": "должен быть корректный адрес электронной почты",
"url": "должен быть корректный URL"
},
"versus": "Против"
},
"components": {
"enter-nonce": {
"form": {
"description": "Откройте папку \\\"Входящие\\\" и введите код из сообщения.",
"next": "Продолжить",
"nonce": "Введите код",
"validations": {
"length": "длина должна быть 6 символов"
}
}
},
"password-reset": {
"change-password": {
"error": "Смена пароля не удалась. Может быть, код безопасности был неправильным?",
"help": "В случае возникновения проблем, не стесняйся обращаться за помощью, отправив нам письмо по адресу:",
"success": "Смена пароля прошла успешно!"
},
"request": {
"form": {
"description": "На указанный адрес электронной почты будет отправлено сообщение с инструкциями для сброса пароля.",
"submit": "Отправить запрос",
"submitted": "На адрес <b>{email}<\/b>было отправлено электронное письмо с дальнейшими инструкциями"
},
"title": "Сбросить пароль"
}
},
"registration": {
"create-user-account": {
"error": "Не удалось создать учетную запись!",
"help": "Может быть, подтверждение было недействительным? В случае возникновения проблем, не стесняйтесь обращаться за помощью, отправив нам письмо по электронной почте:",
"success": "Учетная запись успешно создана!",
"title": "Создать учетную запись"
},
"signup": {
"form": {
"data-privacy": "Я прочитал и понял <a href=\"https:\/\/human-connection.org\/datenschutz\/\" target=\"_blank\"><ds-text bold color=\"primary\" >Заявление о конфиденциальности<\/ds-text><\/a>",
"description": "Для начала работы введите свой адрес электронной почты:",
"errors": {
"email-exists": "Уже есть учетная запись пользователя с этим адресом электронной почты!",
"invalid-invitation-token": "Похоже, что приглашение уже было использовано. Ссылку из приглашения можно использовать только один раз."
},
"invitation-code": "Код приглашения: <b>{code}<\/b>",
"minimum-age": "Мне 18 лет или более",
"no-commercial": "У меня нет коммерческих намерений, и я не представляю коммерческое предприятие или организацию.",
"no-political": "Я не от имени какой-либо партии или политической организации в сети.",
"submit": "Создать учетную запись",
"success": "Письмо со ссылкой для завершения регистрации было отправлено на <b> {email} <\/b>",
"terms-and-condition": "Принимаю <a href=\"\/terms-and-conditions\"><ds-text bold color=\"primary\" >Условия и положения<\/ds-text><\/a>."
},
"title": "Присоединяйся к Human Connection!",
"unavailable": "К сожалению, публичная регистрация пользователей на этом сервере сейчас недоступна."
}
}
},
"contribution": {
"categories": {
"infoSelectedNoOfMaxCategories": "Выбрано {chosen} из {max} категорий"
},
"category": {
"name": {
"animal-protection": "Защита животных",
"art-culture-sport": "Искусство, культура и спорт",
"consumption-sustainability": "Потребление и стабильность",
"cooperation-development": "Сотрудничество и развитие",
"democracy-politics": "Демократия и политика",
"economy-finances": "Экономика и финансы",
"education-sciences": "Образование и наука",
"energy-technology": "Энергия и технологии",
"environment-nature": "Окружающая среда и природа",
"freedom-of-speech": "Свобода слова",
"global-peace-nonviolence": "Глобальный мир и борьба с насилием",
"happiness-values": "Счастье и ценности",
"health-wellbeing": "Здоровье и благополучие",
"human-rights-justice": "Права человека и справедливость",
"it-internet-data-privacy": "ИТ, интернет и конфиденциальность",
"just-for-fun": "Просто для удовольствия"
}
},
"delete": "Удалить",
"edit": "Редактировать",
"emotions-label": {
"angry": "Возмутительно",
"cry": "Плачу",
"funny": "Смешно",
"happy": "Счастлив",
"surprised": "Удивлен"
},
"filterALL": "Просмотреть все посты",
"filterFollow": "Показать сообщения пользователей, на которых я подписан",
"languageSelectLabel": "Язык",
"languageSelectText": "Выберите язык",
"newPost": "Создать пост",
"success": "Сохранено!",
"teaserImage": {
"cropperConfirm": "Подтвердить"
},
"title": "Заголовок"
},
"delete": {
"cancel": "Отменить",
"comment": {
"message": "Вы уверены, что хотите удалить комментарий \"<b>{name}<\/b>\"?",
"success": "Комментарий успешно удален!",
"title": "Удалить комментарий",
"type": "Комментарий"
},
"contribution": {
"message": "Вы уверены, что хотите удалить пост \"<b>{name}<\/b>\"?",
"success": "Пост успешно удален!",
"title": "Удалить пост",
"type": "Пост"
},
"submit": "Удалить"
},
"disable": {
"cancel": "Отменить",
"comment": {
"message": "Вы действительно хотите отключить комментарий от «<b>{name}<\/b>»?",
"title": "Отключить комментарий",
"type": "Комментарий"
},
"contribution": {
"message": "Вы действительно хотите отключить пост «<b>{name}<\/b>»?",
"title": "Отключить пост",
"type": "Пост"
},
"submit": "Отключить",
"success": "Успешно отключен",
"user": {
"message": "Вы действительно хотите отключить пользователя «<b>{name}<\/b>»?",
"title": "Отключить пользователя",
"type": "Пользователь"
}
},
"donations": {
"amount-of-total": "{amount} из {total} € собрано",
"donate-now": "Пожертвуйте сейчас",
"donations-for": "Пожертвования для"
},
"editor": {
"embed": {
"always_allow": "Всегда отображать содержимое сторонних производителей (эту настройку можно изменить в любое время).",
"data_privacy_info": "Ваши данные еще не были переданы третьим лицам. Если вы воспроизведёте это видео, следующий провайдер, вероятно, зарегистрирует ваши данные пользователя:",
"data_privacy_warning": "Предупреждение о конфиденциальности данных!",
"play_now": "Смотреть сейчас"
},
"hashtag": {
"addHashtag": "Новый хэштег",
"addLetter": "Введите букву",
"noHashtagsFound": "Хэштеги не найдены"
},
"mention": {
"noUsersFound": "Пользователи не найдены"
},
"placeholder": "Поделитесь своими вдохновляющими мыслями ..."
},
"filter-menu": {
"clearSearch": "Очистить поиск",
"hashtag-search": "Поиск по #{hashtag}",
"title": "Ваш фильтр пузыря"
},
"filter-posts": {
"categories": {
"all": "Все",
"header": "Категории"
},
"followers": {
"label": "Мои подписки"
},
"general": {
"header": "Другие фильтры"
},
"language": {
"all": "Все",
"header": "Языки"
}
},
"followButton": {
"follow": "Подписаться",
"following": "Вы подписаны"
},
"index": {
"change-filter-settings": "Измените настройки фильтра, чтобы получить больше результатов.",
"no-results": "Посты не найдены."
},
"login": {
"copy": "Авторизуйтесь, если у вас уже есть учетная запись Human Connection.",
"email": "Электронная почта",
"failure": "Неверный адрес электронной почты или пароль.",
"forgotPassword": "Забыли пароль?",
"hello": "Здравствуйте",
"login": "Вход",
"logout": "Выйти",
"moreInfo": "Что такое Human Connection?",
"moreInfoHint": "на страницу проекта",
"moreInfoURL": "https:\/\/human-connection.org\/en\/",
"no-account": "У вас нет аккаунта?",
"password": "Пароль",
"register": "Зарегистрируйтесь",
"success": "Вы вошли в систему!"
},
"maintenance": {
"explanation": "В данный момент мы проводим плановое техническое обслуживание, пожалуйста, повторите попытку позже.",
"questions": "Любые вопросы или сообщения о проблемах отправляйте на электронную почту",
"title": "Human Connection на техническом обслуживании"
},
"moderation": {
"name": "Модерация",
"reports": {
"author": "Автор",
"content": "Содержа́ние",
"decideButton": "Подтвердить",
"decided": "Решил",
"decideModal": {
"cancel": "Отменить",
"Comment": {
"disable": {
"message": "Вы действительно хотите, чтобы комментарий \"<b>{name}<\/b>\" остановиться и <b>отключен<\/b>?",
"title": "Окончательно отключить комментарий"
},
"enable": {
"message": "Вы действительно хотите, чтобы комментарий \"<b>{name}<\/b>\" остановиться и <b>включен<\/b>?",
"title": "Окончательно включить комментарий"
}
},
"Post": {
"disable": {
"message": "Вы действительно хотите, чтобы пост \"<b>{name}<\/b>\" остановиться и <b>отключен<\/b>?",
"title": "Окончательно отключить пост"
},
"enable": {
"message": "Вы действительно хотите, чтобы пост \"<b>{name}<\/b>\" остановиться и <b>включен<\/b>?",
"title": "Окончательно включить пост"
}
},
"submit": "Подтвердить решение",
"User": {
"disable": {
"message": "Вы действительно хотите, чтобы пользователь \"<b>{name}<\/b>\" остановиться и <b>отключен<\/b>?",
"title": "Окончательно отключить пользователя"
},
"enable": {
"message": "Вы уверены, что хотите поделиться пользователем \"<b>{name}<\/b>\"?",
"title": "Окончательно включить пост"
}
}
},
"decision": "Решение",
"DecisionSuccess": "Решил успешно!",
"disabled": "Отключен",
"disabledAt": "Отключено на",
"disabledBy": "Отключил(а)",
"empty": "Поздравляю, модерировать нечего.",
"enabled": "Включен",
"enabledAt": "Включено на",
"enabledBy": "Включено с",
"filterLabel": {
"all": "Все",
"closed": "Закрыто",
"reviewed": "Рассмотренный",
"unreviewed": "Нерассмотренный"
},
"moreDetails": "Посмотреть подробности",
"name": "Отчеты",
"noDecision": "Нет решения!",
"numberOfUsers": "{count} пользователи",
"previousDecision": "Предыдущее решение:",
"reasonCategory": "Категория",
"reasonDescription": "Описание",
"reportedOn": "Дата",
"reporter": "Сообщил(а)",
"status": "Текущее состояние",
"submitter": "Сообщил(а)"
}
},
"notifications": {
"comment": "Комментарий",
"content": "Контент",
"empty": "Извините, на данный момент у вас нет уведомлений.",
"filterLabel": {
"all": "Все",
"read": "Прочитанные",
"unread": "Непрочитанные"
},
"pageLink": "Все уведомления",
"post": "Пост",
"reason": {
"commented_on_post": "Комментарий к посту...",
"mentioned_in_comment": "Упоминание в комментарии....",
"mentioned_in_post": "Упоминание в посте...."
},
"title": "Уведомления",
"user": "Пользователь"
},
"post": {
"comment": {
"submit": "Комментировать",
"submitted": "Комментарий отправлен",
"updated": "Изменения сохраненные"
},
"edited": "Изменен",
"menu": {
"delete": "Удалить пост",
"edit": "Редактировать пост",
"pin": "Закрепить пост",
"pinnedSuccessfully": "Пост больше не закреплен!",
"unpin": "Открепить пост",
"unpinnedSuccessfully": "Пост успешно не закреплено!"
},
"moreInfo": {
"description": "Здесь содержится дополнительная информация по теме.",
"name": "Дополнительная информация",
"title": "Дополнительная информация",
"titleOfCategoriesSection": "Категории",
"titleOfHashtagsSection": "Хэштеги",
"titleOfRelatedContributionsSection": "Похожие посты"
},
"name": "Пост",
"pinned": "Объявление",
"takeAction": {
"name": "Действовать"
}
},
"profile": {
"commented": "Прокомментированные",
"follow": "Подписаться",
"followers": "Подписчики",
"following": "Подписки",
"invites": {
"description": "Введите адрес электронной почты для приглашения.",
"emailPlaceholder": "Электронная почта для приглашения",
"title": "Пригласите кого-нибудь в Human Connection!"
},
"memberSince": "Участник с",
"name": "Мой профиль",
"network": {
"andMore": "и ещё {number} человек... ::: и ещё {number} человека... ::: и ещё {number} человек...",
"followedBy": "ваши подписчики:",
"followedByNobody": "у вас нет подписчиков.",
"following": "подписан на:",
"followingNobody": "ни на кого не подписан.",
"title": "Сеть"
},
"shouted": "С выкриками",
"socialMedia": "Где еще я могу найти",
"userAnonym": "Анонимный"
},
"quotes": {
"african": {
"author": "Африканская пословица",
"quote": "Много маленьких людей делают много маленьких вещей во многих маленьких местах, что может изменить мир до неузнаваемости."
}
},
"release": {
"cancel": "Отменить",
"comment": {
"error": "Вы уже сообщили о комментарии!",
"message": "Вы уверены, что хотите показать комментарий \"<b>{name}<\/b>\"?",
"title": "Показать комментарий",
"type": "Комментарий"
},
"contribution": {
"error": "Вы уже сообщили о посте!",
"message": "Вы уверены, что хотите показать пост \"<b>{name}<\/b>\"?",
"title": "Показать пост",
"type": "Пост"
},
"submit": "Показать",
"success": "Успешно показан!",
"user": {
"error": "Вы уже сообщили о пользователе!",
"message": "Вы уверены, что хотите показать пользователя \"<b>{name}<\/b>\"?",
"title": "Показать пользователя",
"type": "Пользователь"
}
},
"report": {
"cancel": "Отменить",
"comment": {
"error": "Вы уже сообщили о посте!",
"message": "Вы действительно хотите сообщить о посте \"<b> {name} <\/b>\"?",
"title": "Пожаловаться на комментарий",
"type": "Комментарий"
},
"contribution": {
"error": "Вы уже сообщили о посте!",
"message": "Вы действительно хотите сообщить о посте \"<b>{name}<\/b>\"?",
"title": "Пожаловаться на пост",
"type": "Пожаловаться на пост"
},
"reason": {
"category": {
"invalid": "Пожалуйста, выберите подходящую категорию",
"label": "Выберите категорию:",
"options": {
"advert_products_services_commercial": "Реклама продуктов и услуг с коммерческим намерением.",
"criminal_behavior_violation_german_law": "Уголовное поведение или нарушении немецкого права.",
"discrimination_etc": "Дискриминационные посты, комментарии, заявления или оскорбления.",
"doxing": "Публикация персональных данных других лиц без их согласия или угроза публикации (\"Доксинг\").",
"glorific_trivia_of_cruel_inhuman_acts": "Прославление или умаление жестоких, или бесчеловечных актов насилия.",
"intentional_intimidation_stalking_persecution": "Преднамеренное запугивание или преследование.",
"other": "Другое ...",
"pornographic_content_links": "Публикация или ссылка на явно порнографический материал."
},
"placeholder": "Категория ..."
},
"description": {
"label": "Пожалуйста, объясните, почему хотите об этом сообщить?",
"placeholder": "Дополнительная информация ..."
}
},
"submit": "Отправить",
"success": "Спасибо за сообщение!",
"user": {
"error": "Вы уже сообщили о пользователе!",
"message": "Вы действительно хотите сообщить о пользователе \"<b>{name}<\/b>\"?",
"title": "Пожаловаться на пользователя",
"type": "Пользователь"
}
},
"search": {
"failed": "Ничего не найдено",
"hint": "Что вы хотите найти?",
"placeholder": "Поиск"
},
"settings": {
"blocked-users": {
"block": "Блокировать",
"columns": {
"name": "Имя",
"slug": "Псевдоним",
"unblock": "Разблокировать"
},
"empty": "Вы пока никого не блокировали.",
"explanation": {
"closing": "На данный момент этого должно быть достаточно, чтобы заблокированные пользователи больше вас не беспокоили.",
"intro": "Если блокируете другого пользователя, происходит следующее:",
"notifications": "Заблокированные пользователи больше не будут получать уведомления об упоминаниях в ваших постах.",
"search": "Посты заблокированных пользователей не отображаются в результатах поиска.",
"their-perspective": "И наоборот — заблокированный пользователь больше не видит ваши посты в своей ленте.",
"your-perspective": "Посты заблокированного пользователя не отображаются в персональной ленте."
},
"how-to": "Вы можете блокировать других пользователей на странице их профиля с помощью меню профиля.",
"name": "Заблокированные пользователи",
"unblock": "Разблокировать пользователей",
"unblocked": "{name} - снова разблокирован"
},
"data": {
"labelBio": "О себе",
"labelCity": "Город или регион",
"labelName": "Имя",
"labelSlug": "Уникальное имя пользователя",
"name": "Персональные данные",
"namePlaceholder": "Маша Медведева",
"success": "Персональные данные были успешно обновлены!"
},
"delete": {
"name": "Удалить аккаунт"
},
"deleteUserAccount": {
"accountDescription": "Обратите внимание, что ваши посты и комментарии важны для сообщества. Если вы все равно хотите их удалить, то вы должны отметить соответствующие опции ниже.",
"accountWarning": "Вы <b>НЕ СМОЖЕТЕ<\/b> восстановить свой аккаунт, посты или комментарии после удаления.",
"commentedCount": "Удалить мои комментарии: {count}",
"contributionsCount": "Удалить мои посты: {count}",
"name": "Удалить данные",
"pleaseConfirm": "<b class='is-danger'>Разрушительное действие!<\/b> Введите <b>{confirm}<\/b> для подтверждения.",
"success": "Аккаунт успешно удален!"
},
"download": {
"name": "Скачать данные"
},
"email": {
"change-successful": "Адрес электронной почты был успешно изменен.",
"labelEmail": "Адрес электронной почты",
"labelNewEmail": "Новый адрес электронной почты",
"labelNonce": "Введите свой код",
"name": "Электронная почта",
"submitted": "Электронное письмо с подтверждением отправлено на <b>{email}<\/b>.",
"success": "Новый адрес электронной почты был зарегистрирован.",
"validation": {
"same-email": "Это текущий адрес электронной почты."
},
"verification-error": {
"explanation": "Причины могут быть разными:",
"message": "Адрес электронной почты не может быть изменен.",
"reason": {
"invalid-nonce": "Правильно ли указан код подтверждения?",
"no-email-request": "Вы уверены, что отправляли запрос на изменение своего адреса электронной почты?"
},
"support": "Если проблема сохраняется, пожалуйста, свяжитесь с нами по электронной почте"
}
},
"embeds": {
"info-description": "Вот список сторонних провайдеров, чей контент может отображаться в форме вставок кода, например, в виде встроенных видео:",
"name": "Сторонний контент",
"status": {
"change": {
"allow": "Конечно.",
"deny": "Нет, не надо",
"question": "Вы хотите, чтобы вставки кода сторонних провайдеров всегда отображались?"
},
"description": "Значение по умолчанию -",
"disabled": {
"off": "сначала не отображать вставки кода сторонних провайдеров",
"on": "сразу отображать вставки кода сторонних провайдеров"
}
}
},
"invites": {
"name": "Приглашения"
},
"languages": {
"name": "Языки"
},
"name": "Настройки",
"organizations": {
"name": "Мои организации"
},
"privacy": {
"make-shouts-public": "Публиковать в моем публичном профиле статьи в которых я участвовал",
"name": "Конфиденциальность",
"success-update": "Настройки приватности сохранены"
},
"security": {
"change-password": {
"button": "Изменить пароль",
"label-new-password": "Новый пароль",
"label-new-password-confirm": "Подтверждение пароля",
"label-old-password": "Старый пароль",
"message-new-password-confirm-required": "Требуется подтверждение пароля",
"message-new-password-missmatch": "Пароли не совпадают",
"message-new-password-required": "Требуется новый пароль",
"message-old-password-required": "Требуется свой старый пароль",
"passwordSecurity": "Безопасность пароля",
"passwordStrength0": "Очень небезопасный",
"passwordStrength1": "Небезопасный",
"passwordStrength2": "Посредственный",
"passwordStrength3": "Надежный",
"passwordStrength4": "Очень надежный",
"success": "Пароль успешно изменен!"
},
"name": "Безопасность"
},
"social-media": {
"name": "Социальные Медиа",
"placeholder": "Ссылка на профиль социальной сети",
"requireUnique": "Ссылка уже существует",
"submit": "Добавить ссылку",
"successAdd": "Добавлены социальные меди. Профиль обновлен!",
"successDelete": "Социальные Меди удалены. Профиль обновлен!"
},
"validation": {
"slug": {
"alreadyTaken": "Это имя пользователя уже занято.",
"regex": "Допускаются только строчные буквы, цифры, подчеркивания или дефисы."
}
}
},
"shoutButton": {
"shouted": "выкрикнули"
},
"site": {
"back-to-login": "Вернуться на страницу входа",
"bank": "банковский счет",
"changelog": "Изменения",
"code-of-conduct": "Кодекс поведения",
"contact": "Контакт",
"data-privacy": "Конфиденциальность",
"director": "Управляющий директор",
"error-occurred": "Произошла ошибка.",
"faq": "ЧаВо (FAQ)",
"germany": "Германия",
"imprint": "Импрессум",
"made": "Сделано с &#10084;",
"register": "Регистрационный номер",
"responsible": "ответственный за содержание этой страницы (§ 55 Abs. 2 RStV)",
"taxident": "UST-ID. в соответствии с §27a Закона о налоге с продаж Германии:",
"termsAndConditions": "Условия и положения",
"thanks": "Спасибо!",
"tribunal": "Суд регистрации"
},
"store": {
"posts": {
"orderBy": {
"newest": {
"label": "Сначала новые"
},
"oldest": {
"label": "Сначала старые"
}
}
}
},
"termsAndConditions": {
"addition": {
"description": "<a href=\"https:\/\/human-connection.org\/events\/\" target=\"_blank\" > https:\/\/human-connection.org\/events\/ <\/a>",
"title": "Кроме того, мы регулярно проводим мероприятия, где вы также можете\\nподелиться своими впечатлениями и задать вопросы. Информацию о текущих событиях можно найти здесь:"
},
"agree": "Я согласен(на)!",
"code-of-conduct": {
"description": "Наш кодекс поведения служит руководством для личного поведения и взаимодействия друг с другом. Каждый пользователь социальной сети Human Connection, который пишет статьи, комментирует или вступает в контакт с другими пользователями, даже за пределами сети, признает эти правила поведения обязательными. <a href=\"https:\/\/alpha.human-connection.org\/code-of-conduct\" target=\"_blank\"> https:\/\/alpha.human-connection.org\/code-of-conduct<\/a>",
"title": "Кодекс поведения"
},
"errors-and-feedback": {
"description": "Мы прилагаем все усилия для обеспечения безопасности и доступности нашей сети и данных. Каждый новый выпуск программного обеспечения проходит как автоматическое, так и ручное тестирование. Однако могут возникнуть непредвиденные ошибки. Поэтому мы благодарны за любые обнаруженные ошибки. Вы можете сообщить о любых обнаруженных ошибках, отправив электронное письмо в службу поддержки по адресу support@human-connection.org",
"title": "Ошибки и обратная связь"
},
"help-and-questions": {
"description": "Для справки и вопросов мы собрали для вас исчерпывающую подборку часто задаваемых вопросов и ответов (FAQ). Вы можете найти их здесь: <a href=\"https:\/\/support.human-connection.org\/kb\/\" target=\"_blank\" > https:\/\/support.human-connection.org\/kb\/ <\/a>",
"title": "Помощь и вопросы"
},
"moderation": {
"description": "Пока наши финансовые возможности не позволяют нам реализовать полноценную систему модерации, поэтому мы осуществляем упрощенную модерацию собственными силами и с помощью волонтёров. Мы специально обучаем этих модераторов, поэтому только они принимают соответствующие решения. Модераторы действуют анонимно. Вы можете сообщать нам о постах, комментариях и пользователях (например, если они предоставляют информацию в своем профиле или имеют изображения, которые нарушают настоящие Условия использования). При обращении вы можете указать причину и дать краткое пояснение. Мы рассмотрим обращение и применим санкции в случае необходимости, например, путем блокировки постов, комментариев или пользователей. К сожалению, в настоящее время ни вы ни пострадавший пользователь не получите от нас обратной связи, но мы планируем ряд улучшений в этом направлении. Несмотря на это, мы оставляем за собой право на применение санкций по причинам, которые не могут быть или ещё не указаны в нашем Кодексе поведения или настоящих Условиях использования.",
"title": "Модерация"
},
"newTermsAndConditions": "Новые условия и положения",
"no-commercial-use": {
"description": "Использование Human Connection сети не допускается в коммерческих целях. Это включает, но не ограничивается рекламой продуктов с коммерческими целями, размещением партнерских ссылок, прямым привлечением пожертвований или предоставлением финансовой поддержки для целей, которые не признаются благотворительными для целей налогообложения.",
"title": "Нет коммерческого использования"
},
"privacy-statement": {
"description": "Наша сеть — это социальная сеть знаний и действий. Поэтому для нас особенно важно, чтобы как можно больше контента было общедоступным. В процессе развития нашей сети будет добавлено больше возможностей для управления видимостью личных данных. Об этих новых функциях мы сообщим дополнительно. В противном случае вы должны думать о том, какие личные данные вы раскрываете о себе (или других). Это особенно актуально для содержания постов и комментариев, поскольку они имеют в основном общедоступный характер. Позже появятся возможности ограничения видимости вашего профиля. Часть условий использования — это наша политика конфиденциальности, которая информирует вас об обработке персональных данных в нашей сети: <a href=\"https:\/\/human-connection.org\/datenschutz\/#netzwerk\" target=\"_blank\">https:\/\/human-connection.org\/datenschutz\/#netzwerk<\/a> или <a href=\"https:\/\/human-connection.org\/datenschutz\/\" target=\"_blank\">https:\/\/human-connection.org\/datenschutz<\/a>. Наше заявление о конфиденциальности корректируется в соответствии с законодательством и характеристиками нашей сети и является действительной в настоящей версии.",
"title": "Заявление о конфиденциальности"
},
"terms-of-service": {
"description": "Следующие условия использования являются основой для использования нашей сети. При регистрации вы должны принять их, а мы при необходимости сообщим вам об изменениях. Сеть Human Connection работает в Германии и поэтому регулируется немецким законодательством. Место юрисдикции - Kirchheim \/ Teck. Подробности в выходных данных: <a href=\"https:\/\/human-connection.org\/en\/imprint\" target=\"_blank\" >https:\/\/human-connection.org\/en\/imprint<\/a>.",
"title": "Условия обслуживания"
},
"termsAndConditionsConfirmed": "Я прочитал(а) и подтверждаю <a href=\"\/terms-and-conditions\" target=\"_blank\">Условия и положения<\/a>.",
"termsAndConditionsNewConfirm": "Я прочитал(а) и согласен(на) с новыми условиями.",
"termsAndConditionsNewConfirmText": "Пожалуйста, ознакомьтесь с новыми условиями использования!",
"use-and-license": {
"description": "Если размещаемый в сети контент защищен правами на интеллектуальную собственность, вы предоставляете нам неисключительную, передаваемую, сублицензируемую и всемирную лицензию на использование этого контента для публикации в нашей сети. Эта лицензия заканчивается, как только вы удаляете свой контент или учетную запись. Помните, что другие пользователи могут продолжать делиться вашим контентом, и мы не можем его удалить.",
"title": "Использование и лицензия"
}
},
"user": {
"avatar": {
"submitted": "Успешная загрузка!"
}
}
}

View File

@ -1,6 +1,6 @@
{ {
"name": "human-connection", "name": "human-connection",
"version": "0.1.11", "version": "0.1.13",
"description": "Fullstack and API tests with cypress and cucumber for Human Connection", "description": "Fullstack and API tests with cypress and cucumber for Human Connection",
"author": "Human Connection gGmbh", "author": "Human Connection gGmbh",
"license": "MIT", "license": "MIT",
@ -21,17 +21,17 @@
"version": "auto-changelog -p" "version": "auto-changelog -p"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.7.2", "@babel/core": "^7.7.5",
"@babel/preset-env": "^7.7.4", "@babel/preset-env": "^7.7.6",
"@babel/register": "^7.7.4", "@babel/register": "^7.7.4",
"auto-changelog": "^1.16.2", "auto-changelog": "^1.16.2",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"codecov": "^3.6.1", "codecov": "^3.6.1",
"cross-env": "^6.0.3", "cross-env": "^6.0.3",
"cucumber": "^6.0.5", "cucumber": "^6.0.5",
"cypress": "^3.7.0", "cypress": "^3.8.0",
"cypress-cucumber-preprocessor": "^1.18.0", "cypress-cucumber-preprocessor": "^1.18.0",
"cypress-file-upload": "^3.5.0", "cypress-file-upload": "^3.5.1",
"cypress-plugin-retries": "^1.5.0", "cypress-plugin-retries": "^1.5.0",
"date-fns": "^2.8.1", "date-fns": "^2.8.1",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",

View File

@ -1,11 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
ROOT_DIR=$(dirname "$0")/.. ROOT_DIR=$(dirname "$0")/..
DOCKER_CLI_EXPERIMENTAL=enabled
# BUILD_COMMIT=${TRAVIS_COMMIT:-$(git rev-parse HEAD)} # BUILD_COMMIT=${TRAVIS_COMMIT:-$(git rev-parse HEAD)}
IFS='.' read -r major minor patch < $ROOT_DIR/VERSION IFS='.' read -r major minor patch < $ROOT_DIR/VERSION
apps=(nitro-web nitro-backend neo4j maintenance) apps=(nitro-web nitro-backend neo4j maintenance)
tags=(latest $major $major.$minor $major.$minor.$patch) tags=($major $major.$minor $major.$minor.$patch)
# These three docker images have already been built by now: # These three docker images have already been built by now:
# docker build --build-arg BUILD_COMMIT=$BUILD_COMMIT --target production -t humanconnection/nitro-backend:latest $ROOT_DIR/backend # docker build --build-arg BUILD_COMMIT=$BUILD_COMMIT --target production -t humanconnection/nitro-backend:latest $ROOT_DIR/backend
@ -17,13 +16,17 @@ echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
for app in "${apps[@]}" for app in "${apps[@]}"
do do
SOURCE="humanconnection/${app}:latest"
echo "docker push $SOURCE"
docker push $SOURCE
for tag in "${tags[@]}" for tag in "${tags[@]}"
do do
SOURCE="humanconnection/${app}:latest"
TARGET="humanconnection/${app}:${tag}" TARGET="humanconnection/${app}:${tag}"
if docker manifest inspect $TARGET &> /dev/null; then if DOCKER_CLI_EXPERIMENTAL=enabled docker manifest inspect $TARGET >/dev/null; then
echo "Docker image ${TARGET} already present, skipping ..." echo "docker image ${TARGET} already present, skipping ..."
else else
echo -e "docker tag $SOURCE $TARGET\ndocker push $TARGET"
docker tag $SOURCE $TARGET docker tag $SOURCE $TARGET
docker push $TARGET docker push $TARGET
fi fi

View File

@ -1,4 +1,4 @@
FROM node:13.1.0-alpine as base FROM node:13.3.0-alpine as base
LABEL Description="Web Frontend of the Social Network Human-Connection.org" Vendor="Human-Connection gGmbH" Version="0.0.1" Maintainer="Human-Connection gGmbH (developer@human-connection.org)" LABEL Description="Web Frontend of the Social Network Human-Connection.org" Vendor="Human-Connection gGmbH" Version="0.0.1" Maintainer="Human-Connection gGmbH (developer@human-connection.org)"
EXPOSE 3000 EXPOSE 3000

View File

@ -1,4 +1,4 @@
FROM node:13.1.0-alpine as build FROM node:13.3.0-alpine as build
LABEL Description="Maintenance page of the Social Network Human-Connection.org" Vendor="Human-Connection gGmbH" Version="0.0.1" Maintainer="Human-Connection gGmbH (developer@human-connection.org)" LABEL Description="Maintenance page of the Social Network Human-Connection.org" Vendor="Human-Connection gGmbH" Version="0.0.1" Maintainer="Human-Connection gGmbH (developer@human-connection.org)"
EXPOSE 3000 EXPOSE 3000

View File

@ -6,14 +6,15 @@
```bash ```bash
# install all dependencies # install all dependencies
$ cd webapp/
$ yarn install $ yarn install
``` ```
Copy: Copy:
```text ```text
# in webapp/
cp .env.template .env cp .env.template .env
cp cypress.env.template.json cypress.env.json
``` ```
Configure the files according to your needs and your local setup. Configure the files according to your needs and your local setup.

View File

@ -22,10 +22,6 @@ describe('ContentMenu.vue', () => {
locale: () => 'en', locale: () => 'en',
}, },
$router: { $router: {
resolve: jest.fn(obj => {
obj.href = '/post/edit/d23a4265-f5f7-4e17-9f86-85f714b4b9f8'
return obj
}),
push: jest.fn(), push: jest.fn(),
}, },
} }
@ -76,7 +72,7 @@ describe('ContentMenu.vue', () => {
.at(0) .at(0)
.find('span.ds-menu-item-link') .find('span.ds-menu-item-link')
.attributes('to'), .attributes('to'),
).toBe('/post/edit/d23a4265-f5f7-4e17-9f86-85f714b4b9f8') ).toBe('/post-edit-id')
}) })
it('can delete the contribution', () => { it('can delete the contribution', () => {

View File

@ -17,7 +17,7 @@
@click.stop.prevent="openItem(item.route, toggleMenu)" @click.stop.prevent="openItem(item.route, toggleMenu)"
> >
<base-icon :name="item.route.icon" /> <base-icon :name="item.route.icon" />
{{ item.route.name }} {{ item.route.label }}
</ds-menu-item> </ds-menu-item>
</ds-menu> </ds-menu>
</div> </div>
@ -58,17 +58,15 @@ export default {
if (this.resourceType === 'contribution') { if (this.resourceType === 'contribution') {
if (this.isOwner) { if (this.isOwner) {
routes.push({ routes.push({
name: this.$t(`post.menu.edit`), label: this.$t(`post.menu.edit`),
path: this.$router.resolve({
name: 'post-edit-id', name: 'post-edit-id',
params: { params: {
id: this.resource.id, id: this.resource.id,
}, },
}).href,
icon: 'edit', icon: 'edit',
}) })
routes.push({ routes.push({
name: this.$t(`post.menu.delete`), label: this.$t(`post.menu.delete`),
callback: () => { callback: () => {
this.openModal('confirm', 'delete') this.openModal('confirm', 'delete')
}, },
@ -79,7 +77,7 @@ export default {
if (this.isAdmin) { if (this.isAdmin) {
if (!this.resource.pinnedBy) { if (!this.resource.pinnedBy) {
routes.push({ routes.push({
name: this.$t(`post.menu.pin`), label: this.$t(`post.menu.pin`),
callback: () => { callback: () => {
this.$emit('pinPost', this.resource) this.$emit('pinPost', this.resource)
}, },
@ -87,7 +85,7 @@ export default {
}) })
} else { } else {
routes.push({ routes.push({
name: this.$t(`post.menu.unpin`), label: this.$t(`post.menu.unpin`),
callback: () => { callback: () => {
this.$emit('unpinPost', this.resource) this.$emit('unpinPost', this.resource)
}, },
@ -99,14 +97,14 @@ export default {
if (this.isOwner && this.resourceType === 'comment') { if (this.isOwner && this.resourceType === 'comment') {
routes.push({ routes.push({
name: this.$t(`comment.menu.edit`), label: this.$t(`comment.menu.edit`),
callback: () => { callback: () => {
this.$emit('showEditCommentMenu', true) this.$emit('showEditCommentMenu', true)
}, },
icon: 'edit', icon: 'edit',
}) })
routes.push({ routes.push({
name: this.$t(`comment.menu.delete`), label: this.$t(`comment.menu.delete`),
callback: () => { callback: () => {
this.openModal('confirm', 'delete') this.openModal('confirm', 'delete')
}, },
@ -116,7 +114,7 @@ export default {
if (!this.isOwner) { if (!this.isOwner) {
routes.push({ routes.push({
name: this.$t(`report.${this.resourceType}.title`), label: this.$t(`report.${this.resourceType}.title`),
callback: () => { callback: () => {
this.openModal('report') this.openModal('report')
}, },
@ -127,7 +125,7 @@ export default {
if (!this.isOwner && this.isModerator) { if (!this.isOwner && this.isModerator) {
if (!this.resource.disabled) { if (!this.resource.disabled) {
routes.push({ routes.push({
name: this.$t(`disable.${this.resourceType}.title`), label: this.$t(`disable.${this.resourceType}.title`),
callback: () => { callback: () => {
this.openModal('disable') this.openModal('disable')
}, },
@ -135,7 +133,7 @@ export default {
}) })
} else { } else {
routes.push({ routes.push({
name: this.$t(`release.${this.resourceType}.title`), label: this.$t(`release.${this.resourceType}.title`),
callback: () => { callback: () => {
this.openModal('release') this.openModal('release')
}, },
@ -147,14 +145,14 @@ export default {
if (this.resourceType === 'user') { if (this.resourceType === 'user') {
if (this.isOwner) { if (this.isOwner) {
routes.push({ routes.push({
name: this.$t(`settings.name`), label: this.$t(`settings.name`),
path: '/settings', path: '/settings',
icon: 'edit', icon: 'edit',
}) })
} else { } else {
if (this.resource.isBlocked) { if (this.resource.isBlocked) {
routes.push({ routes.push({
name: this.$t(`settings.blocked-users.unblock`), label: this.$t(`settings.blocked-users.unblock`),
callback: () => { callback: () => {
this.$emit('unblock', this.resource) this.$emit('unblock', this.resource)
}, },
@ -162,7 +160,7 @@ export default {
}) })
} else { } else {
routes.push({ routes.push({
name: this.$t(`settings.blocked-users.block`), label: this.$t(`settings.blocked-users.block`),
callback: () => { callback: () => {
this.$emit('block', this.resource) this.$emit('block', this.resource)
}, },
@ -186,7 +184,7 @@ export default {
if (route.callback) { if (route.callback) {
route.callback() route.callback()
} else { } else {
this.$router.push(route.path) this.$router.push(route)
} }
toggleMenu() toggleMenu()
}, },

View File

@ -38,7 +38,6 @@
</ds-chip> </ds-chip>
<ds-chip v-else size="base">{{ form.title.length }}/{{ formSchema.title.max }}</ds-chip> <ds-chip v-else size="base">{{ form.title.length }}/{{ formSchema.title.max }}</ds-chip>
</ds-text> </ds-text>
<client-only>
<hc-editor <hc-editor
:users="users" :users="users"
:value="form.content" :value="form.content"
@ -54,7 +53,6 @@
{{ contentLength }} {{ contentLength }}
</ds-chip> </ds-chip>
</ds-text> </ds-text>
</client-only>
<ds-space margin-bottom="small" /> <ds-space margin-bottom="small" />
<hc-categories-select model="categoryIds" :existingCategoryIds="form.categoryIds" /> <hc-categories-select model="categoryIds" :existingCategoryIds="form.categoryIds" />
<ds-text align="right"> <ds-text align="right">

View File

@ -24,7 +24,6 @@
import { Editor, EditorContent } from 'tiptap' import { Editor, EditorContent } from 'tiptap'
import { History } from 'tiptap-extensions' import { History } from 'tiptap-extensions'
import linkify from 'linkify-it' import linkify from 'linkify-it'
import stringHash from 'string-hash'
import { replace, build } from 'xregexp/xregexp-all.js' import { replace, build } from 'xregexp/xregexp-all.js'
import * as key from '../../constants/keycodes' import * as key from '../../constants/keycodes'
@ -108,17 +107,6 @@ export default {
}, },
}, },
watch: { watch: {
value: {
immediate: true,
handler: function(content, old) {
const contentHash = stringHash(content)
if (!content || contentHash === this.lastValueHash) {
return
}
this.lastValueHash = contentHash
this.$nextTick(() => this.editor.setContent(content))
},
},
placeholder: { placeholder: {
immediate: true, immediate: true,
handler: function(val) { handler: function(val) {
@ -129,7 +117,7 @@ export default {
}, },
}, },
}, },
created() { mounted() {
this.editor = new Editor({ this.editor = new Editor({
content: this.value || '', content: this.value || '',
doc: this.doc, doc: this.doc,
@ -247,11 +235,7 @@ export default {
}, },
onUpdate(e) { onUpdate(e) {
const content = e.getHTML() const content = e.getHTML()
const contentHash = stringHash(content)
if (contentHash !== this.lastValueHash) {
this.lastValueHash = contentHash
this.$emit('input', content) this.$emit('input', content)
}
}, },
toggleLinkInput(attrs, element) { toggleLinkInput(attrs, element) {
if (!this.isLinkInputActive && attrs && element) { if (!this.isLinkInputActive && attrs && element) {

View File

@ -46,7 +46,7 @@
<script> <script>
import { mapGetters, mapMutations } from 'vuex' import { mapGetters, mapMutations } from 'vuex'
import { allowEmbedIframesMutation } from '~/graphql/User.js' import { updateUserMutation } from '~/graphql/User.js'
export default { export default {
name: 'embed-component', name: 'embed-component',
@ -129,7 +129,7 @@ export default {
async updateEmbedSettings(allowEmbedIframes) { async updateEmbedSettings(allowEmbedIframes) {
try { try {
await this.$apollo.mutate({ await this.$apollo.mutate({
mutation: allowEmbedIframesMutation(), mutation: updateUserMutation(),
variables: { variables: {
id: this.currentUser.id, id: this.currentUser.id,
allowEmbedIframes, allowEmbedIframes,

View File

@ -1,4 +1,4 @@
import { config, mount } from '@vue/test-utils' import { config, shallowMount } from '@vue/test-utils'
import MasonryGridItem from './MasonryGridItem' import MasonryGridItem from './MasonryGridItem'
const localVue = global.localVue const localVue = global.localVue
@ -8,41 +8,24 @@ config.stubs['ds-grid-item'] = '<span><slot /></span>'
describe('MasonryGridItem', () => { describe('MasonryGridItem', () => {
let wrapper let wrapper
describe('given an imageAspectRatio', () => { beforeEach(() => {
it('sets the initial rowSpan to 13 when the ratio is higher than 1.3', () => { wrapper = shallowMount(MasonryGridItem, { localVue })
const propsData = { imageAspectRatio: 2 } wrapper.vm.$parent.$emit = jest.fn()
wrapper = mount(MasonryGridItem, { localVue, propsData })
expect(wrapper.vm.rowSpan).toBe(13)
}) })
it('sets the initial rowSpan to 15 when the ratio is between 1.3 and 1', () => { it('emits "calculating-item-height" when starting calculation', async () => {
const propsData = { imageAspectRatio: 1.1 } wrapper.vm.calculateItemHeight()
wrapper = mount(MasonryGridItem, { localVue, propsData }) await wrapper.vm.$nextTick()
expect(wrapper.vm.rowSpan).toBe(15) const firstCallArgument = wrapper.vm.$parent.$emit.mock.calls[0][0]
expect(firstCallArgument).toBe('calculating-item-height')
}) })
it('sets the initial rowSpan to 18 when the ratio is between 1 and 0.7', () => { it('emits "finished-calculating-item-height" after the calculation', async () => {
const propsData = { imageAspectRatio: 0.7 } wrapper.vm.calculateItemHeight()
wrapper = mount(MasonryGridItem, { localVue, propsData }) await wrapper.vm.$nextTick()
expect(wrapper.vm.rowSpan).toBe(18) const secondCallArgument = wrapper.vm.$parent.$emit.mock.calls[1][0]
}) expect(secondCallArgument).toBe('finished-calculating-item-height')
it('sets the initial rowSpan to 25 when the ratio is lower than 0.7', () => {
const propsData = { imageAspectRatio: 0.3 }
wrapper = mount(MasonryGridItem, { localVue, propsData })
expect(wrapper.vm.rowSpan).toBe(25)
})
})
describe('given no aspect ratio', () => {
it('sets the initial rowSpan to 8 when not given an imageAspectRatio', () => {
wrapper = mount(MasonryGridItem, { localVue })
expect(wrapper.vm.rowSpan).toBe(8)
})
}) })
}) })

View File

@ -5,17 +5,6 @@
</template> </template>
<script> <script>
const landscapeRatio = 1.3
const squareRatio = 1
const portraitRatio = 0.7
const getRowSpan = aspectRatio => {
if (aspectRatio >= landscapeRatio) return 13
else if (aspectRatio >= squareRatio) return 15
else if (aspectRatio >= portraitRatio) return 18
else return 25
}
export default { export default {
props: { props: {
imageAspectRatio: { imageAspectRatio: {
@ -25,7 +14,7 @@ export default {
}, },
data() { data() {
return { return {
rowSpan: this.imageAspectRatio ? getRowSpan(this.imageAspectRatio) : 8, rowSpan: 10,
} }
}, },
methods: { methods: {
@ -45,7 +34,13 @@ export default {
}, },
}, },
mounted() { mounted() {
this.calculateItemHeight() const image = this.$el.querySelector('img')
if (image) {
image.onload = () => this.calculateItemHeight()
} else {
// use timeout to make sure layout is set up before calculation
setTimeout(() => this.calculateItemHeight(), 0)
}
}, },
} }
</script> </script>

View File

@ -141,19 +141,10 @@ export default {
this.$emit('unpinPost', post) this.$emit('unpinPost', post)
}, },
}, },
mounted() {
const width = this.$el.offsetWidth
const height = Math.min(width / this.post.imageAspectRatio, 2000)
const imageElement = this.$el.querySelector('.ds-card-image')
if (imageElement) {
imageElement.style.height = `${height}px`
}
},
} }
</script> </script>
<style lang="scss"> <style scoped lang="scss">
.ds-card-image img { .ds-card-image img {
width: 100%; width: 100%;
max-height: 2000px; max-height: 2000px;

View File

@ -66,6 +66,8 @@ describe('CreateUserAccount', () => {
wrapper.find('input#checkbox0').setChecked() wrapper.find('input#checkbox0').setChecked()
wrapper.find('input#checkbox1').setChecked() wrapper.find('input#checkbox1').setChecked()
wrapper.find('input#checkbox2').setChecked() wrapper.find('input#checkbox2').setChecked()
wrapper.find('input#checkbox3').setChecked()
wrapper.find('input#checkbox4').setChecked()
await wrapper.find('form').trigger('submit') await wrapper.find('form').trigger('submit')
await wrapper.html() await wrapper.html()
} }

View File

@ -88,12 +88,33 @@
v-html="$t('components.registration.signup.form.minimum-age')" v-html="$t('components.registration.signup.form.minimum-age')"
></label> ></label>
</ds-text> </ds-text>
<ds-text>
<input id="checkbox3" type="checkbox" v-model="noCommercial" :checked="noCommercial" />
<label
for="checkbox3"
v-html="$t('components.registration.signup.form.no-commercial')"
></label>
</ds-text>
<ds-text>
<input id="checkbox4" type="checkbox" v-model="noPolitical" :checked="noPolitical" />
<label
for="checkbox4"
v-html="$t('components.registration.signup.form.no-political')"
></label>
</ds-text>
<ds-button <ds-button
style="float: right;" style="float: right;"
icon="check" icon="check"
type="submit" type="submit"
:loading="$apollo.loading" :loading="$apollo.loading"
:disabled="errors || !termsAndConditionsConfirmed || !dataPrivacy || !minimumAge" :disabled="
errors ||
!termsAndConditionsConfirmed ||
!dataPrivacy ||
!minimumAge ||
!noCommercial ||
!noPolitical
"
primary primary
> >
{{ $t('actions.save') }} {{ $t('actions.save') }}
@ -145,6 +166,8 @@ export default {
termsAndConditionsConfirmed: false, termsAndConditionsConfirmed: false,
dataPrivacy: false, dataPrivacy: false,
minimumAge: false, minimumAge: false,
noCommercial: false,
noPolitical: false,
} }
}, },
props: { props: {

View File

@ -21,7 +21,7 @@
</template> </template>
<script> <script>
import vueDropzone from 'nuxt-dropzone' import vueDropzone from 'nuxt-dropzone'
import gql from 'graphql-tag' import { updateUserMutation } from '~/graphql/User.js'
export default { export default {
components: { components: {
@ -62,14 +62,7 @@ export default {
const avatarUpload = file[0] const avatarUpload = file[0]
this.$apollo this.$apollo
.mutate({ .mutate({
mutation: gql` mutation: updateUserMutation(),
mutation($id: ID!, $avatarUpload: Upload) {
UpdateUser(id: $id, avatarUpload: $avatarUpload) {
id
avatar
}
}
`,
variables: { variables: {
avatarUpload, avatarUpload,
id: this.user.id, id: this.user.id,

View File

@ -8,7 +8,7 @@
<dropdown v-else :class="{ 'disabled-content': user.disabled }" placement="top-start" offset="0"> <dropdown v-else :class="{ 'disabled-content': user.disabled }" placement="top-start" offset="0">
<template slot="default" slot-scope="{ openMenu, closeMenu, isOpen }"> <template slot="default" slot-scope="{ openMenu, closeMenu, isOpen }">
<nuxt-link :to="userLink" :class="['user', isOpen && 'active']"> <nuxt-link :to="userLink" :class="['user', isOpen && 'active']">
<div @mouseover="openInfoMenu" @mouseleave="closeMenu(true)"> <div @mouseover="showPopover ? openMenu(true) : () => {}" @mouseleave="closeMenu(true)">
<hc-avatar v-if="showAvatar" class="avatar" :user="user" /> <hc-avatar v-if="showAvatar" class="avatar" :user="user" />
<div> <div>
<ds-text class="userinfo"> <ds-text class="userinfo">
@ -26,7 +26,7 @@
</div> </div>
</nuxt-link> </nuxt-link>
</template> </template>
<template slot="popover" v-if="showCounts"> <template slot="popover" v-if="showPopover">
<div style="min-width: 250px"> <div style="min-width: 250px">
<hc-badges v-if="user.badges && user.badges.length" :badges="user.badges" /> <hc-badges v-if="user.badges && user.badges.length" :badges="user.badges" />
<ds-text <ds-text
@ -106,7 +106,7 @@ export default {
showAvatar: { type: Boolean, default: true }, showAvatar: { type: Boolean, default: true },
trunc: { type: Number, default: 18 }, // "-1" is no trunc trunc: { type: Number, default: 18 }, // "-1" is no trunc
dateTime: { type: [Date, String], default: null }, dateTime: { type: [Date, String], default: null },
showCounts: { type: Boolean, default: true }, showPopover: { type: Boolean, default: true },
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
@ -143,9 +143,6 @@ export default {
this.user.followedByCount = followedByCount this.user.followedByCount = followedByCount
this.user.followedByCurrentUser = followedByCurrentUser this.user.followedByCurrentUser = followedByCurrentUser
}, },
openInfoMenu() {
if (this.showCounts) this.openMenu(true)
},
}, },
} }
</script> </script>

View File

@ -10,6 +10,7 @@
<hc-user <hc-user
:user="scope.row.submitter" :user="scope.row.submitter"
:showAvatar="false" :showAvatar="false"
:showPopover="false"
:trunc="30" :trunc="30"
data-test="filing-user" data-test="filing-user"
/> />

View File

@ -19,7 +19,7 @@
<!-- Content Column --> <!-- Content Column -->
<td class="ds-table-col" data-test="report-content"> <td class="ds-table-col" data-test="report-content">
<client-only v-if="isUser"> <client-only v-if="isUser">
<hc-user :user="report.resource" :showAvatar="false" :trunc="30" :showCounts="false" /> <hc-user :user="report.resource" :showAvatar="false" :trunc="30" :showPopover="false" />
</client-only> </client-only>
<nuxt-link v-else class="title" :to="linkTarget"> <nuxt-link v-else class="title" :to="linkTarget">
{{ linkText | truncate(50) }} {{ linkText | truncate(50) }}
@ -33,7 +33,7 @@
:user="report.resource.author" :user="report.resource.author"
:showAvatar="false" :showAvatar="false"
:trunc="30" :trunc="30"
:showCounts="false" :showPopover="false"
/> />
</client-only> </client-only>
<span v-else></span> <span v-else></span>
@ -51,7 +51,7 @@
:showAvatar="false" :showAvatar="false"
:trunc="30" :trunc="30"
:date-time="report.updatedAt" :date-time="report.updatedAt"
:showCounts="false" :showPopover="false"
/> />
</client-only> </client-only>
</td> </td>

View File

@ -0,0 +1,17 @@
import unionBy from 'lodash/unionBy'
export default function UpdateQuery(component, { $state, pageKey }) {
if (!pageKey) throw new Error('No key given for the graphql query { data } object')
return (previousResult, { fetchMoreResult }) => {
const oldData = (previousResult && previousResult[pageKey]) || []
const newData = (fetchMoreResult && fetchMoreResult[pageKey]) || []
if (newData.length < component.pageSize) {
component.hasMore = false
$state.complete()
}
const result = {}
result[pageKey] = unionBy(oldData, newData, item => item.id)
$state.loaded()
return result
}
}

View File

@ -0,0 +1,86 @@
import UpdateQuery from './UpdateQuery'
let $state
let component
let pageKey
let updateQuery
let previousResult
let fetchMoreResult
beforeEach(() => {
component = {
hasMore: true,
pageSize: 1,
}
$state = {
complete: jest.fn(),
loaded: jest.fn(),
}
previousResult = { Post: [{ id: 1, foo: 'bar' }] }
fetchMoreResult = { Post: [{ id: 2, foo: 'baz' }] }
updateQuery = () => UpdateQuery(component, { $state, pageKey })
})
describe('UpdateQuery', () => {
it('throws error because no key is given', () => {
expect(() => {
updateQuery()({ Post: [] }, { fetchMoreResult: { Post: [] } })
}).toThrow(/No key given/)
})
describe('with a page key', () => {
beforeEach(() => (pageKey = 'Post'))
describe('given two arrays of things', () => {
it('merges the arrays', () => {
expect(updateQuery()(previousResult, { fetchMoreResult })).toEqual({
Post: [
{ id: 1, foo: 'bar' },
{ id: 2, foo: 'baz' },
],
})
})
it('does not create duplicates', () => {
fetchMoreResult = { Post: [{ id: 1, foo: 'baz' }] }
expect(updateQuery()(previousResult, { fetchMoreResult })).toEqual({
Post: [{ id: 1, foo: 'bar' }],
})
})
it('does not call $state.complete()', () => {
expect(updateQuery()(previousResult, { fetchMoreResult }))
expect($state.complete).not.toHaveBeenCalled()
})
describe('in case of fewer records than pageSize', () => {
beforeEach(() => (component.pageSize = 10))
it('calls $state.complete()', () => {
expect(updateQuery()(previousResult, { fetchMoreResult }))
expect($state.complete).toHaveBeenCalled()
})
it('changes component.hasMore to `false`', () => {
expect(component.hasMore).toBe(true)
expect(updateQuery()(previousResult, { fetchMoreResult }))
expect(component.hasMore).toBe(false)
})
})
})
describe('given one array is undefined', () => {
describe('does not crash', () => {
it('neither if the previous data was undefined', () => {
expect(updateQuery()(undefined, { fetchMoreResult })).toEqual({
Post: [{ id: 2, foo: 'baz' }],
})
})
it('not if the new data is undefined', () => {
expect(updateQuery()(previousResult, {})).toEqual({ Post: [{ id: 1, foo: 'bar' }] })
})
})
})
})
})

View File

@ -1,3 +1,3 @@
export const COMMENT_MIN_LENGTH = 1 export const COMMENT_MIN_LENGTH = 1
export const COMMENT_MAX_UNTRUNCATED_LENGTH = 300 export const COMMENT_MAX_UNTRUNCATED_LENGTH = 1200
export const COMMENT_TRUNCATE_TO_LENGTH = 180 export const COMMENT_TRUNCATE_TO_LENGTH = 180

View File

@ -1,6 +1,6 @@
import gql from 'graphql-tag' import gql from 'graphql-tag'
export const userFragment = lang => gql` export const userFragment = gql`
fragment user on User { fragment user on User {
id id
slug slug
@ -8,11 +8,10 @@ export const userFragment = lang => gql`
avatar avatar
disabled disabled
deleted deleted
shoutedCount }
contributionsCount `
commentedCount export const locationAndBadgesFragment = lang => gql`
followedByCount fragment locationAndBadges on User {
followedByCurrentUser
location { location {
name: name${lang} name: name${lang}
} }
@ -23,17 +22,17 @@ export const userFragment = lang => gql`
} }
` `
export const postCountsFragment = gql` export const userCountsFragment = gql`
fragment postCounts on Post { fragment userCounts on User {
commentsCount
shoutedCount shoutedCount
shoutedByCurrentUser contributionsCount
emotionsCount commentedCount
followedByCount
followedByCurrentUser
} }
` `
export const postFragment = lang => gql`
${userFragment(lang)}
export const postFragment = gql`
fragment post on Post { fragment post on Post {
id id
title title
@ -46,9 +45,22 @@ export const postFragment = lang => gql`
slug slug
image image
language language
author { pinnedAt
...user imageAspectRatio
} }
`
export const postCountsFragment = gql`
fragment postCounts on Post {
commentsCount
shoutedCount
shoutedByCurrentUser
emotionsCount
}
`
export const tagsCategoriesAndPinnedFragment = gql`
fragment tagsCategoriesAndPinned on Post {
tags { tags {
id id
} }
@ -63,13 +75,10 @@ export const postFragment = lang => gql`
name name
role role
} }
pinnedAt
imageAspectRatio
} }
` `
export const commentFragment = lang => gql`
${userFragment(lang)}
export const commentFragment = gql`
fragment comment on Comment { fragment comment on Comment {
id id
createdAt createdAt
@ -78,8 +87,5 @@ export const commentFragment = lang => gql`
deleted deleted
content content
contentExcerpt contentExcerpt
author {
...user
}
} }
` `

View File

@ -1,19 +1,42 @@
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { postFragment, commentFragment, postCountsFragment } from './Fragments' import {
userFragment,
postFragment,
commentFragment,
postCountsFragment,
userCountsFragment,
locationAndBadgesFragment,
tagsCategoriesAndPinnedFragment,
} from './Fragments'
export default i18n => { export default i18n => {
const lang = i18n.locale().toUpperCase() const lang = i18n.locale().toUpperCase()
return gql` return gql`
${postFragment(lang)} ${userFragment}
${userCountsFragment}
${locationAndBadgesFragment(lang)}
${postFragment}
${postCountsFragment} ${postCountsFragment}
${commentFragment(lang)} ${tagsCategoriesAndPinnedFragment}
${commentFragment}
query Post($id: ID!) { query Post($id: ID!) {
Post(id: $id) { Post(id: $id) {
...post ...post
...postCounts ...postCounts
...tagsCategoriesAndPinned
author {
...user
...userCounts
...locationAndBadges
}
comments(orderBy: createdAt_asc) { comments(orderBy: createdAt_asc) {
...comment ...comment
author {
...user
...userCounts
...locationAndBadges
}
} }
} }
} }
@ -23,13 +46,23 @@ export default i18n => {
export const filterPosts = i18n => { export const filterPosts = i18n => {
const lang = i18n.locale().toUpperCase() const lang = i18n.locale().toUpperCase()
return gql` return gql`
${postFragment(lang)} ${userFragment}
${userCountsFragment}
${locationAndBadgesFragment(lang)}
${postFragment}
${postCountsFragment} ${postCountsFragment}
${tagsCategoriesAndPinnedFragment}
query Post($filter: _PostFilter, $first: Int, $offset: Int, $orderBy: [_PostOrdering]) { query Post($filter: _PostFilter, $first: Int, $offset: Int, $orderBy: [_PostOrdering]) {
Post(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) { Post(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) {
...post ...post
...postCounts ...postCounts
...tagsCategoriesAndPinned
author {
...user
...userCounts
...locationAndBadges
}
} }
} }
` `
@ -38,8 +71,12 @@ export const filterPosts = i18n => {
export const profilePagePosts = i18n => { export const profilePagePosts = i18n => {
const lang = i18n.locale().toUpperCase() const lang = i18n.locale().toUpperCase()
return gql` return gql`
${postFragment(lang)} ${userFragment}
${userCountsFragment}
${locationAndBadgesFragment(lang)}
${postFragment}
${postCountsFragment} ${postCountsFragment}
${tagsCategoriesAndPinnedFragment}
query profilePagePosts( query profilePagePosts(
$filter: _PostFilter $filter: _PostFilter
@ -50,6 +87,12 @@ export const profilePagePosts = i18n => {
profilePagePosts(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) { profilePagePosts(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) {
...post ...post
...postCounts ...postCounts
...tagsCategoriesAndPinned
author {
...user
...userCounts
...locationAndBadges
}
} }
} }
` `
@ -66,16 +109,32 @@ export const PostsEmotionsByCurrentUser = () => {
export const relatedContributions = i18n => { export const relatedContributions = i18n => {
const lang = i18n.locale().toUpperCase() const lang = i18n.locale().toUpperCase()
return gql` return gql`
${postFragment(lang)} ${userFragment}
${userCountsFragment}
${locationAndBadgesFragment(lang)}
${postFragment}
${postCountsFragment} ${postCountsFragment}
${tagsCategoriesAndPinnedFragment}
query Post($slug: String!) { query Post($slug: String!) {
Post(slug: $slug) { Post(slug: $slug) {
...post ...post
...postCounts ...postCounts
...tagsCategoriesAndPinned
author {
...user
...userCounts
...locationAndBadges
}
relatedContributions(first: 2) { relatedContributions(first: 2) {
...post ...post
...postCounts ...postCounts
...tagsCategoriesAndPinned
author {
...user
...userCounts
...locationAndBadges
}
} }
} }
} }

View File

@ -1,27 +1,38 @@
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { userFragment, postFragment, commentFragment } from './Fragments' import {
userCountsFragment,
locationAndBadgesFragment,
userFragment,
postFragment,
commentFragment,
} from './Fragments'
export default i18n => { export default i18n => {
const lang = i18n.locale().toUpperCase() const lang = i18n.locale().toUpperCase()
return gql` return gql`
${userFragment(lang)} ${userFragment}
${userCountsFragment}
${locationAndBadgesFragment(lang)}
query User($id: ID!) { query User($id: ID!) {
User(id: $id) { User(id: $id) {
...user ...user
...userCounts
...locationAndBadges
about about
locationName locationName
createdAt createdAt
badgesCount
followingCount
following(first: 7) {
...user
}
followedByCount
followedByCurrentUser followedByCurrentUser
isBlocked isBlocked
following(first: 7) {
...user
...userCounts
...locationAndBadges
}
followedBy(first: 7) { followedBy(first: 7) {
...user ...user
...userCounts
...locationAndBadges
} }
socialMedia { socialMedia {
id id
@ -47,10 +58,10 @@ export const minimisedUserQuery = () => {
} }
export const notificationQuery = i18n => { export const notificationQuery = i18n => {
const lang = i18n.locale().toUpperCase()
return gql` return gql`
${commentFragment(lang)} ${userFragment}
${postFragment(lang)} ${commentFragment}
${postFragment}
query($read: Boolean, $orderBy: NotificationOrdering, $first: Int, $offset: Int) { query($read: Boolean, $orderBy: NotificationOrdering, $first: Int, $offset: Int) {
notifications(read: $read, orderBy: $orderBy, first: $first, offset: $offset) { notifications(read: $read, orderBy: $orderBy, first: $first, offset: $offset) {
@ -62,11 +73,20 @@ export const notificationQuery = i18n => {
__typename __typename
... on Post { ... on Post {
...post ...post
author {
...user
}
} }
... on Comment { ... on Comment {
...comment ...comment
author {
...user
}
post { post {
...post ...post
author {
...user
}
} }
} }
} }
@ -76,10 +96,10 @@ export const notificationQuery = i18n => {
} }
export const markAsReadMutation = i18n => { export const markAsReadMutation = i18n => {
const lang = i18n.locale().toUpperCase()
return gql` return gql`
${commentFragment(lang)} ${userFragment}
${postFragment(lang)} ${commentFragment}
${postFragment}
mutation($id: ID!) { mutation($id: ID!) {
markAsRead(id: $id) { markAsRead(id: $id) {
@ -91,11 +111,17 @@ export const markAsReadMutation = i18n => {
__typename __typename
... on Post { ... on Post {
...post ...post
author {
...user
}
} }
... on Comment { ... on Comment {
...comment ...comment
post { post {
...post ...post
author {
...user
}
} }
} }
} }
@ -105,16 +131,19 @@ export const markAsReadMutation = i18n => {
} }
export const followUserMutation = i18n => { export const followUserMutation = i18n => {
const lang = i18n.locale().toUpperCase()
return gql` return gql`
${userFragment(lang)} ${userFragment}
${userCountsFragment}
mutation($id: ID!) { mutation($id: ID!) {
followUser(id: $id) { followUser(id: $id) {
name ...user
...userCounts
followedByCount followedByCount
followedByCurrentUser followedByCurrentUser
followedBy(first: 7) { followedBy(first: 7) {
...user ...user
...userCounts
} }
} }
} }
@ -122,39 +151,59 @@ export const followUserMutation = i18n => {
} }
export const unfollowUserMutation = i18n => { export const unfollowUserMutation = i18n => {
const lang = i18n.locale().toUpperCase()
return gql` return gql`
${userFragment(lang)} ${userFragment}
${userCountsFragment}
mutation($id: ID!) { mutation($id: ID!) {
unfollowUser(id: $id) { unfollowUser(id: $id) {
name ...user
...userCounts
followedByCount followedByCount
followedByCurrentUser followedByCurrentUser
followedBy(first: 7) { followedBy(first: 7) {
...user ...user
...userCounts
} }
} }
} }
` `
} }
export const allowEmbedIframesMutation = () => { export const updateUserMutation = () => {
return gql` return gql`
mutation($id: ID!, $allowEmbedIframes: Boolean) { mutation(
UpdateUser(id: $id, allowEmbedIframes: $allowEmbedIframes) { $id: ID!
$slug: String
$name: String
$locationName: String
$about: String
$allowEmbedIframes: Boolean
$showShoutsPublicly: Boolean
$termsAndConditionsAgreedVersion: String
$avatarUpload: Upload
) {
UpdateUser(
id: $id
slug: $slug
name: $name
locationName: $locationName
about: $about
allowEmbedIframes: $allowEmbedIframes
showShoutsPublicly: $showShoutsPublicly
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
avatarUpload: $avatarUpload
) {
id id
slug
name
locationName
about
allowEmbedIframes allowEmbedIframes
}
}
`
}
export const showShoutsPubliclyMutation = () => {
return gql`
mutation($id: ID!, $showShoutsPublicly: Boolean) {
UpdateUser(id: $id, showShoutsPublicly: $showShoutsPublicly) {
id
showShoutsPublicly showShoutsPublicly
locale
termsAndConditionsAgreedVersion
avatar
} }
} }
` `
@ -167,14 +216,3 @@ export const checkSlugAvailableQuery = gql`
} }
} }
` `
export const localeMutation = () => {
return gql`
mutation($id: ID!, $locale: String) {
UpdateUser(id: $id, locale: $locale) {
id
locale
}
}
`
}

View File

@ -113,7 +113,7 @@
} }
}, },
"deleteUserAccount": { "deleteUserAccount": {
"name": "Daten löschen", "name": "Benutzerkonto löschen",
"contributionsCount": "Meine {count} Beiträge löschen", "contributionsCount": "Meine {count} Beiträge löschen",
"commentedCount": "Meine {count} Kommentare löschen", "commentedCount": "Meine {count} Kommentare löschen",
"accountDescription": "Sei dir bewusst, dass deine Beiträge und Kommentare für unsere Community wichtig sind. Wenn du sie trotzdem löschen möchtest, musst du sie unten markieren.", "accountDescription": "Sei dir bewusst, dass deine Beiträge und Kommentare für unsere Community wichtig sind. Wenn du sie trotzdem löschen möchtest, musst du sie unten markieren.",
@ -550,6 +550,8 @@
"terms-and-condition": "Ich stimme den <a href=\"\/terms-and-conditions\"><ds-text bold color=\"primary\" > Nutzungsbedingungen<\/ds-text><\/a>zu.", "terms-and-condition": "Ich stimme den <a href=\"\/terms-and-conditions\"><ds-text bold color=\"primary\" > Nutzungsbedingungen<\/ds-text><\/a>zu.",
"data-privacy": "Ich habe die <a href=\"https:\/\/human-connection.org\/datenschutz\/\" target=\"_blank\"><ds-text bold color=\"primary\" >Datenschutzerklärung<\/ds-text><\/a> gelesen und verstanden", "data-privacy": "Ich habe die <a href=\"https:\/\/human-connection.org\/datenschutz\/\" target=\"_blank\"><ds-text bold color=\"primary\" >Datenschutzerklärung<\/ds-text><\/a> gelesen und verstanden",
"minimum-age": "Ich bin 18 Jahre oder älter.", "minimum-age": "Ich bin 18 Jahre oder älter.",
"no-commercial": "Ich habe keine kommerziellen Absichten und ich repräsentiere kein kommerzielles Unternehmen oder Organisation.",
"no-political": "Ich bin nicht im Auftrag einer Partei oder politischen Organisation im Netzwerk. ",
"invitation-code": "Dein Einladungscode lautet: <b>{code}<\/b>", "invitation-code": "Dein Einladungscode lautet: <b>{code}<\/b>",
"errors": { "errors": {
"email-exists": "Es gibt schon ein Benutzerkonto mit dieser E-Mail Adresse!", "email-exists": "Es gibt schon ein Benutzerkonto mit dieser E-Mail Adresse!",

View File

@ -34,6 +34,8 @@
"terms-and-condition": "I confirm to the <a href=\"/terms-and-conditions\"><ds-text bold color=\"primary\" > Terms and conditions</ds-text></a>.", "terms-and-condition": "I confirm to the <a href=\"/terms-and-conditions\"><ds-text bold color=\"primary\" > Terms and conditions</ds-text></a>.",
"data-privacy": " I have read and understood the <a href=\"https://human-connection.org/datenschutz/\" target=\"_blank\"><ds-text bold color=\"primary\" >Privacy Statement</ds-text></a> ", "data-privacy": " I have read and understood the <a href=\"https://human-connection.org/datenschutz/\" target=\"_blank\"><ds-text bold color=\"primary\" >Privacy Statement</ds-text></a> ",
"minimum-age": "I'm 18 years or older.", "minimum-age": "I'm 18 years or older.",
"no-commercial": "I have no commercial interests and I am not representing a company or any other commercial organisation on the network.",
"no-political": "I am not on behalf of a party or political organization in the network.",
"invitation-code": "Your invitation code is: <b>{code}</b>", "invitation-code": "Your invitation code is: <b>{code}</b>",
"errors": { "errors": {
"email-exists": "There is already a user account with this e-mail address!", "email-exists": "There is already a user account with this e-mail address!",
@ -269,10 +271,10 @@
"name": "Download Data" "name": "Download Data"
}, },
"deleteUserAccount": { "deleteUserAccount": {
"name": "Delete data", "name": "Delete user account",
"contributionsCount": "Delete my {count} posts", "contributionsCount": "Delete my {count} posts",
"commentedCount": "Delete my {count} comments", "commentedCount": "Delete my {count} comments",
"accountDescription": "Be aware that your Post and Comments are important to our community. If you still choose to delete them, you have to mark them below.", "accountDescription": "Be aware that your Posts and Comments are important to our community. If you still choose to delete them, you have to mark them below.",
"accountWarning": "You <b>CAN'T MANAGE</b> and <b>CAN'T RECOVER</b> your Account, Posts, or Comments after deleting your account!", "accountWarning": "You <b>CAN'T MANAGE</b> and <b>CAN'T RECOVER</b> your Account, Posts, or Comments after deleting your account!",
"success": "Account successfully deleted!", "success": "Account successfully deleted!",
"pleaseConfirm": "<b class='is-danger'>Destructive action!</b> Type <b>{confirm}</b> to confirm" "pleaseConfirm": "<b class='is-danger'>Destructive action!</b> Type <b>{confirm}</b> to confirm"

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