mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge remote-tracking branch 'origin/master' into 17-Admin-Remove-user-profile
This commit is contained in:
commit
6a0b5a22e0
11
.travis.yml
11
.travis.yml
@ -1,5 +1,10 @@
|
||||
dist: xenial
|
||||
language: generic
|
||||
language: node_js
|
||||
node_js: lts/*
|
||||
cache:
|
||||
yarn: false
|
||||
npm: false
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
@ -7,11 +12,11 @@ addons:
|
||||
snaps:
|
||||
- docker
|
||||
firefox: "latest-esr"
|
||||
|
||||
|
||||
install:
|
||||
- yarn global add wait-on
|
||||
# Install Codecov
|
||||
- yarn install
|
||||
- yarn install --frozen-lockfile
|
||||
- cp backend/.env.template backend/.env
|
||||
|
||||
before_script:
|
||||
|
||||
158
CHANGELOG.md
158
CHANGELOG.md
@ -4,10 +4,168 @@ 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).
|
||||
|
||||
#### [v0.5.0](https://github.com/Human-Connection/Human-Connection/compare/v0.4.2...v0.5.0)
|
||||
|
||||
> 20 March 2020
|
||||
|
||||
- feat: Search for Hashtags [`#3297`](https://github.com/Human-Connection/Human-Connection/pull/3297)
|
||||
- build(deps): bump metascraper-soundcloud from 5.11.6 to 5.11.7 in /backend [`#3300`](https://github.com/Human-Connection/Human-Connection/pull/3300)
|
||||
- build(deps): bump validator from 12.2.0 to 13.0.0 in /backend [`#3299`](https://github.com/Human-Connection/Human-Connection/pull/3299)
|
||||
- build(deps-dev): bump @storybook/addon-a11y from 5.3.14 to 5.3.17 in /webapp [`#3283`](https://github.com/Human-Connection/Human-Connection/pull/3283)
|
||||
- build(deps-dev): bump cross-env from 6.0.3 to 7.0.2 [`#3294`](https://github.com/Human-Connection/Human-Connection/pull/3294)
|
||||
- build(deps-dev): bump @storybook/vue from 5.3.14 to 5.3.17 in /webapp [`#3285`](https://github.com/Human-Connection/Human-Connection/pull/3285)
|
||||
- build(deps): bump graphql-shield from 7.0.14 to 7.2.0 in /backend [`#3288`](https://github.com/Human-Connection/Human-Connection/pull/3288)
|
||||
- build(deps): bump nuxt from 2.11.0 to 2.12.0 in /webapp [`#3291`](https://github.com/Human-Connection/Human-Connection/pull/3291)
|
||||
- build(deps): bump cookie-universal-nuxt from 2.1.2 to 2.1.3 in /webapp [`#3289`](https://github.com/Human-Connection/Human-Connection/pull/3289)
|
||||
- feat: Specs For Searches [`#3199`](https://github.com/Human-Connection/Human-Connection/pull/3199)
|
||||
- chore(ci): Follow cypress docs to cache libraries [`#3292`](https://github.com/Human-Connection/Human-Connection/pull/3292)
|
||||
- build(deps-dev): bump cypress from 4.1.0 to 4.2.0 [`#3287`](https://github.com/Human-Connection/Human-Connection/pull/3287)
|
||||
- build(deps): [security] bump acorn from 5.7.3 to 5.7.4 in /webapp [`#3268`](https://github.com/Human-Connection/Human-Connection/pull/3268)
|
||||
- feat: Introduce graphql image type [`#3043`](https://github.com/Human-Connection/Human-Connection/pull/3043)
|
||||
- build(deps-dev): bump @storybook/addon-actions from 5.3.14 to 5.3.17 in /webapp [`#3284`](https://github.com/Human-Connection/Human-Connection/pull/3284)
|
||||
- build(deps): bump date-fns from 2.10.0 to 2.11.0 in /webapp [`#3281`](https://github.com/Human-Connection/Human-Connection/pull/3281)
|
||||
- chore: Dockerfile/Travis node versions match [`#3267`](https://github.com/Human-Connection/Human-Connection/pull/3267)
|
||||
- build(deps): bump date-fns from 2.10.0 to 2.11.0 in /backend [`#3278`](https://github.com/Human-Connection/Human-Connection/pull/3278)
|
||||
- build(deps): bump @hapi/joi from 17.1.0 to 17.1.1 in /backend [`#3277`](https://github.com/Human-Connection/Human-Connection/pull/3277)
|
||||
- build(deps): bump neo4j-driver from 4.0.1 to 4.0.2 in /backend [`#3276`](https://github.com/Human-Connection/Human-Connection/pull/3276)
|
||||
- build(deps-dev): bump neo4j-driver from 4.0.1 to 4.0.2 [`#3275`](https://github.com/Human-Connection/Human-Connection/pull/3275)
|
||||
- refactor: deprecated slot syntax [2117] [`#3258`](https://github.com/Human-Connection/Human-Connection/pull/3258)
|
||||
- build(deps-dev): bump @storybook/addon-notes from 5.3.14 to 5.3.17 in /webapp [`#3282`](https://github.com/Human-Connection/Human-Connection/pull/3282)
|
||||
- build(deps): bump mustache from 4.0.0 to 4.0.1 in /backend [`#3280`](https://github.com/Human-Connection/Human-Connection/pull/3280)
|
||||
- build(deps): bump @sentry/node from 5.14.1 to 5.14.2 in /backend [`#3274`](https://github.com/Human-Connection/Human-Connection/pull/3274)
|
||||
- build(deps-dev): bump date-fns from 2.10.0 to 2.11.0 [`#3273`](https://github.com/Human-Connection/Human-Connection/pull/3273)
|
||||
- build(deps): [security] bump acorn from 6.3.0 to 6.4.1 in /backend [`#3270`](https://github.com/Human-Connection/Human-Connection/pull/3270)
|
||||
- build(deps): [security] bump acorn from 6.1.1 to 6.4.1 [`#3269`](https://github.com/Human-Connection/Human-Connection/pull/3269)
|
||||
- build(deps): bump graphql-shield from 7.1.0 to 7.2.0 in /backend [`#3265`](https://github.com/Human-Connection/Human-Connection/pull/3265)
|
||||
- build(deps): bump sanitize-html from 1.22.0 to 1.22.1 in /backend [`#3264`](https://github.com/Human-Connection/Human-Connection/pull/3264)
|
||||
- build(deps): bump @sentry/node from 5.14.0 to 5.14.1 in /backend [`#3263`](https://github.com/Human-Connection/Human-Connection/pull/3263)
|
||||
- build(deps-dev): bump @vue/server-test-utils from 1.0.0-beta.31 to 1.0.0-beta.32 in /webapp [`#3249`](https://github.com/Human-Connection/Human-Connection/pull/3249)
|
||||
- build(deps-dev): bump @babel/preset-env from 7.8.6 to 7.8.7 in /webapp [`#3215`](https://github.com/Human-Connection/Human-Connection/pull/3215)
|
||||
- build(deps-dev): bump fuse.js from 3.4.6 to 3.6.1 in /webapp [`#3239`](https://github.com/Human-Connection/Human-Connection/pull/3239)
|
||||
- build(deps): bump @nuxtjs/sentry from 3.2.4 to 3.3.1 in /webapp [`#3237`](https://github.com/Human-Connection/Human-Connection/pull/3237)
|
||||
- build(deps-dev): bump eslint-plugin-jest from 23.8.1 to 23.8.2 in /webapp [`#3228`](https://github.com/Human-Connection/Human-Connection/pull/3228)
|
||||
- chore: Update to version 0.4.2 [`#3261`](https://github.com/Human-Connection/Human-Connection/pull/3261)
|
||||
- Changes requested by @mattwr18 [`9c08db2`](https://github.com/Human-Connection/Human-Connection/commit/9c08db22dcd0ca1ad6e59be8fb0f287935b45537)
|
||||
- search specs refactored [`46fca22`](https://github.com/Human-Connection/Human-Connection/commit/46fca229ec35047eda9ac7809e7bc456785a6c70)
|
||||
- Search for Hashtags works due watching route in pages/index.vue [`1c43d5f`](https://github.com/Human-Connection/Human-Connection/commit/1c43d5fe6f44c7b11168af8bd765b8d785a6641a)
|
||||
|
||||
#### [v0.4.2](https://github.com/Human-Connection/Human-Connection/compare/v0.4.1...v0.4.2)
|
||||
|
||||
> 12 March 2020
|
||||
|
||||
- build(deps): bump @sentry/node from 5.13.1 to 5.14.0 in /backend [`#3260`](https://github.com/Human-Connection/Human-Connection/pull/3260)
|
||||
- build(deps): bump graphql-shield from 7.0.14 to 7.1.0 in /backend [`#3259`](https://github.com/Human-Connection/Human-Connection/pull/3259)
|
||||
- feat: more prominent output of ./scripts/translations/sort.sh and hint to --fix feature of the script on errors [`#3251`](https://github.com/Human-Connection/Human-Connection/pull/3251)
|
||||
- build(deps): bump nodemailer from 6.4.4 to 6.4.5 in /backend [`#3254`](https://github.com/Human-Connection/Human-Connection/pull/3254)
|
||||
- build(deps-dev): bump @vue/test-utils from 1.0.0-beta.31 to 1.0.0-beta.32 in /webapp [`#3248`](https://github.com/Human-Connection/Human-Connection/pull/3248)
|
||||
- build(deps-dev): bump async-validator from 3.2.3 to 3.2.4 in /webapp [`#3255`](https://github.com/Human-Connection/Human-Connection/pull/3255)
|
||||
- build(deps-dev): bump eslint-plugin-jest from 23.8.1 to 23.8.2 in /backend [`#3253`](https://github.com/Human-Connection/Human-Connection/pull/3253)
|
||||
- feature: Delete_user_as_admin_through_API_only [`#3063`](https://github.com/Human-Connection/Human-Connection/pull/3063)
|
||||
- feat: zero bell to all notifications page [2823] [`#3219`](https://github.com/Human-Connection/Human-Connection/pull/3219)
|
||||
- fix: layout shift [2607] [`#3218`](https://github.com/Human-Connection/Human-Connection/pull/3218)
|
||||
- feat: Documentation for locales script [`#3242`](https://github.com/Human-Connection/Human-Connection/pull/3242)
|
||||
- build(deps): bump metascraper-audio from 5.11.1 to 5.11.6 in /backend [`#3235`](https://github.com/Human-Connection/Human-Connection/pull/3235)
|
||||
- build(deps): bump metascraper-video from 5.11.1 to 5.11.6 in /backend [`#3247`](https://github.com/Human-Connection/Human-Connection/pull/3247)
|
||||
- build(deps): bump metascraper-soundcloud from 5.11.5 to 5.11.6 in /backend [`#3246`](https://github.com/Human-Connection/Human-Connection/pull/3246)
|
||||
- build(deps): bump metascraper-lang from 5.11.1 to 5.11.6 in /backend [`#3234`](https://github.com/Human-Connection/Human-Connection/pull/3234)
|
||||
- build(deps): bump metascraper-description from 5.11.1 to 5.11.6 in /backend [`#3233`](https://github.com/Human-Connection/Human-Connection/pull/3233)
|
||||
- build(deps): bump cross-env from 7.0.1 to 7.0.2 in /backend [`#3245`](https://github.com/Human-Connection/Human-Connection/pull/3245)
|
||||
- build(deps): bump metascraper-title from 5.11.1 to 5.11.6 in /backend [`#3244`](https://github.com/Human-Connection/Human-Connection/pull/3244)
|
||||
- chore: Update to v0.4.1 [`#3243`](https://github.com/Human-Connection/Human-Connection/pull/3243)
|
||||
- DRY user.spec.js [`da16590`](https://github.com/Human-Connection/Human-Connection/commit/da165906e2ed12baddd902b43064103ab3adfa06)
|
||||
- test deleteuser as admin, moderator, another user and as I myself, fix lint [`3983612`](https://github.com/Human-Connection/Human-Connection/commit/3983612c56ac92473a192a318959e4c691a3e7b8)
|
||||
- feature: test delete user as admin [`84c1547`](https://github.com/Human-Connection/Human-Connection/commit/84c154798efac0cec4c13dfefae18a6a9542058a)
|
||||
|
||||
#### [v0.4.1](https://github.com/Human-Connection/Human-Connection/compare/v0.4.0...v0.4.1)
|
||||
|
||||
> 9 March 2020
|
||||
|
||||
- build(deps): bump metascraper-publisher from 5.11.1 to 5.11.6 in /backend [`#3226`](https://github.com/Human-Connection/Human-Connection/pull/3226)
|
||||
- build(deps-dev): bump eslint-plugin-vue from 6.2.1 to 6.2.2 in /webapp [`#3238`](https://github.com/Human-Connection/Human-Connection/pull/3238)
|
||||
- build(deps): bump metascraper-date from 5.11.1 to 5.11.6 in /backend [`#3236`](https://github.com/Human-Connection/Human-Connection/pull/3236)
|
||||
- build(deps): bump metascraper-image from 5.11.1 to 5.11.6 in /backend [`#3224`](https://github.com/Human-Connection/Human-Connection/pull/3224)
|
||||
- build(deps): bump uuid from 7.0.1 to 7.0.2 in /backend [`#3214`](https://github.com/Human-Connection/Human-Connection/pull/3214)
|
||||
- build(deps-dev): bump cypress from 4.0.2 to 4.1.0 [`#3190`](https://github.com/Human-Connection/Human-Connection/pull/3190)
|
||||
- build(deps): bump cross-env from 7.0.1 to 7.0.2 in /webapp [`#3230`](https://github.com/Human-Connection/Human-Connection/pull/3230)
|
||||
- build(deps): bump vue-infinite-loading from 2.4.4 to 2.4.5 in /webapp [`#3227`](https://github.com/Human-Connection/Human-Connection/pull/3227)
|
||||
- build(deps): bump metascraper-youtube from 5.11.1 to 5.11.6 in /backend [`#3225`](https://github.com/Human-Connection/Human-Connection/pull/3225)
|
||||
- build(deps): bump metascraper-url from 5.11.1 to 5.11.6 in /backend [`#3223`](https://github.com/Human-Connection/Human-Connection/pull/3223)
|
||||
- build(deps): bump metascraper-author from 5.11.1 to 5.11.6 in /backend [`#3222`](https://github.com/Human-Connection/Human-Connection/pull/3222)
|
||||
- build(deps): bump metascraper-logo from 5.11.1 to 5.11.6 in /backend [`#3221`](https://github.com/Human-Connection/Human-Connection/pull/3221)
|
||||
- build(deps): bump metascraper from 5.11.4 to 5.11.6 in /backend [`#3220`](https://github.com/Human-Connection/Human-Connection/pull/3220)
|
||||
- build(deps-dev): bump @storybook/addon-a11y from 5.3.13 to 5.3.14 in /webapp [`#3167`](https://github.com/Human-Connection/Human-Connection/pull/3167)
|
||||
- build(deps-dev): bump @babel/core from 7.8.6 to 7.8.7 in /backend [`#3213`](https://github.com/Human-Connection/Human-Connection/pull/3213)
|
||||
- build(deps): bump metascraper-soundcloud from 5.11.4 to 5.11.5 in /backend [`#3189`](https://github.com/Human-Connection/Human-Connection/pull/3189)
|
||||
- build(deps-dev): bump @babel/preset-env from 7.8.6 to 7.8.7 in /backend [`#3211`](https://github.com/Human-Connection/Human-Connection/pull/3211)
|
||||
- build(deps-dev): bump @babel/core from 7.8.6 to 7.8.7 [`#3210`](https://github.com/Human-Connection/Human-Connection/pull/3210)
|
||||
- build(deps-dev): bump @babel/core from 7.8.6 to 7.8.7 in /webapp [`#3216`](https://github.com/Human-Connection/Human-Connection/pull/3216)
|
||||
- build(deps-dev): bump @babel/node from 7.8.4 to 7.8.7 in /backend [`#3212`](https://github.com/Human-Connection/Human-Connection/pull/3212)
|
||||
- build(deps-dev): bump @babel/preset-env from 7.8.6 to 7.8.7 [`#3209`](https://github.com/Human-Connection/Human-Connection/pull/3209)
|
||||
- perf(neo4j): Improve currentUser read performance [`#3207`](https://github.com/Human-Connection/Human-Connection/pull/3207)
|
||||
- build(deps-dev): bump apollo-server-testing from 2.10.1 to 2.11.0 in /backend [`#3205`](https://github.com/Human-Connection/Human-Connection/pull/3205)
|
||||
- build(deps): bump apollo-server from 2.10.1 to 2.11.0 in /backend [`#3201`](https://github.com/Human-Connection/Human-Connection/pull/3201)
|
||||
- build(deps): bump cross-env from 7.0.0 to 7.0.1 in /webapp [`#3206`](https://github.com/Human-Connection/Human-Connection/pull/3206)
|
||||
- build(deps): bump apollo-server-express from 2.10.1 to 2.11.0 in /backend [`#3202`](https://github.com/Human-Connection/Human-Connection/pull/3202)
|
||||
- build(deps): bump graphql-redis-subscriptions from 2.1.2 to 2.2.1 in /backend [`#3203`](https://github.com/Human-Connection/Human-Connection/pull/3203)
|
||||
- build(deps): bump cross-env from 7.0.0 to 7.0.1 in /backend [`#3204`](https://github.com/Human-Connection/Human-Connection/pull/3204)
|
||||
- build(deps-dev): bump @babel/preset-env from 7.8.4 to 7.8.6 [`#3175`](https://github.com/Human-Connection/Human-Connection/pull/3175)
|
||||
- feat: Russian Translations Update By Ewald Arnold [`#3198`](https://github.com/Human-Connection/Human-Connection/pull/3198)
|
||||
- feat: Translations update [`#3111`](https://github.com/Human-Connection/Human-Connection/pull/3111)
|
||||
- build(deps-dev): bump @babel/core from 7.8.4 to 7.8.6 in /backend [`#3172`](https://github.com/Human-Connection/Human-Connection/pull/3172)
|
||||
- build(deps-dev): bump @babel/core from 7.8.4 to 7.8.6 [`#3173`](https://github.com/Human-Connection/Human-Connection/pull/3173)
|
||||
- fix: Update user.updatedAt when password is reset [`#3197`](https://github.com/Human-Connection/Human-Connection/pull/3197)
|
||||
- build(deps-dev): bump @babel/register from 7.8.3 to 7.8.6 in /backend [`#3174`](https://github.com/Human-Connection/Human-Connection/pull/3174)
|
||||
- build(deps-dev): bump @babel/preset-env from 7.8.4 to 7.8.6 in /webapp [`#3183`](https://github.com/Human-Connection/Human-Connection/pull/3183)
|
||||
- build(deps-dev): bump eslint-plugin-jest from 23.8.0 to 23.8.1 in /backend [`#3191`](https://github.com/Human-Connection/Human-Connection/pull/3191)
|
||||
- build(deps): bump wait-on from 4.0.0 to 4.0.1 in /backend [`#3176`](https://github.com/Human-Connection/Human-Connection/pull/3176)
|
||||
- build(deps-dev): bump @babel/register from 7.8.3 to 7.8.6 [`#3179`](https://github.com/Human-Connection/Human-Connection/pull/3179)
|
||||
- build(deps-dev): bump @babel/preset-env from 7.8.4 to 7.8.6 in /backend [`#3181`](https://github.com/Human-Connection/Human-Connection/pull/3181)
|
||||
- build(deps-dev): bump @babel/core from 7.8.4 to 7.8.6 in /webapp [`#3182`](https://github.com/Human-Connection/Human-Connection/pull/3182)
|
||||
- build(deps-dev): bump @vue/cli-shared-utils from 4.2.2 to 4.2.3 in /webapp [`#3184`](https://github.com/Human-Connection/Human-Connection/pull/3184)
|
||||
- build(deps): bump @sentry/node from 5.12.4 to 5.13.1 in /backend [`#3192`](https://github.com/Human-Connection/Human-Connection/pull/3192)
|
||||
- build(deps): bump nodemailer from 6.4.3 to 6.4.4 in /backend [`#3193`](https://github.com/Human-Connection/Human-Connection/pull/3193)
|
||||
- build(deps-dev): bump eslint-plugin-jest from 23.8.0 to 23.8.1 in /webapp [`#3195`](https://github.com/Human-Connection/Human-Connection/pull/3195)
|
||||
- build(deps): bump date-fns from 2.9.0 to 2.10.0 in /backend [`#3159`](https://github.com/Human-Connection/Human-Connection/pull/3159)
|
||||
- build(deps-dev): bump @storybook/vue from 5.3.13 to 5.3.14 in /webapp [`#3165`](https://github.com/Human-Connection/Human-Connection/pull/3165)
|
||||
- build(deps-dev): bump babel-eslint from 10.0.3 to 10.1.0 in /webapp [`#3168`](https://github.com/Human-Connection/Human-Connection/pull/3168)
|
||||
- build(deps-dev): bump @storybook/addon-actions from 5.3.13 to 5.3.14 in /webapp [`#3166`](https://github.com/Human-Connection/Human-Connection/pull/3166)
|
||||
- build(deps-dev): bump date-fns from 2.9.0 to 2.10.0 [`#3160`](https://github.com/Human-Connection/Human-Connection/pull/3160)
|
||||
- build(deps-dev): bump @storybook/addon-notes from 5.3.13 to 5.3.14 in /webapp [`#3164`](https://github.com/Human-Connection/Human-Connection/pull/3164)
|
||||
- build(deps): bump date-fns from 2.9.0 to 2.10.0 in /webapp [`#3163`](https://github.com/Human-Connection/Human-Connection/pull/3163)
|
||||
- build(deps-dev): bump babel-eslint from 10.0.3 to 10.1.0 in /backend [`#3162`](https://github.com/Human-Connection/Human-Connection/pull/3162)
|
||||
- build(deps): bump uuid from 7.0.0 to 7.0.1 in /backend [`#3161`](https://github.com/Human-Connection/Human-Connection/pull/3161)
|
||||
- build(deps): bump xregexp from 4.2.4 to 4.3.0 in /backend [`#3044`](https://github.com/Human-Connection/Human-Connection/pull/3044)
|
||||
- build(deps): bump metascraper-url from 5.10.7 to 5.11.1 in /backend [`#3147`](https://github.com/Human-Connection/Human-Connection/pull/3147)
|
||||
- chore(build): Fix uuid deprecations [`#3156`](https://github.com/Human-Connection/Human-Connection/pull/3156)
|
||||
- build(deps): bump graphql-shield from 7.0.13 to 7.0.14 in /backend [`#3153`](https://github.com/Human-Connection/Human-Connection/pull/3153)
|
||||
- build(deps): bump metascraper-title from 5.10.7 to 5.11.1 in /backend [`#3148`](https://github.com/Human-Connection/Human-Connection/pull/3148)
|
||||
- build(deps): bump helmet from 3.21.2 to 3.21.3 in /backend [`#3154`](https://github.com/Human-Connection/Human-Connection/pull/3154)
|
||||
- build(deps): bump uuid from 3.4.0 to 7.0.0 in /backend [`#3155`](https://github.com/Human-Connection/Human-Connection/pull/3155)
|
||||
- build(deps-dev): bump eslint-plugin-jest from 23.7.0 to 23.8.0 in /webapp [`#3150`](https://github.com/Human-Connection/Human-Connection/pull/3150)
|
||||
- fix(webapp): remove ribbon z-index [`#3152`](https://github.com/Human-Connection/Human-Connection/pull/3152)
|
||||
- build(deps): bump metascraper-description from 5.11.0 to 5.11.1 in /backend [`#3149`](https://github.com/Human-Connection/Human-Connection/pull/3149)
|
||||
- build(deps-dev): bump eslint-plugin-jest from 23.7.0 to 23.8.0 in /backend [`#3146`](https://github.com/Human-Connection/Human-Connection/pull/3146)
|
||||
- build(deps): bump sanitize-html from 1.21.1 to 1.22.0 in /backend [`#3145`](https://github.com/Human-Connection/Human-Connection/pull/3145)
|
||||
- build(deps): bump nodemailer from 6.4.2 to 6.4.3 in /backend [`#3144`](https://github.com/Human-Connection/Human-Connection/pull/3144)
|
||||
- build(deps): bump metascraper-video from 5.10.7 to 5.11.1 in /backend [`#3143`](https://github.com/Human-Connection/Human-Connection/pull/3143)
|
||||
- feat: the point -no political use - added [`#3138`](https://github.com/Human-Connection/Human-Connection/pull/3138)
|
||||
- build(deps): bump metascraper-lang from 5.10.7 to 5.11.1 in /backend [`#3071`](https://github.com/Human-Connection/Human-Connection/pull/3071)
|
||||
- build(deps): bump metascraper from 5.11.0 to 5.11.4 in /backend [`#3136`](https://github.com/Human-Connection/Human-Connection/pull/3136)
|
||||
- build(deps): bump metascraper-soundcloud from 5.10.7 to 5.11.4 in /backend [`#3137`](https://github.com/Human-Connection/Human-Connection/pull/3137)
|
||||
- chore: Update to v0.4.0 [`#3132`](https://github.com/Human-Connection/Human-Connection/pull/3132)
|
||||
- build(deps): bump metascraper-logo from 5.10.7 to 5.11.1 in /backend [`#3126`](https://github.com/Human-Connection/Human-Connection/pull/3126)
|
||||
- chore(cypress): Favor firefox in cypress [`#3121`](https://github.com/Human-Connection/Human-Connection/pull/3121)
|
||||
- build(deps): bump graphql-shield from 7.0.11 to 7.0.13 in /backend [`#3127`](https://github.com/Human-Connection/Human-Connection/pull/3127)
|
||||
- build(deps): bump ioredis from 4.14.1 to 4.16.0 in /backend [`#3128`](https://github.com/Human-Connection/Human-Connection/pull/3128)
|
||||
- build(deps-dev): bump @storybook/addon-notes in /webapp [`5ef2b25`](https://github.com/Human-Connection/Human-Connection/commit/5ef2b25ee6a3402a2ebe2f3f55dd65a6e0a1111e)
|
||||
- build(deps-dev): bump @storybook/addon-a11y in /webapp [`f209436`](https://github.com/Human-Connection/Human-Connection/commit/f209436147fbf9afacfbd6edb6135847e6c4faed)
|
||||
- build(deps): bump apollo-server-express in /backend [`3b35487`](https://github.com/Human-Connection/Human-Connection/commit/3b35487f0671490dee1e636fb938c408722bfd45)
|
||||
|
||||
#### [v0.4.0](https://github.com/Human-Connection/Human-Connection/compare/v0.3.1...v0.4.0)
|
||||
|
||||
> 21 February 2020
|
||||
|
||||
- build(deps): bump apollo-server from 2.10.0 to 2.10.1 in /backend [`#3125`](https://github.com/Human-Connection/Human-Connection/pull/3125)
|
||||
- fix: Favor Cypress.Promise over async/await in e2e tests [`#3115`](https://github.com/Human-Connection/Human-Connection/pull/3115)
|
||||
- docs(setup): Fix links in tests [`#3120`](https://github.com/Human-Connection/Human-Connection/pull/3120)
|
||||
- feat: 🍰 Expose sensitive report type to moderators only [`#3075`](https://github.com/Human-Connection/Human-Connection/pull/3075)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "human-connection-backend",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.0",
|
||||
"description": "GraphQL Backend for Human Connection",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
@ -37,8 +37,8 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@hapi/joi": "^17.1.0",
|
||||
"@sentry/node": "^5.13.1",
|
||||
"@hapi/joi": "^17.1.1",
|
||||
"@sentry/node": "^5.14.2",
|
||||
"apollo-cache-inmemory": "~1.6.5",
|
||||
"apollo-client": "~2.6.8",
|
||||
"apollo-link-context": "~1.0.19",
|
||||
@ -49,8 +49,8 @@
|
||||
"bcryptjs": "~2.4.3",
|
||||
"cheerio": "~1.0.0-rc.3",
|
||||
"cors": "~2.8.5",
|
||||
"cross-env": "~7.0.1",
|
||||
"date-fns": "2.10.0",
|
||||
"cross-env": "~7.0.2",
|
||||
"date-fns": "2.11.0",
|
||||
"debug": "~4.1.1",
|
||||
"dotenv": "~8.2.0",
|
||||
"express": "^4.17.1",
|
||||
@ -61,7 +61,7 @@
|
||||
"graphql-middleware": "~4.0.2",
|
||||
"graphql-middleware-sentry": "^3.2.1",
|
||||
"graphql-redis-subscriptions": "^2.2.1",
|
||||
"graphql-shield": "~7.0.14",
|
||||
"graphql-shield": "~7.2.1",
|
||||
"graphql-tag": "~2.10.3",
|
||||
"helmet": "~3.21.3",
|
||||
"ioredis": "^4.16.0",
|
||||
@ -70,25 +70,25 @@
|
||||
"lodash": "~4.17.14",
|
||||
"merge-graphql-schemas": "^1.7.6",
|
||||
"metascraper": "^5.11.6",
|
||||
"metascraper-audio": "^5.11.1",
|
||||
"metascraper-audio": "^5.11.6",
|
||||
"metascraper-author": "^5.11.6",
|
||||
"metascraper-clearbit-logo": "^5.3.0",
|
||||
"metascraper-date": "^5.11.6",
|
||||
"metascraper-description": "^5.11.1",
|
||||
"metascraper-description": "^5.11.6",
|
||||
"metascraper-image": "^5.11.6",
|
||||
"metascraper-lang": "^5.11.1",
|
||||
"metascraper-lang": "^5.11.6",
|
||||
"metascraper-lang-detector": "^4.10.2",
|
||||
"metascraper-logo": "^5.11.6",
|
||||
"metascraper-publisher": "^5.11.6",
|
||||
"metascraper-soundcloud": "^5.11.5",
|
||||
"metascraper-title": "^5.11.1",
|
||||
"metascraper-soundcloud": "^5.11.7",
|
||||
"metascraper-title": "^5.11.6",
|
||||
"metascraper-url": "^5.11.6",
|
||||
"metascraper-video": "^5.11.1",
|
||||
"metascraper-video": "^5.11.6",
|
||||
"metascraper-youtube": "^5.11.6",
|
||||
"migrate": "^1.6.2",
|
||||
"minimatch": "^3.0.4",
|
||||
"mustache": "^4.0.0",
|
||||
"neo4j-driver": "^4.0.1",
|
||||
"mustache": "^4.0.1",
|
||||
"neo4j-driver": "^4.0.2",
|
||||
"neo4j-graphql-js": "^2.11.5",
|
||||
"neode": "^0.3.7",
|
||||
"node-fetch": "~2.6.0",
|
||||
@ -101,7 +101,7 @@
|
||||
"subscriptions-transport-ws": "^0.9.16",
|
||||
"trunc-html": "~1.1.2",
|
||||
"uuid": "~7.0.2",
|
||||
"validator": "^12.2.0",
|
||||
"validator": "^13.0.0",
|
||||
"wait-on": "~4.0.1",
|
||||
"xregexp": "^4.3.0"
|
||||
},
|
||||
@ -122,7 +122,7 @@
|
||||
"eslint-config-prettier": "~6.10.0",
|
||||
"eslint-config-standard": "~14.1.0",
|
||||
"eslint-plugin-import": "~2.20.1",
|
||||
"eslint-plugin-jest": "~23.8.1",
|
||||
"eslint-plugin-jest": "~23.8.2",
|
||||
"eslint-plugin-node": "~11.0.0",
|
||||
"eslint-plugin-prettier": "~3.1.2",
|
||||
"eslint-plugin-promise": "~4.2.1",
|
||||
|
||||
@ -7,7 +7,7 @@ import request from 'request'
|
||||
import NitroDataSource from './NitroDataSource'
|
||||
import router from './routes'
|
||||
import Collections from './Collections'
|
||||
import uuid from 'uuid/v4'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import CONFIG from '../config'
|
||||
const debug = require('debug')('ea')
|
||||
|
||||
|
||||
@ -4,9 +4,16 @@ import slugify from 'slug'
|
||||
import { hashSync } from 'bcryptjs'
|
||||
import { Factory } from 'rosie'
|
||||
import { getDriver, getNeode } from './neo4j'
|
||||
import CONFIG from '../config/index.js'
|
||||
|
||||
const neode = getNeode()
|
||||
|
||||
const uniqueImageUrl = imageUrl => {
|
||||
const newUrl = new URL(imageUrl, CONFIG.CLIENT_URI)
|
||||
newUrl.search = `random=${uuid()}`
|
||||
return newUrl.toString()
|
||||
}
|
||||
|
||||
export const cleanDatabase = async (options = {}) => {
|
||||
const { driver = getDriver() } = options
|
||||
const session = driver.session()
|
||||
@ -39,14 +46,23 @@ Factory.define('badge')
|
||||
return neode.create('Badge', buildObject)
|
||||
})
|
||||
|
||||
Factory.define('userWithoutEmailAddress')
|
||||
Factory.define('image')
|
||||
.attr('url', faker.image.unsplash.imageUrl)
|
||||
.attr('aspectRatio', 1)
|
||||
.attr('alt', faker.lorem.sentence)
|
||||
.after((buildObject, options) => {
|
||||
const { url: imageUrl } = buildObject
|
||||
if (imageUrl) buildObject.url = uniqueImageUrl(imageUrl)
|
||||
return neode.create('Image', buildObject)
|
||||
})
|
||||
|
||||
Factory.define('basicUser')
|
||||
.option('password', '1234')
|
||||
.attrs({
|
||||
id: uuid,
|
||||
name: faker.name.findName,
|
||||
password: '1234',
|
||||
role: 'user',
|
||||
avatar: faker.internet.avatar,
|
||||
about: faker.lorem.paragraph,
|
||||
termsAndConditionsAgreedVersion: '0.0.1',
|
||||
termsAndConditionsAgreedAt: '2019-08-01T10:47:19.212Z',
|
||||
@ -60,19 +76,29 @@ Factory.define('userWithoutEmailAddress')
|
||||
.attr('encryptedPassword', ['password'], password => {
|
||||
return hashSync(password, 10)
|
||||
})
|
||||
|
||||
Factory.define('userWithoutEmailAddress')
|
||||
.extend('basicUser')
|
||||
.after(async (buildObject, options) => {
|
||||
return neode.create('User', buildObject)
|
||||
})
|
||||
|
||||
Factory.define('user')
|
||||
.extend('userWithoutEmailAddress')
|
||||
.extend('basicUser')
|
||||
.option('email', faker.internet.exampleEmail)
|
||||
.option('avatar', () =>
|
||||
Factory.build('image', {
|
||||
url: faker.internet.avatar(),
|
||||
}),
|
||||
)
|
||||
.after(async (buildObject, options) => {
|
||||
const [user, email] = await Promise.all([
|
||||
buildObject,
|
||||
const [user, email, avatar] = await Promise.all([
|
||||
neode.create('User', buildObject),
|
||||
neode.create('EmailAddress', { email: options.email }),
|
||||
options.avatar,
|
||||
])
|
||||
await Promise.all([user.relateTo(email, 'primaryEmail'), email.relateTo(user, 'belongsTo')])
|
||||
if (avatar) await user.relateTo(avatar, 'avatar')
|
||||
return user
|
||||
})
|
||||
|
||||
@ -93,11 +119,11 @@ Factory.define('post')
|
||||
return Factory.build('user')
|
||||
})
|
||||
.option('pinnedBy', null)
|
||||
.option('image', () => Factory.build('image'))
|
||||
.attrs({
|
||||
id: uuid,
|
||||
title: faker.lorem.sentence,
|
||||
content: faker.lorem.paragraphs,
|
||||
image: faker.image.unsplash.imageUrl,
|
||||
visibility: 'public',
|
||||
deleted: false,
|
||||
imageBlurred: false,
|
||||
@ -117,9 +143,10 @@ Factory.define('post')
|
||||
return language || 'en'
|
||||
})
|
||||
.after(async (buildObject, options) => {
|
||||
const [post, author, categories, tags] = await Promise.all([
|
||||
const [post, author, image, categories, tags] = await Promise.all([
|
||||
neode.create('Post', buildObject),
|
||||
options.author,
|
||||
options.image,
|
||||
options.categories,
|
||||
options.tags,
|
||||
])
|
||||
@ -128,6 +155,7 @@ Factory.define('post')
|
||||
Promise.all(categories.map(c => c.relateTo(post, 'post'))),
|
||||
Promise.all(tags.map(t => t.relateTo(post, 'post'))),
|
||||
])
|
||||
if (image) await post.relateTo(image, 'image')
|
||||
if (buildObject.pinned) {
|
||||
const pinnedBy = await (options.pinnedBy || Factory.build('user', { role: 'admin' }))
|
||||
await pinnedBy.relateTo(post, 'pinned')
|
||||
|
||||
@ -12,6 +12,7 @@ class Store {
|
||||
[
|
||||
'CALL db.index.fulltext.createNodeIndex("post_fulltext_search",["Post"],["title", "content"])',
|
||||
'CALL db.index.fulltext.createNodeIndex("user_fulltext_search",["User"],["name", "slug"])',
|
||||
'CALL db.index.fulltext.createNodeIndex("tag_fulltext_search",["Tag"],["id"])',
|
||||
].map(statement => txc.run(statement)),
|
||||
)
|
||||
})
|
||||
|
||||
@ -40,6 +40,7 @@ export async function down(next) {
|
||||
await transaction.rollback()
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('rolled back')
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
@ -0,0 +1,51 @@
|
||||
import { getDriver } from '../../db/neo4j'
|
||||
|
||||
export const description =
|
||||
'This migration adds a fulltext index for the tags in order to search for Hasthags.'
|
||||
|
||||
export async function up(next) {
|
||||
const driver = getDriver()
|
||||
const session = driver.session()
|
||||
const transaction = session.beginTransaction()
|
||||
|
||||
try {
|
||||
await transaction.run(`
|
||||
CALL db.index.fulltext.createNodeIndex("tag_fulltext_search",["Tag"],["id"])
|
||||
`)
|
||||
await transaction.commit()
|
||||
next()
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error)
|
||||
await transaction.rollback()
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('rolled back')
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(next) {
|
||||
const driver = getDriver()
|
||||
const session = driver.session()
|
||||
const transaction = session.beginTransaction()
|
||||
|
||||
try {
|
||||
// Implement your migration here.
|
||||
await transaction.run(`
|
||||
CALL db.index.fulltext.drop("tag_fulltext_search")
|
||||
`)
|
||||
await transaction.commit()
|
||||
next()
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error)
|
||||
await transaction.rollback()
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('rolled back')
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
import { getDriver } from '../../db/neo4j'
|
||||
|
||||
export const description = `
|
||||
We introduced a new node label 'Image' and we need a primary key for it. Best
|
||||
would probably be the 'url' property which should be unique and would also
|
||||
prevent us from overwriting existing images.
|
||||
`
|
||||
|
||||
export async function up(next) {
|
||||
const driver = getDriver()
|
||||
const session = driver.session()
|
||||
const transaction = session.beginTransaction()
|
||||
|
||||
try {
|
||||
// Implement your migration here.
|
||||
await transaction.run(`
|
||||
CREATE CONSTRAINT ON ( image:Image ) ASSERT image.url IS UNIQUE
|
||||
`)
|
||||
await transaction.commit()
|
||||
next()
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error)
|
||||
await transaction.rollback()
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('rolled back')
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(next) {
|
||||
const driver = getDriver()
|
||||
const session = driver.session()
|
||||
const transaction = session.beginTransaction()
|
||||
|
||||
try {
|
||||
// Implement your migration here.
|
||||
await transaction.run(`
|
||||
DROP CONSTRAINT ON ( image:Image ) ASSERT image.url IS UNIQUE
|
||||
`)
|
||||
await transaction.commit()
|
||||
next()
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error)
|
||||
await transaction.rollback()
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('rolled back')
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
/* eslint-disable no-console */
|
||||
import { getDriver } from '../../db/neo4j'
|
||||
|
||||
export const description = `
|
||||
Refactor all our image properties on posts and users to a dedicated type
|
||||
"Image" which contains metadata and image file urls.
|
||||
`
|
||||
|
||||
const printSummaries = summaries => {
|
||||
console.log('=========================================')
|
||||
summaries.forEach(stat => {
|
||||
console.log(stat.query.text)
|
||||
console.log(JSON.stringify(stat.counters, null, 2))
|
||||
})
|
||||
console.log('=========================================')
|
||||
}
|
||||
|
||||
export async function up() {
|
||||
const driver = getDriver()
|
||||
const session = driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async txc => {
|
||||
const runs = await Promise.all(
|
||||
[
|
||||
`
|
||||
MATCH (post:Post)
|
||||
WHERE post.image IS NOT NULL AND post.deleted = FALSE
|
||||
MERGE(image:Image {url: post.image})
|
||||
CREATE (post)-[:HERO_IMAGE]->(image)
|
||||
SET
|
||||
image.sensitive = post.imageBlurred,
|
||||
image.aspectRatio = post.imageAspectRatio
|
||||
REMOVE
|
||||
post.image,
|
||||
post.imageBlurred,
|
||||
post.imageAspectRatio
|
||||
`,
|
||||
`
|
||||
MATCH (user:User)
|
||||
WHERE user.avatar IS NOT NULL AND user.deleted = FALSE
|
||||
MERGE(avatar:Image {url: user.avatar})
|
||||
CREATE (user)-[:AVATAR_IMAGE]->(avatar)
|
||||
REMOVE user.avatar
|
||||
`,
|
||||
`
|
||||
MATCH (user:User)
|
||||
WHERE user.coverImg IS NOT NULL AND user.deleted = FALSE
|
||||
MERGE(coverImage:Image {url: user.coverImg})
|
||||
CREATE (user)-[:COVER_IMAGE]->(coverImage)
|
||||
REMOVE user.coverImg
|
||||
`,
|
||||
].map(s => txc.run(s)),
|
||||
)
|
||||
return runs.map(({ summary }) => summary)
|
||||
})
|
||||
|
||||
try {
|
||||
const stats = await writeTxResultPromise
|
||||
console.log('Created image nodes from all user avatars and post images.')
|
||||
printSummaries(stats)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
export async function down() {
|
||||
const driver = getDriver()
|
||||
const session = driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async txc => {
|
||||
const runs = await Promise.all(
|
||||
[
|
||||
`
|
||||
MATCH (post)-[:HERO_IMAGE]->(image:Image)
|
||||
SET
|
||||
post.image = image.url,
|
||||
post.imageBlurred = image.sensitive,
|
||||
post.imageAspectRatio = image.aspectRatio
|
||||
DETACH DELETE image
|
||||
`,
|
||||
`
|
||||
MATCH(user)-[:AVATAR_IMAGE]->(avatar:Image)
|
||||
SET user.avatar = avatar.url
|
||||
DETACH DELETE avatar
|
||||
`,
|
||||
`
|
||||
MATCH(user)-[:COVER_IMAGE]->(coverImage:Image)
|
||||
SET user.coverImg = coverImage.url
|
||||
DETACH DELETE coverImage
|
||||
`,
|
||||
].map(s => txc.run(s)),
|
||||
)
|
||||
return runs.map(({ summary }) => summary)
|
||||
})
|
||||
|
||||
try {
|
||||
const stats = await writeTxResultPromise
|
||||
console.log('UNDO: Split images from users and posts.')
|
||||
printSummaries(stats)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
import { getDriver } from '../../db/neo4j'
|
||||
|
||||
export const description =
|
||||
'We should not maintain obsolete attributes for users who have been deleted.'
|
||||
|
||||
export async function up(next) {
|
||||
const driver = getDriver()
|
||||
const session = driver.session()
|
||||
const transaction = session.beginTransaction()
|
||||
const updateDeletedUserAttributes = await transaction.run(`
|
||||
MATCH (user:User)
|
||||
WHERE user.deleted = TRUE
|
||||
SET user.createdAt = 'UNAVAILABLE'
|
||||
SET user.updatedAt = 'UNAVAILABLE'
|
||||
SET user.lastActiveAt = 'UNAVAILABLE'
|
||||
SET user.termsAndConditionsAgreedVersion = 'UNAVAILABLE'
|
||||
SET user.avatar = null
|
||||
SET user.coverImg = null
|
||||
RETURN user {.*};
|
||||
`)
|
||||
try {
|
||||
// Implement your migration here.
|
||||
const users = await updateDeletedUserAttributes.records.map(record => record.get('user'))
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(users)
|
||||
await transaction.commit()
|
||||
next()
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error)
|
||||
await transaction.rollback()
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('rolled back')
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(next) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Irreversible migration')
|
||||
next()
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
import { getDriver } from '../../db/neo4j'
|
||||
|
||||
export const description =
|
||||
'We should not maintain obsolete attributes for posts which have been deleted.'
|
||||
|
||||
export async function up(next) {
|
||||
const driver = getDriver()
|
||||
const session = driver.session()
|
||||
const transaction = session.beginTransaction()
|
||||
const updateDeletedPostsAttributes = await transaction.run(`
|
||||
MATCH (post:Post)
|
||||
WHERE post.deleted = TRUE
|
||||
SET post.language = 'UNAVAILABLE'
|
||||
SET post.createdAt = 'UNAVAILABLE'
|
||||
SET post.updatedAt = 'UNAVAILABLE'
|
||||
SET post.content = 'UNAVAILABLE'
|
||||
SET post.title = 'UNAVAILABLE'
|
||||
SET post.visibility = 'UNAVAILABLE'
|
||||
SET post.contentExcerpt = 'UNAVAILABLE'
|
||||
SET post.image = null
|
||||
RETURN post {.*};
|
||||
`)
|
||||
try {
|
||||
// Implement your migration here.
|
||||
const posts = await updateDeletedPostsAttributes.records.map(record => record.get('post'))
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(posts)
|
||||
await transaction.commit()
|
||||
next()
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error)
|
||||
await transaction.rollback()
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('rolled back')
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(next) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Irreversible migration')
|
||||
next()
|
||||
}
|
||||
@ -389,13 +389,15 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
{
|
||||
id: 'p0',
|
||||
language: sample(languages),
|
||||
image: faker.image.unsplash.food(300, 169),
|
||||
imageBlurred: true,
|
||||
imageAspectRatio: 300 / 169,
|
||||
},
|
||||
{
|
||||
categoryIds: ['cat16'],
|
||||
author: peterLustig,
|
||||
image: Factory.build('image', {
|
||||
url: faker.image.unsplash.food(300, 169),
|
||||
sensitive: true,
|
||||
aspectRatio: 300 / 169,
|
||||
}),
|
||||
},
|
||||
),
|
||||
Factory.build(
|
||||
@ -403,12 +405,14 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
{
|
||||
id: 'p1',
|
||||
language: sample(languages),
|
||||
image: faker.image.unsplash.technology(300, 1500),
|
||||
imageAspectRatio: 300 / 1500,
|
||||
},
|
||||
{
|
||||
categoryIds: ['cat1'],
|
||||
author: bobDerBaumeister,
|
||||
image: Factory.build('image', {
|
||||
url: faker.image.unsplash.technology(300, 1500),
|
||||
aspectRatio: 300 / 1500,
|
||||
}),
|
||||
},
|
||||
),
|
||||
Factory.build(
|
||||
@ -449,12 +453,14 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
{
|
||||
id: 'p6',
|
||||
language: sample(languages),
|
||||
image: faker.image.unsplash.buildings(300, 857),
|
||||
imageAspectRatio: 300 / 857,
|
||||
},
|
||||
{
|
||||
categoryIds: ['cat6'],
|
||||
author: peterLustig,
|
||||
image: Factory.build('image', {
|
||||
url: faker.image.unsplash.buildings(300, 857),
|
||||
aspectRatio: 300 / 857,
|
||||
}),
|
||||
},
|
||||
),
|
||||
Factory.build(
|
||||
@ -472,11 +478,13 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
'post',
|
||||
{
|
||||
id: 'p10',
|
||||
imageBlurred: true,
|
||||
},
|
||||
{
|
||||
categoryIds: ['cat10'],
|
||||
author: dewey,
|
||||
image: Factory.build('image', {
|
||||
sensitive: true,
|
||||
}),
|
||||
},
|
||||
),
|
||||
Factory.build(
|
||||
@ -484,12 +492,14 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
{
|
||||
id: 'p11',
|
||||
language: sample(languages),
|
||||
image: faker.image.unsplash.people(300, 901),
|
||||
imageAspectRatio: 300 / 901,
|
||||
},
|
||||
{
|
||||
categoryIds: ['cat11'],
|
||||
author: louie,
|
||||
image: Factory.build('image', {
|
||||
url: faker.image.unsplash.people(300, 901),
|
||||
aspectRatio: 300 / 901,
|
||||
}),
|
||||
},
|
||||
),
|
||||
Factory.build(
|
||||
@ -508,12 +518,14 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
{
|
||||
id: 'p14',
|
||||
language: sample(languages),
|
||||
image: faker.image.unsplash.objects(300, 200),
|
||||
imageAspectRatio: 300 / 450,
|
||||
},
|
||||
{
|
||||
categoryIds: ['cat14'],
|
||||
author: jennyRostock,
|
||||
image: Factory.build('image', {
|
||||
url: faker.image.unsplash.objects(300, 200),
|
||||
aspectRatio: 300 / 450,
|
||||
}),
|
||||
},
|
||||
),
|
||||
Factory.build(
|
||||
@ -539,22 +551,8 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
const hashtagAndMention1 =
|
||||
'The new physics of <a class="hashtag" data-hashtag-id="QuantenFlussTheorie" href="/?hashtag=QuantenFlussTheorie">#QuantenFlussTheorie</a> can explain <a class="hashtag" data-hashtag-id="QuantumGravity" href="/?hashtag=QuantumGravity">#QuantumGravity</a>! <a class="mention" data-mention-id="u1" href="/profile/u1">@peter-lustig</a> got that already. ;-)'
|
||||
const createPostMutation = gql`
|
||||
mutation(
|
||||
$id: ID
|
||||
$title: String!
|
||||
$content: String!
|
||||
$categoryIds: [ID]
|
||||
$imageBlurred: Boolean
|
||||
$imageAspectRatio: Float
|
||||
) {
|
||||
CreatePost(
|
||||
id: $id
|
||||
title: $title
|
||||
content: $content
|
||||
categoryIds: $categoryIds
|
||||
imageBlurred: $imageBlurred
|
||||
imageAspectRatio: $imageAspectRatio
|
||||
) {
|
||||
mutation($id: ID, $title: String!, $content: String!, $categoryIds: [ID]) {
|
||||
CreatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@ -568,7 +566,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
title: `Nature Philosophy Yoga`,
|
||||
content: hashtag1,
|
||||
categoryIds: ['cat2'],
|
||||
imageAspectRatio: 300 / 200,
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
@ -578,7 +575,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
title: 'This is post #7',
|
||||
content: `${mention1} ${faker.lorem.paragraph()}`,
|
||||
categoryIds: ['cat7'],
|
||||
imageAspectRatio: 300 / 180,
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
@ -589,7 +585,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
title: `Quantum Flow Theory explains Quantum Gravity`,
|
||||
content: hashtagAndMention1,
|
||||
categoryIds: ['cat8'],
|
||||
imageAspectRatio: 300 / 900,
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
@ -599,7 +594,6 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
title: 'This is post #12',
|
||||
content: `${mention2} ${faker.lorem.paragraph()}`,
|
||||
categoryIds: ['cat12'],
|
||||
imageAspectRatio: 300 / 200,
|
||||
},
|
||||
}),
|
||||
])
|
||||
@ -759,6 +753,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
},
|
||||
),
|
||||
])
|
||||
|
||||
const trollingComment = comments[0]
|
||||
|
||||
await Promise.all([
|
||||
@ -939,12 +934,13 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
[...Array(30).keys()].map(() =>
|
||||
Factory.build(
|
||||
'post',
|
||||
{
|
||||
image: faker.image.unsplash.objects(),
|
||||
},
|
||||
{},
|
||||
{
|
||||
categoryIds: ['cat1'],
|
||||
author: jennyRostock,
|
||||
image: Factory.build('image', {
|
||||
url: faker.image.unsplash.objects(),
|
||||
}),
|
||||
},
|
||||
),
|
||||
),
|
||||
@ -993,12 +989,13 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
[...Array(21).keys()].map(() =>
|
||||
Factory.build(
|
||||
'post',
|
||||
{
|
||||
image: faker.image.unsplash.buildings(),
|
||||
},
|
||||
{},
|
||||
{
|
||||
categoryIds: ['cat1'],
|
||||
author: peterLustig,
|
||||
image: Factory.build('image', {
|
||||
url: faker.image.unsplash.buildings(),
|
||||
}),
|
||||
},
|
||||
),
|
||||
),
|
||||
@ -1047,12 +1044,13 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
[...Array(11).keys()].map(() =>
|
||||
Factory.build(
|
||||
'post',
|
||||
{
|
||||
image: faker.image.unsplash.food(),
|
||||
},
|
||||
{},
|
||||
{
|
||||
categoryIds: ['cat1'],
|
||||
author: dewey,
|
||||
image: Factory.build('image', {
|
||||
url: faker.image.unsplash.food(),
|
||||
}),
|
||||
},
|
||||
),
|
||||
),
|
||||
@ -1101,12 +1099,13 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
[...Array(16).keys()].map(() =>
|
||||
Factory.build(
|
||||
'post',
|
||||
{
|
||||
image: faker.image.unsplash.technology(),
|
||||
},
|
||||
{},
|
||||
{
|
||||
categoryIds: ['cat1'],
|
||||
author: louie,
|
||||
image: Factory.build('image', {
|
||||
url: faker.image.unsplash.technology(),
|
||||
}),
|
||||
},
|
||||
),
|
||||
),
|
||||
@ -1155,12 +1154,13 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
[...Array(45).keys()].map(() =>
|
||||
Factory.build(
|
||||
'post',
|
||||
{
|
||||
image: faker.image.unsplash.people(),
|
||||
},
|
||||
{},
|
||||
{
|
||||
categoryIds: ['cat1'],
|
||||
author: bobDerBaumeister,
|
||||
image: Factory.build('image', {
|
||||
url: faker.image.unsplash.people(),
|
||||
}),
|
||||
},
|
||||
),
|
||||
),
|
||||
@ -1209,12 +1209,13 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
||||
[...Array(8).keys()].map(() =>
|
||||
Factory.build(
|
||||
'post',
|
||||
{
|
||||
image: faker.image.unsplash.nature(),
|
||||
},
|
||||
{},
|
||||
{
|
||||
categoryIds: ['cat1'],
|
||||
author: huey,
|
||||
image: Factory.build('image', {
|
||||
url: faker.image.unsplash.nature(),
|
||||
}),
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@ -15,10 +15,10 @@ export default async (driver, authorizationHeader) => {
|
||||
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const updateUserLastActiveTransactionResponse = await transaction.run(
|
||||
`
|
||||
`
|
||||
MATCH (user:User {id: $id, deleted: false, disabled: false })
|
||||
SET user.lastActiveAt = toString(datetime())
|
||||
RETURN user {.id, .slug, .name, .avatar, .email, .role, .disabled, .actorId}
|
||||
RETURN user {.id, .slug, .name, .role, .disabled, .actorId}
|
||||
LIMIT 1
|
||||
`,
|
||||
{ id },
|
||||
|
||||
@ -69,23 +69,23 @@ describe('decode', () => {
|
||||
{
|
||||
role: 'user',
|
||||
name: 'Jenny Rostock',
|
||||
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/sasha_shestakov/128.jpg',
|
||||
id: 'u3',
|
||||
slug: 'jenny-rostock',
|
||||
},
|
||||
{
|
||||
image: Factory.build('image', {
|
||||
url: 'https://s3.amazonaws.com/uifaces/faces/twitter/sasha_shestakov/128.jpg',
|
||||
}),
|
||||
email: 'user@example.org',
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
it('returns user object except email', async () => {
|
||||
it('returns user object without email', async () => {
|
||||
await expect(decode(driver, authorizationHeader)).resolves.toMatchObject({
|
||||
role: 'user',
|
||||
name: 'Jenny Rostock',
|
||||
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/sasha_shestakov/128.jpg',
|
||||
id: 'u3',
|
||||
email: null,
|
||||
slug: 'jenny-rostock',
|
||||
})
|
||||
})
|
||||
|
||||
@ -17,10 +17,10 @@ const obfuscate = async (resolve, root, args, context, info) => {
|
||||
root.contentExcerpt = 'UNAVAILABLE'
|
||||
root.title = 'UNAVAILABLE'
|
||||
root.slug = 'UNAVAILABLE'
|
||||
root.avatar = 'UNAVAILABLE'
|
||||
root.avatar = null
|
||||
root.about = 'UNAVAILABLE'
|
||||
root.name = 'UNAVAILABLE'
|
||||
root.image = null // avoid unecessary 500 errors
|
||||
root.image = null
|
||||
}
|
||||
return resolve(root, args, context, info)
|
||||
}
|
||||
|
||||
@ -28,14 +28,21 @@ beforeAll(async () => {
|
||||
password: '1234',
|
||||
},
|
||||
),
|
||||
Factory.build('user', {
|
||||
id: 'u2',
|
||||
role: 'user',
|
||||
name: 'Offensive Name',
|
||||
slug: 'offensive-name',
|
||||
avatar: '/some/offensive/avatar.jpg',
|
||||
about: 'This self description is very offensive',
|
||||
}),
|
||||
Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'u2',
|
||||
role: 'user',
|
||||
name: 'Offensive Name',
|
||||
slug: 'offensive-name',
|
||||
about: 'This self description is very offensive',
|
||||
},
|
||||
{
|
||||
avatar: Factory.build('image', {
|
||||
url: '/some/offensive/avatar.jpg',
|
||||
}),
|
||||
},
|
||||
),
|
||||
neode.create('Category', {
|
||||
id: 'cat9',
|
||||
name: 'Democracy & Politics',
|
||||
@ -96,10 +103,12 @@ beforeAll(async () => {
|
||||
title: 'Disabled post',
|
||||
content: 'This is an offensive post content',
|
||||
contentExcerpt: 'This is an offensive post content',
|
||||
image: '/some/offensive/image.jpg',
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
image: Factory.build('image', {
|
||||
url: '/some/offensive/image.jpg',
|
||||
}),
|
||||
author: troll,
|
||||
categoryIds,
|
||||
},
|
||||
@ -213,7 +222,9 @@ describe('softDeleteMiddleware', () => {
|
||||
name
|
||||
slug
|
||||
about
|
||||
avatar
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -229,7 +240,9 @@ describe('softDeleteMiddleware', () => {
|
||||
contributions {
|
||||
title
|
||||
slug
|
||||
image
|
||||
image {
|
||||
url
|
||||
}
|
||||
content
|
||||
contentExcerpt
|
||||
}
|
||||
@ -253,7 +266,10 @@ describe('softDeleteMiddleware', () => {
|
||||
it('displays slug', () => expect(subject.slug).toEqual('offensive-name'))
|
||||
it('displays about', () =>
|
||||
expect(subject.about).toEqual('This self description is very offensive'))
|
||||
it('displays avatar', () => expect(subject.avatar).toEqual('/some/offensive/avatar.jpg'))
|
||||
it('displays avatar', () =>
|
||||
expect(subject.avatar).toEqual({
|
||||
url: expect.stringContaining('/some/offensive/avatar.jpg'),
|
||||
}))
|
||||
})
|
||||
|
||||
describe('Post', () => {
|
||||
@ -265,7 +281,10 @@ describe('softDeleteMiddleware', () => {
|
||||
expect(subject.content).toEqual('This is an offensive post content'))
|
||||
it('displays contentExcerpt', () =>
|
||||
expect(subject.contentExcerpt).toEqual('This is an offensive post content'))
|
||||
it('displays image', () => expect(subject.image).toEqual('/some/offensive/image.jpg'))
|
||||
it('displays image', () =>
|
||||
expect(subject.image).toEqual({
|
||||
url: expect.stringContaining('/some/offensive/image.jpg'),
|
||||
}))
|
||||
})
|
||||
|
||||
describe('Comment', () => {
|
||||
@ -288,7 +307,7 @@ describe('softDeleteMiddleware', () => {
|
||||
it('obfuscates name', () => expect(subject.name).toEqual('UNAVAILABLE'))
|
||||
it('obfuscates slug', () => expect(subject.slug).toEqual('UNAVAILABLE'))
|
||||
it('obfuscates about', () => expect(subject.about).toEqual('UNAVAILABLE'))
|
||||
it('obfuscates avatar', () => expect(subject.avatar).toEqual('UNAVAILABLE'))
|
||||
it('obfuscates avatar', () => expect(subject.avatar).toEqual(null))
|
||||
})
|
||||
|
||||
describe('Post', () => {
|
||||
|
||||
7
backend/src/models/Image.js
Normal file
7
backend/src/models/Image.js
Normal file
@ -0,0 +1,7 @@
|
||||
export default {
|
||||
url: { primary: true, type: 'string', uri: { allowRelative: true } },
|
||||
alt: { type: 'string' },
|
||||
sensitive: { type: 'boolean', default: false },
|
||||
aspectRatio: { type: 'float', default: 1.0 },
|
||||
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||
}
|
||||
@ -4,6 +4,12 @@ export default {
|
||||
id: { type: 'string', primary: true, default: uuid },
|
||||
activityId: { type: 'string', allow: [null] },
|
||||
objectId: { type: 'string', allow: [null] },
|
||||
image: {
|
||||
type: 'relationship',
|
||||
relationship: 'HERO_IMAGE',
|
||||
target: 'Image',
|
||||
direction: 'out',
|
||||
},
|
||||
author: {
|
||||
type: 'relationship',
|
||||
relationship: 'WROTE',
|
||||
@ -14,7 +20,6 @@ export default {
|
||||
slug: { type: 'string', allow: [null], unique: 'true' },
|
||||
content: { type: 'string', disallow: [null], min: 3 },
|
||||
contentExcerpt: { type: 'string', allow: [null] },
|
||||
image: { type: 'string', allow: [null] },
|
||||
deleted: { type: 'boolean', default: false },
|
||||
disabled: { type: 'boolean', default: false },
|
||||
notified: {
|
||||
@ -39,8 +44,6 @@ export default {
|
||||
default: () => new Date().toISOString(),
|
||||
},
|
||||
language: { type: 'string', allow: [null] },
|
||||
imageBlurred: { type: 'boolean', default: false },
|
||||
imageAspectRatio: { type: 'float', default: 1.0 },
|
||||
comments: {
|
||||
type: 'relationship',
|
||||
relationship: 'COMMENTS',
|
||||
|
||||
@ -6,8 +6,12 @@ export default {
|
||||
name: { type: 'string', disallow: [null], min: 3 },
|
||||
slug: { type: 'string', unique: 'true', regex: /^[a-z0-9_-]+$/, lowercase: true },
|
||||
encryptedPassword: 'string',
|
||||
avatar: { type: 'string', allow: [null] },
|
||||
coverImg: { type: 'string', allow: [null] },
|
||||
avatar: {
|
||||
type: 'relationship',
|
||||
relationship: 'AVATAR_IMAGE',
|
||||
target: 'Image',
|
||||
direction: 'out',
|
||||
},
|
||||
deleted: { type: 'boolean', default: false },
|
||||
disabled: { type: 'boolean', default: false },
|
||||
role: { type: 'string', default: 'user' },
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
// NOTE: We cannot use `fs` here to clean up the code. Cypress breaks on any npm
|
||||
// module that is not browser-compatible. Node's `fs` module is server-side only
|
||||
export default {
|
||||
Image: require('./Image.js').default,
|
||||
Badge: require('./Badge.js').default,
|
||||
User: require('./User.js').default,
|
||||
EmailAddress: require('./EmailAddress.js').default,
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
import { createWriteStream } from 'fs'
|
||||
import path from 'path'
|
||||
import slug from 'slug'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
const localFileUpload = async ({ createReadStream, uniqueFilename }) => {
|
||||
await new Promise((resolve, reject) =>
|
||||
createReadStream()
|
||||
.pipe(createWriteStream(`public${uniqueFilename}`))
|
||||
.on('finish', resolve)
|
||||
.on('error', reject),
|
||||
)
|
||||
return uniqueFilename
|
||||
}
|
||||
|
||||
export default async function fileUpload(params, { file, url }, uploadCallback = localFileUpload) {
|
||||
const upload = params[file]
|
||||
if (upload) {
|
||||
const { createReadStream, filename } = await upload
|
||||
const { name, ext } = path.parse(filename)
|
||||
const uniqueFilename = `/uploads/${uuid()}-${slug(name)}${ext}`
|
||||
const location = await uploadCallback({ createReadStream, uniqueFilename })
|
||||
delete params[file]
|
||||
params[url] = location
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
@ -1,59 +0,0 @@
|
||||
import fileUpload from '.'
|
||||
|
||||
const uuid = '[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}'
|
||||
|
||||
describe('fileUpload', () => {
|
||||
let params
|
||||
let uploadCallback
|
||||
|
||||
beforeEach(() => {
|
||||
params = {
|
||||
uploadAttribute: {
|
||||
filename: 'avatar.jpg',
|
||||
mimetype: 'image/jpeg',
|
||||
encoding: '7bit',
|
||||
createReadStream: jest.fn(),
|
||||
},
|
||||
}
|
||||
uploadCallback = jest.fn(({ uniqueFilename }) => uniqueFilename)
|
||||
})
|
||||
|
||||
it('calls uploadCallback', async () => {
|
||||
await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback)
|
||||
expect(uploadCallback).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
describe('file name', () => {
|
||||
it('saves the upload url in params[url]', async () => {
|
||||
await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback)
|
||||
expect(params.attribute).toMatch(new RegExp(`^/uploads/${uuid}-avatar.jpg`))
|
||||
})
|
||||
|
||||
it('creates a url safe name', async () => {
|
||||
params.uploadAttribute.filename = '/path/to/awkward?/ file-location/?foo- bar-avatar.jpg'
|
||||
await fileUpload(params, { file: 'uploadAttribute', url: 'attribute' }, uploadCallback)
|
||||
expect(params.attribute).toMatch(new RegExp(`/uploads/${uuid}-foo-bar-avatar.jpg$`))
|
||||
})
|
||||
|
||||
describe('in case of duplicates', () => {
|
||||
it('creates unique names to avoid overwriting existing files', async () => {
|
||||
const { attribute: first } = await fileUpload(
|
||||
{
|
||||
...params,
|
||||
},
|
||||
{ file: 'uploadAttribute', url: 'attribute' },
|
||||
uploadCallback,
|
||||
)
|
||||
|
||||
const { attribute: second } = await fileUpload(
|
||||
{
|
||||
...params,
|
||||
},
|
||||
{ file: 'uploadAttribute', url: 'attribute' },
|
||||
uploadCallback,
|
||||
)
|
||||
expect(first).not.toEqual(second)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
8
backend/src/schema/resolvers/images.js
Normal file
8
backend/src/schema/resolvers/images.js
Normal file
@ -0,0 +1,8 @@
|
||||
import Resolver from './helpers/Resolver'
|
||||
export default {
|
||||
Image: {
|
||||
...Resolver('Image', {
|
||||
undefinedToNull: ['sensitive', 'alt', 'aspectRatio'],
|
||||
}),
|
||||
},
|
||||
}
|
||||
121
backend/src/schema/resolvers/images/images.js
Normal file
121
backend/src/schema/resolvers/images/images.js
Normal file
@ -0,0 +1,121 @@
|
||||
import path from 'path'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import slug from 'slug'
|
||||
import { existsSync, unlinkSync, createWriteStream } from 'fs'
|
||||
import { getDriver } from '../../../db/neo4j'
|
||||
import { UserInputError } from 'apollo-server'
|
||||
|
||||
// const widths = [34, 160, 320, 640, 1024]
|
||||
|
||||
export async function deleteImage(resource, relationshipType, opts = {}) {
|
||||
sanitizeRelationshipType(relationshipType)
|
||||
const { transaction, deleteCallback } = opts
|
||||
if (!transaction) return wrapTransaction(deleteImage, [resource, relationshipType], opts)
|
||||
const txResult = await transaction.run(
|
||||
`
|
||||
MATCH (resource {id: $resource.id})-[rel:${relationshipType}]->(image:Image)
|
||||
WITH image, image {.*} as imageProps
|
||||
DETACH DELETE image
|
||||
RETURN imageProps
|
||||
`,
|
||||
{ resource },
|
||||
)
|
||||
const [image] = txResult.records.map(record => record.get('imageProps'))
|
||||
// This behaviour differs from `mergeImage`. If you call `mergeImage`
|
||||
// with metadata for an image that does not exist, it's an indicator
|
||||
// of an error (so throw an error). If we bulk delete an image, it
|
||||
// could very well be that there is no image for the resource.
|
||||
if (image) deleteImageFile(image, deleteCallback)
|
||||
return image
|
||||
}
|
||||
|
||||
export async function mergeImage(resource, relationshipType, imageInput, opts = {}) {
|
||||
if (typeof imageInput === 'undefined') return
|
||||
if (imageInput === null) return deleteImage(resource, relationshipType, opts)
|
||||
sanitizeRelationshipType(relationshipType)
|
||||
const { transaction, uploadCallback, deleteCallback } = opts
|
||||
if (!transaction)
|
||||
return wrapTransaction(mergeImage, [resource, relationshipType, imageInput], opts)
|
||||
|
||||
let txResult
|
||||
txResult = await transaction.run(
|
||||
`
|
||||
MATCH (resource {id: $resource.id})-[:${relationshipType}]->(image:Image)
|
||||
RETURN image {.*}
|
||||
`,
|
||||
{ resource },
|
||||
)
|
||||
const [existingImage] = txResult.records.map(record => record.get('image'))
|
||||
const { upload } = imageInput
|
||||
if (!(existingImage || upload)) throw new UserInputError('Cannot find image for given resource')
|
||||
if (existingImage && upload) deleteImageFile(existingImage, deleteCallback)
|
||||
const url = await uploadImageFile(upload, uploadCallback)
|
||||
const { alt, sensitive, aspectRatio } = imageInput
|
||||
const image = { alt, sensitive, aspectRatio, url }
|
||||
txResult = await transaction.run(
|
||||
`
|
||||
MATCH (resource {id: $resource.id})
|
||||
MERGE (resource)-[:${relationshipType}]->(image:Image)
|
||||
ON CREATE SET image.createdAt = toString(datetime())
|
||||
ON MATCH SET image.updatedAt = toString(datetime())
|
||||
SET image += $image
|
||||
RETURN image {.*}
|
||||
`,
|
||||
{ resource, image },
|
||||
)
|
||||
const [mergedImage] = txResult.records.map(record => record.get('image'))
|
||||
return mergedImage
|
||||
}
|
||||
|
||||
const wrapTransaction = async (wrappedCallback, args, opts) => {
|
||||
const session = getDriver().session()
|
||||
try {
|
||||
const result = await session.writeTransaction(async transaction => {
|
||||
return wrappedCallback(...args, { ...opts, transaction })
|
||||
})
|
||||
return result
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
const deleteImageFile = (image, deleteCallback = localFileDelete) => {
|
||||
const { url } = image
|
||||
deleteCallback(url)
|
||||
return url
|
||||
}
|
||||
|
||||
const uploadImageFile = async (upload, uploadCallback = localFileUpload) => {
|
||||
if (!upload) return undefined
|
||||
const { createReadStream, filename, mimetype } = await upload
|
||||
const { name, ext } = path.parse(filename)
|
||||
const uniqueFilename = `${uuid()}-${slug(name)}${ext}`
|
||||
|
||||
return uploadCallback({
|
||||
createReadStream,
|
||||
destination: `/uploads/${uniqueFilename}`,
|
||||
mimetype,
|
||||
})
|
||||
}
|
||||
|
||||
const sanitizeRelationshipType = relationshipType => {
|
||||
// Cypher query language does not allow to parameterize relationship types
|
||||
// See: https://github.com/neo4j/neo4j/issues/340
|
||||
if (!['HERO_IMAGE', 'AVATAR_IMAGE'].includes(relationshipType)) {
|
||||
throw new Error(`Unknown relationship type ${relationshipType}`)
|
||||
}
|
||||
}
|
||||
|
||||
const localFileUpload = ({ createReadStream, destination }) => {
|
||||
return new Promise((resolve, reject) =>
|
||||
createReadStream()
|
||||
.pipe(createWriteStream(`public${destination}`))
|
||||
.on('finish', () => resolve(destination))
|
||||
.on('error', reject),
|
||||
)
|
||||
}
|
||||
|
||||
const localFileDelete = async url => {
|
||||
const location = `public${url}`
|
||||
if (existsSync(location)) unlinkSync(location)
|
||||
}
|
||||
344
backend/src/schema/resolvers/images/images.spec.js
Normal file
344
backend/src/schema/resolvers/images/images.spec.js
Normal file
@ -0,0 +1,344 @@
|
||||
import { deleteImage, mergeImage } from './images'
|
||||
import { getNeode, getDriver } from '../../../db/neo4j'
|
||||
import Factory, { cleanDatabase } from '../../../db/factories'
|
||||
import { UserInputError } from 'apollo-server'
|
||||
|
||||
const driver = getDriver()
|
||||
const neode = getNeode()
|
||||
const uuid = '[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}'
|
||||
let uploadCallback
|
||||
let deleteCallback
|
||||
|
||||
beforeEach(async () => {
|
||||
await cleanDatabase()
|
||||
uploadCallback = jest.fn(({ destination }) => destination)
|
||||
deleteCallback = jest.fn()
|
||||
})
|
||||
|
||||
describe('deleteImage', () => {
|
||||
describe('given a resource with an image', () => {
|
||||
let user
|
||||
beforeEach(async () => {
|
||||
user = await Factory.build(
|
||||
'user',
|
||||
{},
|
||||
{
|
||||
avatar: Factory.build('image', {
|
||||
url: '/some/avatar/url/',
|
||||
alt: 'This is the avatar image of a user',
|
||||
}),
|
||||
},
|
||||
)
|
||||
user = await user.toJson()
|
||||
})
|
||||
|
||||
it('soft deletes `Image` node', async () => {
|
||||
await expect(neode.all('Image')).resolves.toHaveLength(1)
|
||||
await deleteImage(user, 'AVATAR_IMAGE', { deleteCallback })
|
||||
await expect(neode.all('Image')).resolves.toHaveLength(0)
|
||||
})
|
||||
|
||||
it('calls deleteCallback', async () => {
|
||||
user = await Factory.build('user')
|
||||
user = await user.toJson()
|
||||
await deleteImage(user, 'AVATAR_IMAGE', { deleteCallback })
|
||||
expect(deleteCallback).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
describe('given a transaction parameter', () => {
|
||||
it('executes cypher statements within the transaction', async () => {
|
||||
const session = driver.session()
|
||||
let someString
|
||||
try {
|
||||
someString = await session.writeTransaction(async transaction => {
|
||||
await deleteImage(user, 'AVATAR_IMAGE', {
|
||||
deleteCallback,
|
||||
transaction,
|
||||
})
|
||||
const txResult = await transaction.run('RETURN "Hello" as result')
|
||||
const [result] = txResult.records.map(record => record.get('result'))
|
||||
return result
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
await expect(neode.all('Image')).resolves.toHaveLength(0)
|
||||
await expect(someString).toEqual('Hello')
|
||||
})
|
||||
|
||||
it('rolls back the transaction in case of errors', async done => {
|
||||
await expect(neode.all('Image')).resolves.toHaveLength(1)
|
||||
const session = driver.session()
|
||||
try {
|
||||
await session.writeTransaction(async transaction => {
|
||||
await deleteImage(user, 'AVATAR_IMAGE', {
|
||||
deleteCallback,
|
||||
transaction,
|
||||
})
|
||||
throw new Error('Ouch!')
|
||||
})
|
||||
} catch (err) {
|
||||
// nothing has been deleted
|
||||
await expect(neode.all('Image')).resolves.toHaveLength(1)
|
||||
// all good
|
||||
done()
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('mergeImage', () => {
|
||||
let imageInput
|
||||
let post
|
||||
beforeEach(() => {
|
||||
imageInput = {
|
||||
alt: 'A description of the new image',
|
||||
}
|
||||
})
|
||||
|
||||
describe('on existing resource', () => {
|
||||
beforeEach(async () => {
|
||||
post = await Factory.build(
|
||||
'post',
|
||||
{ id: 'p99' },
|
||||
{
|
||||
author: Factory.build('user', {}, { avatar: null }),
|
||||
image: null,
|
||||
},
|
||||
)
|
||||
post = await post.toJson()
|
||||
})
|
||||
|
||||
describe('given image.upload', () => {
|
||||
beforeEach(() => {
|
||||
imageInput = {
|
||||
...imageInput,
|
||||
upload: {
|
||||
filename: 'image.jpg',
|
||||
mimetype: 'image/jpeg',
|
||||
encoding: '7bit',
|
||||
createReadStream: () => ({
|
||||
pipe: () => ({
|
||||
on: (_, callback) => callback(),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
it('returns new image', async () => {
|
||||
await expect(
|
||||
mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }),
|
||||
).resolves.toMatchObject({
|
||||
url: expect.any(String),
|
||||
alt: 'A description of the new image',
|
||||
})
|
||||
})
|
||||
|
||||
it('calls upload callback', async () => {
|
||||
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
|
||||
expect(uploadCallback).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('creates `:Image` node', async () => {
|
||||
await expect(neode.all('Image')).resolves.toHaveLength(0)
|
||||
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
|
||||
await expect(neode.all('Image')).resolves.toHaveLength(1)
|
||||
})
|
||||
|
||||
it('creates a url safe name', async () => {
|
||||
imageInput.upload.filename = '/path/to/awkward?/ file-location/?foo- bar-avatar.jpg'
|
||||
await expect(
|
||||
mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }),
|
||||
).resolves.toMatchObject({
|
||||
url: expect.stringMatching(new RegExp(`^/uploads/${uuid}-foo-bar-avatar.jpg`)),
|
||||
})
|
||||
})
|
||||
|
||||
it.skip('automatically creates different image sizes', async () => {
|
||||
await expect(
|
||||
mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }),
|
||||
).resolves.toEqual({
|
||||
url: expect.any(String),
|
||||
alt: expect.any(String),
|
||||
urlW34: expect.stringMatching(new RegExp(`^/uploads/W34/${uuid}-image.jpg`)),
|
||||
urlW160: expect.stringMatching(new RegExp(`^/uploads/W160/${uuid}-image.jpg`)),
|
||||
urlW320: expect.stringMatching(new RegExp(`^/uploads/W320/${uuid}-image.jpg`)),
|
||||
urlW640: expect.stringMatching(new RegExp(`^/uploads/W640/${uuid}-image.jpg`)),
|
||||
urlW1024: expect.stringMatching(new RegExp(`^/uploads/W1024/${uuid}-image.jpg`)),
|
||||
})
|
||||
})
|
||||
|
||||
it('connects resource with image via given image type', async () => {
|
||||
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
|
||||
const result = await neode.cypher(`
|
||||
MATCH(p:Post {id: "p99"})-[:HERO_IMAGE]->(i:Image) RETURN i,p
|
||||
`)
|
||||
post = neode.hydrateFirst(result, 'p', neode.model('Post'))
|
||||
const image = neode.hydrateFirst(result, 'i', neode.model('Image'))
|
||||
expect(post).toBeTruthy()
|
||||
expect(image).toBeTruthy()
|
||||
})
|
||||
|
||||
it('whitelists relationship types', async () => {
|
||||
await expect(
|
||||
mergeImage(post, 'WHATEVER', imageInput, { uploadCallback, deleteCallback }),
|
||||
).rejects.toEqual(new Error('Unknown relationship type WHATEVER'))
|
||||
})
|
||||
|
||||
it('sets metadata', async () => {
|
||||
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
|
||||
const image = await neode.first('Image', {})
|
||||
await expect(image.toJson()).resolves.toMatchObject({
|
||||
alt: 'A description of the new image',
|
||||
createdAt: expect.any(String),
|
||||
url: expect.any(String),
|
||||
})
|
||||
})
|
||||
|
||||
describe('given a transaction parameter', () => {
|
||||
it('executes cypher statements within the transaction', async () => {
|
||||
const session = driver.session()
|
||||
try {
|
||||
await session.writeTransaction(async transaction => {
|
||||
const image = await mergeImage(post, 'HERO_IMAGE', imageInput, {
|
||||
uploadCallback,
|
||||
deleteCallback,
|
||||
transaction,
|
||||
})
|
||||
return transaction.run(
|
||||
`
|
||||
MATCH(image:Image {url: $image.url})
|
||||
SET image.alt = 'This alt text gets overwritten'
|
||||
RETURN image {.*}
|
||||
`,
|
||||
{ image },
|
||||
)
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
const image = await neode.first('Image', { alt: 'This alt text gets overwritten' })
|
||||
await expect(image.toJson()).resolves.toMatchObject({
|
||||
alt: 'This alt text gets overwritten',
|
||||
})
|
||||
})
|
||||
|
||||
it('rolls back the transaction in case of errors', async done => {
|
||||
const session = driver.session()
|
||||
try {
|
||||
await session.writeTransaction(async transaction => {
|
||||
const image = await mergeImage(post, 'HERO_IMAGE', imageInput, {
|
||||
uploadCallback,
|
||||
deleteCallback,
|
||||
transaction,
|
||||
})
|
||||
return transaction.run('Ooops invalid cypher!', { image })
|
||||
})
|
||||
} catch (err) {
|
||||
// nothing has been created
|
||||
await expect(neode.all('Image')).resolves.toHaveLength(0)
|
||||
// all good
|
||||
done()
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('if resource has an image already', () => {
|
||||
beforeEach(async () => {
|
||||
const [post, image] = await Promise.all([
|
||||
neode.find('Post', 'p99'),
|
||||
Factory.build('image'),
|
||||
])
|
||||
await post.relateTo(image, 'image')
|
||||
})
|
||||
|
||||
it('calls deleteCallback', async () => {
|
||||
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
|
||||
expect(deleteCallback).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('calls uploadCallback', async () => {
|
||||
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
|
||||
expect(uploadCallback).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('updates metadata of existing image node', async () => {
|
||||
await expect(neode.all('Image')).resolves.toHaveLength(1)
|
||||
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
|
||||
await expect(neode.all('Image')).resolves.toHaveLength(1)
|
||||
const image = await neode.first('Image', {})
|
||||
await expect(image.toJson()).resolves.toMatchObject({
|
||||
alt: 'A description of the new image',
|
||||
createdAt: expect.any(String),
|
||||
url: expect.any(String),
|
||||
// TODO
|
||||
// width:
|
||||
// height:
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('without image.upload', () => {
|
||||
it('throws UserInputError', async () => {
|
||||
post = await Factory.build('post', { id: 'p99' }, { image: null })
|
||||
post = await post.toJson()
|
||||
await expect(mergeImage(post, 'HERO_IMAGE', imageInput)).rejects.toEqual(
|
||||
new UserInputError('Cannot find image for given resource'),
|
||||
)
|
||||
})
|
||||
|
||||
describe('if resource has an image already', () => {
|
||||
beforeEach(async () => {
|
||||
post = await Factory.build(
|
||||
'post',
|
||||
{
|
||||
id: 'p99',
|
||||
},
|
||||
{
|
||||
author: Factory.build(
|
||||
'user',
|
||||
{},
|
||||
{
|
||||
avatar: null,
|
||||
},
|
||||
),
|
||||
image: Factory.build('image', {
|
||||
alt: 'This is the previous, not updated image',
|
||||
url: '/some/original/url',
|
||||
}),
|
||||
},
|
||||
)
|
||||
post = await post.toJson()
|
||||
})
|
||||
|
||||
it('does not call deleteCallback', async () => {
|
||||
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
|
||||
expect(deleteCallback).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not call uploadCallback', async () => {
|
||||
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
|
||||
expect(uploadCallback).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('updates metadata', async () => {
|
||||
await mergeImage(post, 'HERO_IMAGE', imageInput)
|
||||
const images = await neode.all('Image')
|
||||
expect(images).toHaveLength(1)
|
||||
await expect(images.first().toJson()).resolves.toMatchObject({
|
||||
createdAt: expect.any(String),
|
||||
url: expect.any(String),
|
||||
alt: 'A description of the new image',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -2,7 +2,7 @@ import { v4 as uuid } from 'uuid'
|
||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { UserInputError } from 'apollo-server'
|
||||
import fileUpload from './fileUpload'
|
||||
import { mergeImage, deleteImage } from './images/images'
|
||||
import Resolver from './helpers/Resolver'
|
||||
import { filterForMutedUsers } from './helpers/filterForMutedUsers'
|
||||
|
||||
@ -77,14 +77,16 @@ export default {
|
||||
Mutation: {
|
||||
CreatePost: async (_parent, params, context, _resolveInfo) => {
|
||||
const { categoryIds } = params
|
||||
const { image: imageInput } = params
|
||||
delete params.categoryIds
|
||||
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
|
||||
delete params.image
|
||||
params.id = params.id || uuid()
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const createPostTransactionResponse = await transaction.run(
|
||||
`
|
||||
CREATE (post:Post {params})
|
||||
CREATE (post:Post)
|
||||
SET post += $params
|
||||
SET post.createdAt = toString(datetime())
|
||||
SET post.updatedAt = toString(datetime())
|
||||
WITH post
|
||||
@ -94,14 +96,18 @@ export default {
|
||||
UNWIND $categoryIds AS categoryId
|
||||
MATCH (category:Category {id: categoryId})
|
||||
MERGE (post)-[:CATEGORIZED]->(category)
|
||||
RETURN post
|
||||
RETURN post {.*}
|
||||
`,
|
||||
{ userId: context.user.id, categoryIds, params },
|
||||
)
|
||||
return createPostTransactionResponse.records.map(record => record.get('post').properties)
|
||||
const [post] = createPostTransactionResponse.records.map(record => record.get('post'))
|
||||
if (imageInput) {
|
||||
await mergeImage(post, 'HERO_IMAGE', imageInput, { transaction })
|
||||
}
|
||||
return post
|
||||
})
|
||||
try {
|
||||
const [post] = await writeTxResultPromise
|
||||
const post = await writeTxResultPromise
|
||||
return post
|
||||
} catch (e) {
|
||||
if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
|
||||
@ -113,8 +119,9 @@ export default {
|
||||
},
|
||||
UpdatePost: async (_parent, params, context, _resolveInfo) => {
|
||||
const { categoryIds } = params
|
||||
const { image: imageInput } = params
|
||||
delete params.categoryIds
|
||||
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
|
||||
delete params.image
|
||||
const session = context.driver.session()
|
||||
let updatePostCypher = `
|
||||
MATCH (post:Post {id: $params.id})
|
||||
@ -142,7 +149,7 @@ export default {
|
||||
`
|
||||
}
|
||||
|
||||
updatePostCypher += `RETURN post`
|
||||
updatePostCypher += `RETURN post {.*}`
|
||||
const updatePostVariables = { categoryIds, params }
|
||||
try {
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
@ -150,9 +157,11 @@ export default {
|
||||
updatePostCypher,
|
||||
updatePostVariables,
|
||||
)
|
||||
return updatePostTransactionResponse.records.map(record => record.get('post').properties)
|
||||
const [post] = updatePostTransactionResponse.records.map(record => record.get('post'))
|
||||
await mergeImage(post, 'HERO_IMAGE', imageInput, { transaction })
|
||||
return post
|
||||
})
|
||||
const [post] = await writeTxResultPromise
|
||||
const post = await writeTxResultPromise
|
||||
return post
|
||||
} finally {
|
||||
session.close()
|
||||
@ -171,15 +180,16 @@ export default {
|
||||
SET post.contentExcerpt = 'UNAVAILABLE'
|
||||
SET post.title = 'UNAVAILABLE'
|
||||
SET comment.deleted = TRUE
|
||||
REMOVE post.image
|
||||
RETURN post
|
||||
RETURN post {.*}
|
||||
`,
|
||||
{ postId: args.id },
|
||||
)
|
||||
return deletePostTransactionResponse.records.map(record => record.get('post').properties)
|
||||
const [post] = deletePostTransactionResponse.records.map(record => record.get('post'))
|
||||
await deleteImage(post, 'HERO_IMAGE', { transaction })
|
||||
return post
|
||||
})
|
||||
try {
|
||||
const [post] = await writeTxResultPromise
|
||||
const post = await writeTxResultPromise
|
||||
return post
|
||||
} finally {
|
||||
session.close()
|
||||
@ -311,16 +321,7 @@ export default {
|
||||
},
|
||||
Post: {
|
||||
...Resolver('Post', {
|
||||
undefinedToNull: [
|
||||
'activityId',
|
||||
'objectId',
|
||||
'image',
|
||||
'language',
|
||||
'pinnedAt',
|
||||
'pinned',
|
||||
'imageBlurred',
|
||||
'imageAspectRatio',
|
||||
],
|
||||
undefinedToNull: ['activityId', 'objectId', 'language', 'pinnedAt', 'pinned'],
|
||||
hasMany: {
|
||||
tags: '-[:TAGGED]->(related:Tag)',
|
||||
categories: '-[:CATEGORIZED]->(related:Category)',
|
||||
@ -331,6 +332,7 @@ export default {
|
||||
hasOne: {
|
||||
author: '<-[:WROTE]-(related:User)',
|
||||
pinnedBy: '<-[:PINNED]-(related:User)',
|
||||
image: '-[:HERO_IMAGE]->(related:Image)',
|
||||
},
|
||||
count: {
|
||||
commentsCount:
|
||||
|
||||
@ -336,8 +336,14 @@ describe('CreatePost', () => {
|
||||
describe('UpdatePost', () => {
|
||||
let author, newlyCreatedPost
|
||||
const updatePostMutation = gql`
|
||||
mutation($id: ID!, $title: String!, $content: String!, $categoryIds: [ID]) {
|
||||
UpdatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
|
||||
mutation($id: ID!, $title: String!, $content: String!, $categoryIds: [ID], $image: ImageInput) {
|
||||
UpdatePost(
|
||||
id: $id
|
||||
title: $title
|
||||
content: $content
|
||||
categoryIds: $categoryIds
|
||||
image: $image
|
||||
) {
|
||||
id
|
||||
title
|
||||
content
|
||||
@ -472,418 +478,471 @@ describe('UpdatePost', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('params.image', () => {
|
||||
describe('is object', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, image: { sensitive: true } }
|
||||
})
|
||||
it('updates the image', async () => {
|
||||
await expect(neode.first('Image', { sensitive: true })).resolves.toBeFalsy()
|
||||
await mutate({ mutation: updatePostMutation, variables })
|
||||
await expect(neode.first('Image', { sensitive: true })).resolves.toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('is null', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, image: null }
|
||||
})
|
||||
it('deletes the image', async () => {
|
||||
await expect(neode.all('Image')).resolves.toHaveLength(6)
|
||||
await mutate({ mutation: updatePostMutation, variables })
|
||||
await expect(neode.all('Image')).resolves.toHaveLength(5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('is undefined', () => {
|
||||
beforeEach(() => {
|
||||
delete variables.image
|
||||
})
|
||||
it('keeps the image unchanged', async () => {
|
||||
await expect(neode.first('Image', { sensitive: true })).resolves.toBeFalsy()
|
||||
await mutate({ mutation: updatePostMutation, variables })
|
||||
await expect(neode.first('Image', { sensitive: true })).resolves.toBeFalsy()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('pin posts', () => {
|
||||
let author
|
||||
const pinPostMutation = gql`
|
||||
mutation($id: ID!) {
|
||||
pinPost(id: $id) {
|
||||
id
|
||||
title
|
||||
content
|
||||
author {
|
||||
name
|
||||
slug
|
||||
}
|
||||
pinnedBy {
|
||||
id
|
||||
name
|
||||
role
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
pinnedAt
|
||||
pinned
|
||||
}
|
||||
}
|
||||
`
|
||||
beforeEach(async () => {
|
||||
author = await Factory.build('user', { slug: 'the-author' })
|
||||
await Factory.build(
|
||||
'post',
|
||||
{
|
||||
id: 'p9876',
|
||||
title: 'Old title',
|
||||
content: 'Old content',
|
||||
},
|
||||
{
|
||||
author,
|
||||
categoryIds,
|
||||
},
|
||||
)
|
||||
variables = {
|
||||
id: 'p9876',
|
||||
}
|
||||
})
|
||||
|
||||
describe('pin posts', () => {
|
||||
const pinPostMutation = gql`
|
||||
mutation($id: ID!) {
|
||||
pinPost(id: $id) {
|
||||
id
|
||||
title
|
||||
content
|
||||
author {
|
||||
name
|
||||
slug
|
||||
}
|
||||
pinnedBy {
|
||||
id
|
||||
name
|
||||
role
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
pinnedAt
|
||||
pinned
|
||||
}
|
||||
}
|
||||
`
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
authenticatedUser = null
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
data: { pinPost: null },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('ordinary users', () => {
|
||||
it('throws authorization error', async () => {
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
data: { pinPost: null },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('moderators', () => {
|
||||
let moderator
|
||||
beforeEach(async () => {
|
||||
variables = { ...variables }
|
||||
moderator = await user.update({ role: 'moderator', updatedAt: new Date().toISOString() })
|
||||
authenticatedUser = await moderator.toJson()
|
||||
})
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
authenticatedUser = null
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
data: { pinPost: null },
|
||||
})
|
||||
it('throws authorization error', async () => {
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
data: { pinPost: null },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('ordinary users', () => {
|
||||
it('throws authorization error', async () => {
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
data: { pinPost: null },
|
||||
})
|
||||
describe('admins', () => {
|
||||
let admin
|
||||
beforeEach(async () => {
|
||||
admin = await user.update({
|
||||
role: 'admin',
|
||||
name: 'Admin',
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
authenticatedUser = await admin.toJson()
|
||||
})
|
||||
|
||||
describe('moderators', () => {
|
||||
let moderator
|
||||
describe('are allowed to pin posts', () => {
|
||||
beforeEach(async () => {
|
||||
moderator = await user.update({ role: 'moderator', updatedAt: new Date().toISOString() })
|
||||
authenticatedUser = await moderator.toJson()
|
||||
await Factory.build(
|
||||
'post',
|
||||
{
|
||||
id: 'created-and-pinned-by-same-admin',
|
||||
},
|
||||
{
|
||||
author: admin,
|
||||
},
|
||||
)
|
||||
variables = { ...variables, id: 'created-and-pinned-by-same-admin' }
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
data: { pinPost: null },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('admins', () => {
|
||||
let admin
|
||||
beforeEach(async () => {
|
||||
admin = await user.update({
|
||||
role: 'admin',
|
||||
name: 'Admin',
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
authenticatedUser = await admin.toJson()
|
||||
})
|
||||
|
||||
describe('are allowed to pin posts', () => {
|
||||
beforeEach(async () => {
|
||||
await Factory.build(
|
||||
'post',
|
||||
{
|
||||
it('responds with the updated Post', async () => {
|
||||
const expected = {
|
||||
data: {
|
||||
pinPost: {
|
||||
id: 'created-and-pinned-by-same-admin',
|
||||
},
|
||||
{
|
||||
author: admin,
|
||||
},
|
||||
)
|
||||
variables = { ...variables, id: 'created-and-pinned-by-same-admin' }
|
||||
})
|
||||
|
||||
it('responds with the updated Post', async () => {
|
||||
const expected = {
|
||||
data: {
|
||||
pinPost: {
|
||||
id: 'created-and-pinned-by-same-admin',
|
||||
author: {
|
||||
name: 'Admin',
|
||||
},
|
||||
pinnedBy: {
|
||||
id: 'current-user',
|
||||
name: 'Admin',
|
||||
role: 'admin',
|
||||
},
|
||||
author: {
|
||||
name: 'Admin',
|
||||
},
|
||||
pinnedBy: {
|
||||
id: 'current-user',
|
||||
name: 'Admin',
|
||||
role: 'admin',
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
|
||||
it('sets createdAt date for PINNED', async () => {
|
||||
const expected = {
|
||||
data: {
|
||||
pinPost: {
|
||||
id: 'created-and-pinned-by-same-admin',
|
||||
pinnedAt: expect.any(String),
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
|
||||
it('sets redundant `pinned` property for performant ordering', async () => {
|
||||
variables = { ...variables, id: 'created-and-pinned-by-same-admin' }
|
||||
const expected = {
|
||||
data: { pinPost: { pinned: true } },
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
|
||||
describe('post created by another admin', () => {
|
||||
let otherAdmin
|
||||
beforeEach(async () => {
|
||||
otherAdmin = await Factory.build('user', {
|
||||
role: 'admin',
|
||||
name: 'otherAdmin',
|
||||
})
|
||||
authenticatedUser = await otherAdmin.toJson()
|
||||
await Factory.build(
|
||||
'post',
|
||||
{
|
||||
it('sets createdAt date for PINNED', async () => {
|
||||
const expected = {
|
||||
data: {
|
||||
pinPost: {
|
||||
id: 'created-and-pinned-by-same-admin',
|
||||
pinnedAt: expect.any(String),
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
|
||||
it('sets redundant `pinned` property for performant ordering', async () => {
|
||||
variables = { ...variables, id: 'created-and-pinned-by-same-admin' }
|
||||
const expected = {
|
||||
data: { pinPost: { pinned: true } },
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('post created by another admin', () => {
|
||||
let otherAdmin
|
||||
beforeEach(async () => {
|
||||
otherAdmin = await Factory.build('user', {
|
||||
role: 'admin',
|
||||
name: 'otherAdmin',
|
||||
})
|
||||
authenticatedUser = await otherAdmin.toJson()
|
||||
await Factory.build(
|
||||
'post',
|
||||
{
|
||||
id: 'created-by-one-admin-pinned-by-different-one',
|
||||
},
|
||||
{
|
||||
author: otherAdmin,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
it('responds with the updated Post', async () => {
|
||||
authenticatedUser = await admin.toJson()
|
||||
variables = { ...variables, id: 'created-by-one-admin-pinned-by-different-one' }
|
||||
const expected = {
|
||||
data: {
|
||||
pinPost: {
|
||||
id: 'created-by-one-admin-pinned-by-different-one',
|
||||
},
|
||||
{
|
||||
author: otherAdmin,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
it('responds with the updated Post', async () => {
|
||||
authenticatedUser = await admin.toJson()
|
||||
variables = { ...variables, id: 'created-by-one-admin-pinned-by-different-one' }
|
||||
const expected = {
|
||||
data: {
|
||||
pinPost: {
|
||||
id: 'created-by-one-admin-pinned-by-different-one',
|
||||
author: {
|
||||
name: 'otherAdmin',
|
||||
},
|
||||
pinnedBy: {
|
||||
id: 'current-user',
|
||||
name: 'Admin',
|
||||
role: 'admin',
|
||||
},
|
||||
author: {
|
||||
name: 'otherAdmin',
|
||||
},
|
||||
pinnedBy: {
|
||||
id: 'current-user',
|
||||
name: 'Admin',
|
||||
role: 'admin',
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('post created by another user', () => {
|
||||
it('responds with the updated Post', async () => {
|
||||
const expected = {
|
||||
data: {
|
||||
pinPost: {
|
||||
id: 'p9876',
|
||||
author: {
|
||||
slug: 'the-author',
|
||||
},
|
||||
pinnedBy: {
|
||||
id: 'current-user',
|
||||
name: 'Admin',
|
||||
role: 'admin',
|
||||
},
|
||||
describe('post created by another user', () => {
|
||||
it('responds with the updated Post', async () => {
|
||||
const expected = {
|
||||
data: {
|
||||
pinPost: {
|
||||
id: 'p9876',
|
||||
author: {
|
||||
slug: 'the-author',
|
||||
},
|
||||
pinnedBy: {
|
||||
id: 'current-user',
|
||||
name: 'Admin',
|
||||
role: 'admin',
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('pinned post already exists', () => {
|
||||
let pinnedPost
|
||||
beforeEach(async () => {
|
||||
await Factory.build(
|
||||
'post',
|
||||
{
|
||||
id: 'only-pinned-post',
|
||||
},
|
||||
{
|
||||
author: admin,
|
||||
},
|
||||
)
|
||||
await mutate({ mutation: pinPostMutation, variables })
|
||||
})
|
||||
|
||||
it('removes previous `pinned` attribute', async () => {
|
||||
const cypher = 'MATCH (post:Post) WHERE post.pinned IS NOT NULL RETURN post'
|
||||
pinnedPost = await neode.cypher(cypher)
|
||||
expect(pinnedPost.records).toHaveLength(1)
|
||||
variables = { ...variables, id: 'only-pinned-post' }
|
||||
await mutate({ mutation: pinPostMutation, variables })
|
||||
pinnedPost = await neode.cypher(cypher)
|
||||
expect(pinnedPost.records).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('removes previous PINNED relationship', async () => {
|
||||
variables = { ...variables, id: 'only-pinned-post' }
|
||||
await mutate({ mutation: pinPostMutation, variables })
|
||||
pinnedPost = await neode.cypher(
|
||||
`MATCH (:User)-[pinned:PINNED]->(post:Post) RETURN post, pinned`,
|
||||
)
|
||||
expect(pinnedPost.records).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('PostOrdering', () => {
|
||||
beforeEach(async () => {
|
||||
await Factory.build('post', {
|
||||
id: 'im-a-pinned-post',
|
||||
createdAt: '2019-11-22T17:26:29.070Z',
|
||||
pinned: true,
|
||||
})
|
||||
await Factory.build('post', {
|
||||
id: 'i-was-created-before-pinned-post',
|
||||
// fairly old, so this should be 3rd
|
||||
createdAt: '2019-10-22T17:26:29.070Z',
|
||||
})
|
||||
})
|
||||
|
||||
describe('pinned post already exists', () => {
|
||||
let pinnedPost
|
||||
beforeEach(async () => {
|
||||
await Factory.build(
|
||||
'post',
|
||||
{
|
||||
id: 'only-pinned-post',
|
||||
},
|
||||
{
|
||||
author: admin,
|
||||
},
|
||||
)
|
||||
await mutate({ mutation: pinPostMutation, variables })
|
||||
describe('order by `pinned_asc` and `createdAt_desc`', () => {
|
||||
beforeEach(() => {
|
||||
// this is the ordering in the frontend
|
||||
variables = { orderBy: ['pinned_asc', 'createdAt_desc'] }
|
||||
})
|
||||
|
||||
it('removes previous `pinned` attribute', async () => {
|
||||
const cypher = 'MATCH (post:Post) WHERE post.pinned IS NOT NULL RETURN post'
|
||||
pinnedPost = await neode.cypher(cypher)
|
||||
expect(pinnedPost.records).toHaveLength(1)
|
||||
variables = { ...variables, id: 'only-pinned-post' }
|
||||
await mutate({ mutation: pinPostMutation, variables })
|
||||
pinnedPost = await neode.cypher(cypher)
|
||||
expect(pinnedPost.records).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('removes previous PINNED relationship', async () => {
|
||||
variables = { ...variables, id: 'only-pinned-post' }
|
||||
await mutate({ mutation: pinPostMutation, variables })
|
||||
pinnedPost = await neode.cypher(
|
||||
`MATCH (:User)-[pinned:PINNED]->(post:Post) RETURN post, pinned`,
|
||||
)
|
||||
expect(pinnedPost.records).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('PostOrdering', () => {
|
||||
beforeEach(async () => {
|
||||
await Factory.build('post', {
|
||||
id: 'im-a-pinned-post',
|
||||
createdAt: '2019-11-22T17:26:29.070Z',
|
||||
pinned: true,
|
||||
})
|
||||
await Factory.build('post', {
|
||||
id: 'i-was-created-before-pinned-post',
|
||||
// fairly old, so this should be 3rd
|
||||
createdAt: '2019-10-22T17:26:29.070Z',
|
||||
})
|
||||
})
|
||||
|
||||
describe('order by `pinned_asc` and `createdAt_desc`', () => {
|
||||
beforeEach(() => {
|
||||
// this is the ordering in the frontend
|
||||
variables = { orderBy: ['pinned_asc', 'createdAt_desc'] }
|
||||
})
|
||||
|
||||
it('pinned post appear first even when created before other posts', async () => {
|
||||
const postOrderingQuery = gql`
|
||||
query($orderBy: [_PostOrdering]) {
|
||||
Post(orderBy: $orderBy) {
|
||||
id
|
||||
pinned
|
||||
createdAt
|
||||
pinnedAt
|
||||
}
|
||||
it('pinned post appear first even when created before other posts', async () => {
|
||||
const postOrderingQuery = gql`
|
||||
query($orderBy: [_PostOrdering]) {
|
||||
Post(orderBy: $orderBy) {
|
||||
id
|
||||
pinned
|
||||
createdAt
|
||||
pinnedAt
|
||||
}
|
||||
`
|
||||
await expect(query({ query: postOrderingQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
Post: [
|
||||
{
|
||||
id: 'im-a-pinned-post',
|
||||
pinned: true,
|
||||
createdAt: '2019-11-22T17:26:29.070Z',
|
||||
pinnedAt: expect.any(String),
|
||||
},
|
||||
{
|
||||
id: 'p9876',
|
||||
pinned: null,
|
||||
createdAt: expect.any(String),
|
||||
pinnedAt: null,
|
||||
},
|
||||
{
|
||||
id: 'i-was-created-before-pinned-post',
|
||||
pinned: null,
|
||||
createdAt: '2019-10-22T17:26:29.070Z',
|
||||
pinnedAt: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
}
|
||||
`
|
||||
await expect(query({ query: postOrderingQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
Post: [
|
||||
{
|
||||
id: 'im-a-pinned-post',
|
||||
pinned: true,
|
||||
createdAt: '2019-11-22T17:26:29.070Z',
|
||||
pinnedAt: expect.any(String),
|
||||
},
|
||||
{
|
||||
id: 'p9876',
|
||||
pinned: null,
|
||||
createdAt: expect.any(String),
|
||||
pinnedAt: null,
|
||||
},
|
||||
{
|
||||
id: 'i-was-created-before-pinned-post',
|
||||
pinned: null,
|
||||
createdAt: '2019-10-22T17:26:29.070Z',
|
||||
pinnedAt: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('unpin posts', () => {
|
||||
const unpinPostMutation = gql`
|
||||
mutation($id: ID!) {
|
||||
unpinPost(id: $id) {
|
||||
describe('unpin posts', () => {
|
||||
let pinnedPost
|
||||
const unpinPostMutation = gql`
|
||||
mutation($id: ID!) {
|
||||
unpinPost(id: $id) {
|
||||
id
|
||||
title
|
||||
content
|
||||
author {
|
||||
name
|
||||
slug
|
||||
}
|
||||
pinnedBy {
|
||||
id
|
||||
title
|
||||
content
|
||||
author {
|
||||
name
|
||||
slug
|
||||
}
|
||||
pinnedBy {
|
||||
id
|
||||
name
|
||||
role
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
pinned
|
||||
pinnedAt
|
||||
name
|
||||
role
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
pinned
|
||||
pinnedAt
|
||||
}
|
||||
`
|
||||
}
|
||||
`
|
||||
beforeEach(async () => {
|
||||
pinnedPost = await Factory.build('post', { id: 'post-to-be-unpinned' })
|
||||
variables = {
|
||||
id: 'post-to-be-unpinned',
|
||||
}
|
||||
})
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
authenticatedUser = null
|
||||
await expect(mutate({ mutation: unpinPostMutation, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
data: { unpinPost: null },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('users cannot unpin posts', () => {
|
||||
it('throws authorization error', async () => {
|
||||
await expect(mutate({ mutation: unpinPostMutation, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
data: { unpinPost: null },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('moderators cannot unpin posts', () => {
|
||||
let moderator
|
||||
beforeEach(async () => {
|
||||
variables = { ...variables }
|
||||
moderator = await user.update({ role: 'moderator', updatedAt: new Date().toISOString() })
|
||||
authenticatedUser = await moderator.toJson()
|
||||
})
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
authenticatedUser = null
|
||||
await expect(mutate({ mutation: unpinPostMutation, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
data: { unpinPost: null },
|
||||
})
|
||||
it('throws authorization error', async () => {
|
||||
await expect(mutate({ mutation: unpinPostMutation, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
data: { unpinPost: null },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('users cannot unpin posts', () => {
|
||||
it('throws authorization error', async () => {
|
||||
await expect(mutate({ mutation: unpinPostMutation, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
data: { unpinPost: null },
|
||||
})
|
||||
describe('admin can unpin posts', () => {
|
||||
let admin
|
||||
beforeEach(async () => {
|
||||
admin = await user.update({
|
||||
role: 'admin',
|
||||
name: 'Admin',
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
authenticatedUser = await admin.toJson()
|
||||
await admin.relateTo(pinnedPost, 'pinned', { createdAt: new Date().toISOString() })
|
||||
})
|
||||
|
||||
describe('moderators cannot unpin posts', () => {
|
||||
let moderator
|
||||
beforeEach(async () => {
|
||||
moderator = await user.update({ role: 'moderator', updatedAt: new Date().toISOString() })
|
||||
authenticatedUser = await moderator.toJson()
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
await expect(mutate({ mutation: unpinPostMutation, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
data: { unpinPost: null },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('admin can unpin posts', () => {
|
||||
let admin, pinnedPost
|
||||
beforeEach(async () => {
|
||||
pinnedPost = await Factory.build('post', { id: 'post-to-be-unpinned' })
|
||||
admin = await user.update({
|
||||
role: 'admin',
|
||||
name: 'Admin',
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
authenticatedUser = await admin.toJson()
|
||||
await admin.relateTo(pinnedPost, 'pinned', { createdAt: new Date().toISOString() })
|
||||
variables = { ...variables, id: 'post-to-be-unpinned' }
|
||||
})
|
||||
|
||||
it('responds with the unpinned Post', async () => {
|
||||
authenticatedUser = await admin.toJson()
|
||||
const expected = {
|
||||
data: {
|
||||
unpinPost: {
|
||||
id: 'post-to-be-unpinned',
|
||||
pinnedBy: null,
|
||||
pinnedAt: null,
|
||||
},
|
||||
it('responds with the unpinned Post', async () => {
|
||||
authenticatedUser = await admin.toJson()
|
||||
const expected = {
|
||||
data: {
|
||||
unpinPost: {
|
||||
id: 'post-to-be-unpinned',
|
||||
pinnedBy: null,
|
||||
pinnedAt: null,
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
|
||||
await expect(mutate({ mutation: unpinPostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
await expect(mutate({ mutation: unpinPostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
|
||||
it('unsets `pinned` property', async () => {
|
||||
const expected = {
|
||||
data: {
|
||||
unpinPost: {
|
||||
id: 'post-to-be-unpinned',
|
||||
pinned: null,
|
||||
},
|
||||
it('unsets `pinned` property', async () => {
|
||||
const expected = {
|
||||
data: {
|
||||
unpinPost: {
|
||||
id: 'post-to-be-unpinned',
|
||||
pinned: null,
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: unpinPostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: unpinPostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -897,7 +956,9 @@ describe('DeletePost', () => {
|
||||
deleted
|
||||
content
|
||||
contentExcerpt
|
||||
image
|
||||
image {
|
||||
url
|
||||
}
|
||||
comments {
|
||||
deleted
|
||||
content
|
||||
@ -915,9 +976,11 @@ describe('DeletePost', () => {
|
||||
id: 'p4711',
|
||||
title: 'I will be deleted',
|
||||
content: 'To be deleted',
|
||||
image: 'path/to/some/image',
|
||||
},
|
||||
{
|
||||
image: Factory.build('image', {
|
||||
url: 'path/to/some/image',
|
||||
}),
|
||||
author,
|
||||
categoryIds,
|
||||
},
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
import { UserInputError } from 'apollo-server'
|
||||
import { getNeode } from '../../db/neo4j'
|
||||
import fileUpload from './fileUpload'
|
||||
import encryptPassword from '../../helpers/encryptPassword'
|
||||
import generateNonce from './helpers/generateNonce'
|
||||
import existingEmailAddress from './helpers/existingEmailAddress'
|
||||
import normalizeEmail from './helpers/normalizeEmail'
|
||||
import createOrUpdateLocations from './users/location'
|
||||
|
||||
const neode = getNeode()
|
||||
|
||||
@ -24,8 +22,6 @@ export default {
|
||||
}
|
||||
},
|
||||
SignupVerification: async (_parent, args, context) => {
|
||||
const { driver } = context
|
||||
const session = driver.session()
|
||||
const { termsAndConditionsAgreedVersion } = args
|
||||
const regEx = new RegExp(/^[0-9]+\.[0-9]+\.[0-9]+$/g)
|
||||
if (!regEx.test(termsAndConditionsAgreedVersion)) {
|
||||
@ -35,27 +31,39 @@ export default {
|
||||
|
||||
let { nonce, email } = args
|
||||
email = normalizeEmail(email)
|
||||
const result = await neode.cypher(
|
||||
`
|
||||
MATCH(email:EmailAddress {nonce: {nonce}, email: {email}})
|
||||
WHERE NOT (email)-[:BELONGS_TO]->()
|
||||
RETURN email
|
||||
`,
|
||||
{ nonce, email },
|
||||
)
|
||||
const emailAddress = await neode.hydrateFirst(result, 'email', neode.model('EmailAddress'))
|
||||
if (!emailAddress) throw new UserInputError('Invalid email or nonce')
|
||||
args = await fileUpload(args, { file: 'avatarUpload', url: 'avatar' })
|
||||
args = await encryptPassword(args)
|
||||
delete args.nonce
|
||||
delete args.email
|
||||
args = encryptPassword(args)
|
||||
|
||||
const { driver } = context
|
||||
const session = driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const createUserTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH(email:EmailAddress {nonce: $nonce, email: $email})
|
||||
WHERE NOT (email)-[:BELONGS_TO]->()
|
||||
CREATE (user:User)
|
||||
MERGE(user)-[:PRIMARY_EMAIL]->(email)
|
||||
MERGE(user)<-[:BELONGS_TO]-(email)
|
||||
SET user += $args
|
||||
SET user.id = randomUUID()
|
||||
SET user.role = 'user'
|
||||
SET user.createdAt = toString(datetime())
|
||||
SET user.updatedAt = toString(datetime())
|
||||
SET user.allowEmbedIframes = FALSE
|
||||
SET user.showShoutsPublicly = FALSE
|
||||
SET email.verifiedAt = toString(datetime())
|
||||
RETURN user {.*}
|
||||
`,
|
||||
{ args, nonce, email },
|
||||
)
|
||||
const [user] = createUserTransactionResponse.records.map(record => record.get('user'))
|
||||
if (!user) throw new UserInputError('Invalid email or nonce')
|
||||
return user
|
||||
})
|
||||
try {
|
||||
const user = await neode.create('User', args)
|
||||
await Promise.all([
|
||||
user.relateTo(emailAddress, 'primaryEmail'),
|
||||
emailAddress.relateTo(user, 'belongsTo'),
|
||||
emailAddress.update({ verifiedAt: new Date().toISOString() }),
|
||||
])
|
||||
await createOrUpdateLocations(args.id, args.locationName, session)
|
||||
return user.toJson()
|
||||
const user = await writeTxResultPromise
|
||||
return user
|
||||
} catch (e) {
|
||||
if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
|
||||
throw new UserInputError('User with this slug already exists!')
|
||||
|
||||
@ -1,22 +1,19 @@
|
||||
import log from './helpers/databaseLogger'
|
||||
import { queryString } from './searches/queryString'
|
||||
|
||||
// see http://lucene.apache.org/core/8_3_1/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package.description
|
||||
|
||||
export default {
|
||||
Query: {
|
||||
findResources: async (_parent, args, context, _resolveInfo) => {
|
||||
const { query, limit } = args
|
||||
const { id: thisUserId } = context.user
|
||||
// see http://lucene.apache.org/core/8_3_1/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package.description
|
||||
const myQuery = query
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/[[@#:*~\\$|^\]?/"'(){}+?!,.-;]/g, '')
|
||||
.split(' ')
|
||||
.map(s => (s.toLowerCase().match(/^(not|and|or)$/) ? '"' + s + '"' : s + '*'))
|
||||
.join(' ')
|
||||
|
||||
const postCypher = `
|
||||
CALL db.index.fulltext.queryNodes('post_fulltext_search', $query)
|
||||
YIELD node as resource, score
|
||||
MATCH (resource)<-[:WROTE]-(author:User)
|
||||
WHERE score >= 0.5
|
||||
WHERE score >= 0.0
|
||||
AND NOT (
|
||||
author.deleted = true OR author.disabled = true
|
||||
OR resource.deleted = true OR resource.disabled = true
|
||||
@ -39,11 +36,22 @@ export default {
|
||||
CALL db.index.fulltext.queryNodes('user_fulltext_search', $query)
|
||||
YIELD node as resource, score
|
||||
MATCH (resource)
|
||||
WHERE score >= 0.5
|
||||
WHERE score >= 0.0
|
||||
AND NOT (resource.deleted = true OR resource.disabled = true)
|
||||
RETURN resource {.*, __typename: labels(resource)[0]}
|
||||
LIMIT $limit
|
||||
`
|
||||
const tagCypher = `
|
||||
CALL db.index.fulltext.queryNodes('tag_fulltext_search', $query)
|
||||
YIELD node as resource, score
|
||||
MATCH (resource)
|
||||
WHERE score >= 0.0
|
||||
AND NOT (resource.deleted = true OR resource.disabled = true)
|
||||
RETURN resource {.*, __typename: labels(resource)[0]}
|
||||
LIMIT $limit
|
||||
`
|
||||
|
||||
const myQuery = queryString(query)
|
||||
|
||||
const session = context.driver.session()
|
||||
const searchResultPromise = session.readTransaction(async transaction => {
|
||||
@ -57,14 +65,25 @@ export default {
|
||||
limit,
|
||||
thisUserId,
|
||||
})
|
||||
return Promise.all([postTransactionResponse, userTransactionResponse])
|
||||
const tagTransactionResponse = transaction.run(tagCypher, {
|
||||
query: myQuery,
|
||||
limit,
|
||||
})
|
||||
return Promise.all([
|
||||
postTransactionResponse,
|
||||
userTransactionResponse,
|
||||
tagTransactionResponse,
|
||||
])
|
||||
})
|
||||
|
||||
try {
|
||||
const [postResults, userResults] = await searchResultPromise
|
||||
const [postResults, userResults, tagResults] = await searchResultPromise
|
||||
log(postResults)
|
||||
log(userResults)
|
||||
return [...postResults.records, ...userResults.records].map(r => r.get('resource'))
|
||||
log(tagResults)
|
||||
return [...postResults.records, ...userResults.records, ...tagResults.records].map(r =>
|
||||
r.get('resource'),
|
||||
)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
469
backend/src/schema/resolvers/searches.spec.js
Normal file
469
backend/src/schema/resolvers/searches.spec.js
Normal file
@ -0,0 +1,469 @@
|
||||
import Factory, { cleanDatabase } from '../../db/factories'
|
||||
import { gql } from '../../helpers/jest'
|
||||
import { getNeode, getDriver } from '../../db/neo4j'
|
||||
import createServer from '../../server'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
|
||||
let query, authenticatedUser, user
|
||||
|
||||
const driver = getDriver()
|
||||
const neode = getNeode()
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
driver,
|
||||
neode,
|
||||
user: authenticatedUser,
|
||||
}
|
||||
},
|
||||
})
|
||||
query = createTestClient(server).query
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDatabase()
|
||||
})
|
||||
|
||||
const searchQuery = gql`
|
||||
query($query: String!) {
|
||||
findResources(query: $query, limit: 5) {
|
||||
__typename
|
||||
... on Post {
|
||||
id
|
||||
title
|
||||
content
|
||||
}
|
||||
... on User {
|
||||
id
|
||||
slug
|
||||
name
|
||||
}
|
||||
... on Tag {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
describe('resolvers/searches', () => {
|
||||
let variables
|
||||
|
||||
describe('given one user', () => {
|
||||
beforeAll(async () => {
|
||||
user = await Factory.build('user', {
|
||||
id: 'a-user',
|
||||
name: 'John Doe',
|
||||
slug: 'john-doe',
|
||||
})
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
describe('query contains first name of user', () => {
|
||||
it('finds the user', async () => {
|
||||
variables = { query: 'John' }
|
||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
findResources: [
|
||||
{
|
||||
id: 'a-user',
|
||||
name: 'John Doe',
|
||||
slug: 'john-doe',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('adding one post', () => {
|
||||
beforeAll(async () => {
|
||||
await Factory.build(
|
||||
'post',
|
||||
{
|
||||
id: 'a-post',
|
||||
title: 'Beitrag',
|
||||
content: 'Ein erster Beitrag',
|
||||
},
|
||||
{ authorId: 'a-user' },
|
||||
)
|
||||
})
|
||||
|
||||
describe('query contains title of post', () => {
|
||||
it('finds the post', async () => {
|
||||
variables = { query: 'beitrag' }
|
||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
findResources: [
|
||||
{
|
||||
__typename: 'Post',
|
||||
id: 'a-post',
|
||||
title: 'Beitrag',
|
||||
content: 'Ein erster Beitrag',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('casing', () => {
|
||||
it('does not matter', async () => {
|
||||
variables = { query: 'BEITRAG' }
|
||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
findResources: [
|
||||
{
|
||||
__typename: 'Post',
|
||||
id: 'a-post',
|
||||
title: 'Beitrag',
|
||||
content: 'Ein erster Beitrag',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('query consists of words not present in the corpus', () => {
|
||||
it('returns empty search results', async () => {
|
||||
await expect(
|
||||
query({ query: searchQuery, variables: { query: 'Unfug' } }),
|
||||
).resolves.toMatchObject({ data: { findResources: [] } })
|
||||
})
|
||||
})
|
||||
|
||||
describe('testing different post content', () => {
|
||||
beforeAll(async () => {
|
||||
return Promise.all([
|
||||
Factory.build(
|
||||
'post',
|
||||
{
|
||||
id: 'b-post',
|
||||
title: 'Aufruf',
|
||||
content: 'Jeder sollte seinen Beitrag leisten.',
|
||||
},
|
||||
{ authorId: 'a-user' },
|
||||
),
|
||||
Factory.build(
|
||||
'post',
|
||||
{
|
||||
id: 'g-post',
|
||||
title: 'Zusammengesetzte Wörter',
|
||||
content: `Ein Bindestrich kann zwischen zwei Substantiven auch dann gesetzt werden, wenn drei gleichlautende Buchstaben aufeinandertreffen. Das ist etwa bei einem „Teeei“ der Fall, das so korrekt geschrieben ist. Möglich ist hier auch die Schreibweise mit Bindestrich: Tee-Ei.`,
|
||||
},
|
||||
{ authorId: 'a-user' },
|
||||
),
|
||||
Factory.build(
|
||||
'post',
|
||||
{
|
||||
id: 'c-post',
|
||||
title: 'Die binomischen Formeln',
|
||||
content: `1. binomische Formel: (a + b)² = a² + 2ab + b²
|
||||
2. binomische Formel: (a - b)² = a² - 2ab + b²
|
||||
3. binomische Formel: (a + b)(a - b) = a² - b²`,
|
||||
},
|
||||
{ authorId: 'a-user' },
|
||||
),
|
||||
Factory.build(
|
||||
'post',
|
||||
{
|
||||
id: 'd-post',
|
||||
title: 'Der Panther',
|
||||
content: `Sein Blick ist vom Vorübergehn der Stäbe
|
||||
so müd geworden, daß er nichts mehr hält.
|
||||
Ihm ist, als ob es tausend Stäbe gäbe
|
||||
und hinter tausend Stäben keine Welt.`,
|
||||
},
|
||||
{ authorId: 'a-user' },
|
||||
),
|
||||
])
|
||||
})
|
||||
|
||||
describe('a post which content contains the title of the first post', () => {
|
||||
describe('query contains the title of the first post', () => {
|
||||
it('finds both posts', async () => {
|
||||
variables = { query: 'beitrag' }
|
||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
findResources: expect.arrayContaining([
|
||||
{
|
||||
__typename: 'Post',
|
||||
id: 'a-post',
|
||||
title: 'Beitrag',
|
||||
content: 'Ein erster Beitrag',
|
||||
},
|
||||
{
|
||||
__typename: 'Post',
|
||||
id: 'b-post',
|
||||
title: 'Aufruf',
|
||||
content: 'Jeder sollte seinen Beitrag leisten.',
|
||||
},
|
||||
]),
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('a post that contains a hyphen between two words and German quotation marks', () => {
|
||||
describe('hyphens in query', () => {
|
||||
it('will be treated as ordinary characters', async () => {
|
||||
variables = { query: 'tee-ei' }
|
||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
findResources: [
|
||||
{
|
||||
__typename: 'Post',
|
||||
id: 'g-post',
|
||||
title: 'Zusammengesetzte Wörter',
|
||||
content: `Ein Bindestrich kann zwischen zwei Substantiven auch dann gesetzt werden, wenn drei gleichlautende Buchstaben aufeinandertreffen. Das ist etwa bei einem „Teeei“ der Fall, das so korrekt geschrieben ist. Möglich ist hier auch die Schreibweise mit Bindestrich: Tee-Ei.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('German quotation marks in query to test unicode characters (\u201E ... \u201C)', () => {
|
||||
it('will be treated as ordinary characters', async () => {
|
||||
variables = { query: '„teeei“' }
|
||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
findResources: [
|
||||
{
|
||||
__typename: 'Post',
|
||||
id: 'g-post',
|
||||
title: 'Zusammengesetzte Wörter',
|
||||
content: `Ein Bindestrich kann zwischen zwei Substantiven auch dann gesetzt werden, wenn drei gleichlautende Buchstaben aufeinandertreffen. Das ist etwa bei einem „Teeei“ der Fall, das so korrekt geschrieben ist. Möglich ist hier auch die Schreibweise mit Bindestrich: Tee-Ei.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('a post that contains a simple mathematical exprssion and line breaks', () => {
|
||||
describe('query a part of the mathematical expression', () => {
|
||||
it('finds that post', async () => {
|
||||
variables = { query: '(a - b)²' }
|
||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
findResources: [
|
||||
{
|
||||
__typename: 'Post',
|
||||
id: 'c-post',
|
||||
title: 'Die binomischen Formeln',
|
||||
content: `1. binomische Formel: (a + b)² = a² + 2ab + b²<br>
|
||||
2. binomische Formel: (a - b)² = a² - 2ab + b²<br>
|
||||
3. binomische Formel: (a + b)(a - b) = a² - b²`,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('query the same part of the mathematical expression without spaces', () => {
|
||||
it('finds that post', async () => {
|
||||
variables = { query: '(a-b)²' }
|
||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
findResources: [
|
||||
{
|
||||
__typename: 'Post',
|
||||
id: 'c-post',
|
||||
title: 'Die binomischen Formeln',
|
||||
content: `1. binomische Formel: (a + b)² = a² + 2ab + b²<br>
|
||||
2. binomische Formel: (a - b)² = a² - 2ab + b²<br>
|
||||
3. binomische Formel: (a + b)(a - b) = a² - b²`,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('query the mathematical expression over line break', () => {
|
||||
it('finds that post', async () => {
|
||||
variables = { query: '+ b² 2.' }
|
||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
findResources: [
|
||||
{
|
||||
__typename: 'Post',
|
||||
id: 'c-post',
|
||||
title: 'Die binomischen Formeln',
|
||||
content: `1. binomische Formel: (a + b)² = a² + 2ab + b²<br>
|
||||
2. binomische Formel: (a - b)² = a² - 2ab + b²<br>
|
||||
3. binomische Formel: (a + b)(a - b) = a² - b²`,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('a post that contains a poem', () => {
|
||||
describe('query for more than one word, e.g. the title of the poem', () => {
|
||||
it('finds the poem and another post that contains only one word but with lower score', async () => {
|
||||
variables = { query: 'der panther' }
|
||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
findResources: [
|
||||
{
|
||||
__typename: 'Post',
|
||||
id: 'd-post',
|
||||
title: 'Der Panther',
|
||||
content: `Sein Blick ist vom Vorübergehn der Stäbe<br>
|
||||
so müd geworden, daß er nichts mehr hält.<br>
|
||||
Ihm ist, als ob es tausend Stäbe gäbe<br>
|
||||
und hinter tausend Stäben keine Welt.`,
|
||||
},
|
||||
{
|
||||
__typename: 'Post',
|
||||
id: 'g-post',
|
||||
title: 'Zusammengesetzte Wörter',
|
||||
content: `Ein Bindestrich kann zwischen zwei Substantiven auch dann gesetzt werden, wenn drei gleichlautende Buchstaben aufeinandertreffen. Das ist etwa bei einem „Teeei“ der Fall, das so korrekt geschrieben ist. Möglich ist hier auch die Schreibweise mit Bindestrich: Tee-Ei.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('query for the first four letters of two longer words', () => {
|
||||
it('finds the posts that contain words starting with these four letters', async () => {
|
||||
variables = { query: 'Vorü Subs' }
|
||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
findResources: expect.arrayContaining([
|
||||
{
|
||||
__typename: 'Post',
|
||||
id: 'd-post',
|
||||
title: 'Der Panther',
|
||||
content: `Sein Blick ist vom Vorübergehn der Stäbe<br>
|
||||
so müd geworden, daß er nichts mehr hält.<br>
|
||||
Ihm ist, als ob es tausend Stäbe gäbe<br>
|
||||
und hinter tausend Stäben keine Welt.`,
|
||||
},
|
||||
{
|
||||
__typename: 'Post',
|
||||
id: 'g-post',
|
||||
title: 'Zusammengesetzte Wörter',
|
||||
content: `Ein Bindestrich kann zwischen zwei Substantiven auch dann gesetzt werden, wenn drei gleichlautende Buchstaben aufeinandertreffen. Das ist etwa bei einem „Teeei“ der Fall, das so korrekt geschrieben ist. Möglich ist hier auch die Schreibweise mit Bindestrich: Tee-Ei.`,
|
||||
},
|
||||
]),
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('adding two users that have the same word in their slugs', () => {
|
||||
beforeAll(async () => {
|
||||
await Promise.all([
|
||||
Factory.build('user', {
|
||||
id: 'c-user',
|
||||
name: 'Rainer Maria Rilke',
|
||||
slug: 'rainer-maria-rilke',
|
||||
}),
|
||||
Factory.build('user', {
|
||||
id: 'd-user',
|
||||
name: 'Erich Maria Remarque',
|
||||
slug: 'erich-maria-remarque',
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
describe('query the word that both slugs contain', () => {
|
||||
it('finds both users', async () => {
|
||||
variables = { query: '-maria-' }
|
||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
findResources: expect.arrayContaining([
|
||||
{
|
||||
__typename: 'User',
|
||||
id: 'c-user',
|
||||
name: 'Rainer Maria Rilke',
|
||||
slug: 'rainer-maria-rilke',
|
||||
},
|
||||
{
|
||||
__typename: 'User',
|
||||
id: 'd-user',
|
||||
name: 'Erich Maria Remarque',
|
||||
slug: 'erich-maria-remarque',
|
||||
},
|
||||
]),
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('adding a post, written by a user who is muted by the authenticated user', () => {
|
||||
beforeAll(async () => {
|
||||
const mutedUser = await Factory.build('user', {
|
||||
id: 'muted-user',
|
||||
name: 'Muted',
|
||||
slug: 'muted',
|
||||
})
|
||||
await user.relateTo(mutedUser, 'muted')
|
||||
await Factory.build(
|
||||
'post',
|
||||
{
|
||||
id: 'muted-post',
|
||||
title: 'Beleidigender Beitrag',
|
||||
content: 'Dieser Beitrag stammt von einem bleidigendem Nutzer.',
|
||||
},
|
||||
{ authorId: 'muted-user' },
|
||||
)
|
||||
})
|
||||
|
||||
describe('query for text in a post written by a muted user', () => {
|
||||
it('does not include the post of the muted user in the results', async () => {
|
||||
variables = { query: 'beitrag' }
|
||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
findResources: expect.not.arrayContaining([
|
||||
{
|
||||
__typename: 'Post',
|
||||
id: 'muted-post',
|
||||
title: 'Beleidigender Beitrag',
|
||||
content: 'Dieser Beitrag stammt von einem bleidigendem Nutzer.',
|
||||
},
|
||||
]),
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('adding a tag', () => {
|
||||
beforeAll(async () => {
|
||||
await Factory.build('tag', { id: 'myHashtag' })
|
||||
})
|
||||
|
||||
describe('query the first four characters of the tag', () => {
|
||||
it('finds the tag', async () => {
|
||||
variables = { query: 'myha' }
|
||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
findResources: [
|
||||
{
|
||||
__typename: 'Tag',
|
||||
id: 'myHashtag',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
47
backend/src/schema/resolvers/searches/queryString.js
Normal file
47
backend/src/schema/resolvers/searches/queryString.js
Normal file
@ -0,0 +1,47 @@
|
||||
export function queryString(str) {
|
||||
const normalizedString = normalizeWhitespace(str)
|
||||
const escapedString = escapeSpecialCharacters(normalizedString)
|
||||
return `
|
||||
${matchWholeText(escapedString)}
|
||||
${matchEachWordExactly(escapedString)}
|
||||
${matchSomeWordsExactly(escapedString)}
|
||||
${matchBeginningOfWords(escapedString)}
|
||||
`
|
||||
}
|
||||
|
||||
const matchWholeText = (str, boost = 8) => {
|
||||
return `"${str}"^${boost}`
|
||||
}
|
||||
|
||||
const matchEachWordExactly = (str, boost = 4) => {
|
||||
if (!str.includes(' ')) return ''
|
||||
const tmp = str
|
||||
.split(' ')
|
||||
.map((s, i) => (i === 0 ? `"${s}"` : `AND "${s}"`))
|
||||
.join(' ')
|
||||
return `(${tmp})^${boost}`
|
||||
}
|
||||
|
||||
const matchSomeWordsExactly = (str, boost = 2) => {
|
||||
if (!str.includes(' ')) return ''
|
||||
return str
|
||||
.split(' ')
|
||||
.map(s => `"${s}"^${boost}`)
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
const matchBeginningOfWords = str => {
|
||||
return str
|
||||
.split(' ')
|
||||
.filter(s => s.length > 3)
|
||||
.map(s => s + '*')
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
export function normalizeWhitespace(str) {
|
||||
return str.replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
export function escapeSpecialCharacters(str) {
|
||||
return str.replace(/(["[\]&|\\{}+!()^~*?:/-])/g, '\\$1')
|
||||
}
|
||||
43
backend/src/schema/resolvers/searches/queryString.spec.js
Normal file
43
backend/src/schema/resolvers/searches/queryString.spec.js
Normal file
@ -0,0 +1,43 @@
|
||||
import { queryString, escapeSpecialCharacters, normalizeWhitespace } from './queryString'
|
||||
|
||||
describe('queryString', () => {
|
||||
describe('special characters', () => {
|
||||
it('does escaping correctly', () => {
|
||||
expect(escapeSpecialCharacters('+ - && || ! ( ) { } [ ] ^ " ~ * ? : \\ / ')).toEqual(
|
||||
'\\+ \\- \\&\\& \\|\\| \\! \\( \\) \\{ \\} \\[ \\] \\^ \\" \\~ \\* \\? \\: \\\\ \\/ ',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('whitespace', () => {
|
||||
it('normalizes correctly', () => {
|
||||
expect(normalizeWhitespace(' a \t \n b \n ')).toEqual('a b')
|
||||
})
|
||||
})
|
||||
|
||||
describe('exact match', () => {
|
||||
it('boosts score by factor 8', () => {
|
||||
expect(queryString('a couple of words')).toContain('"a couple of words"^8')
|
||||
})
|
||||
})
|
||||
|
||||
describe('match all words exactly', () => {
|
||||
it('boosts score by factor 4', () => {
|
||||
expect(queryString('a couple of words')).toContain(
|
||||
'("a" AND "couple" AND "of" AND "words")^4',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('match at least one word exactly', () => {
|
||||
it('boosts score by factor 2', () => {
|
||||
expect(queryString('a couple of words')).toContain('"a"^2 "couple"^2 "of"^2 "words"^2')
|
||||
})
|
||||
})
|
||||
|
||||
describe('globbing for longer words', () => {
|
||||
it('globs words with more than three characters', () => {
|
||||
expect(queryString('a couple of words')).toContain('couple* words*')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -48,7 +48,7 @@ export default {
|
||||
const loginTransactionResponse = await transaction.run(
|
||||
`
|
||||
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, .encryptedPassword, .role, .disabled, email:e.email} as user LIMIT 1
|
||||
`,
|
||||
{ userEmail: email },
|
||||
)
|
||||
|
||||
@ -106,7 +106,9 @@ describe('currentUser', () => {
|
||||
id
|
||||
slug
|
||||
name
|
||||
avatar
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
email
|
||||
role
|
||||
}
|
||||
@ -131,13 +133,15 @@ describe('currentUser', () => {
|
||||
{
|
||||
id: 'u3',
|
||||
// the `id` is the only thing that has to match the decoded JWT bearer token
|
||||
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg',
|
||||
name: 'Matilde Hermiston',
|
||||
slug: 'matilde-hermiston',
|
||||
role: 'user',
|
||||
},
|
||||
{
|
||||
email: 'test@example.org',
|
||||
avatar: Factory.build('image', {
|
||||
url: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg',
|
||||
}),
|
||||
},
|
||||
)
|
||||
const userBearerToken = encode({ id: 'u3' })
|
||||
@ -149,7 +153,9 @@ describe('currentUser', () => {
|
||||
data: {
|
||||
currentUser: {
|
||||
id: 'u3',
|
||||
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg',
|
||||
avatar: Factory.build('image', {
|
||||
url: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg',
|
||||
}),
|
||||
email: 'test@example.org',
|
||||
name: 'Matilde Hermiston',
|
||||
slug: 'matilde-hermiston',
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||
import fileUpload from './fileUpload'
|
||||
import { getNeode } from '../../db/neo4j'
|
||||
import { UserInputError, ForbiddenError } from 'apollo-server'
|
||||
import { mergeImage, deleteImage } from './images/images'
|
||||
import Resolver from './helpers/Resolver'
|
||||
import log from './helpers/databaseLogger'
|
||||
import createOrUpdateLocations from './users/location'
|
||||
@ -140,6 +140,8 @@ export default {
|
||||
},
|
||||
UpdateUser: async (_parent, params, context, _resolveInfo) => {
|
||||
const { termsAndConditionsAgreedVersion } = params
|
||||
const { avatar: avatarInput } = params
|
||||
delete params.avatar
|
||||
if (termsAndConditionsAgreedVersion) {
|
||||
const regEx = new RegExp(/^[0-9]+\.[0-9]+\.[0-9]+$/g)
|
||||
if (!regEx.test(termsAndConditionsAgreedVersion)) {
|
||||
@ -147,7 +149,6 @@ export default {
|
||||
}
|
||||
params.termsAndConditionsAgreedAt = new Date().toISOString()
|
||||
}
|
||||
params = await fileUpload(params, { file: 'avatarUpload', url: 'avatar' })
|
||||
const session = context.driver.session()
|
||||
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
@ -156,14 +157,18 @@ export default {
|
||||
MATCH (user:User {id: $params.id})
|
||||
SET user += $params
|
||||
SET user.updatedAt = toString(datetime())
|
||||
RETURN user
|
||||
RETURN user {.*}
|
||||
`,
|
||||
{ params },
|
||||
)
|
||||
return updateUserTransactionResponse.records.map(record => record.get('user').properties)
|
||||
const [user] = updateUserTransactionResponse.records.map(record => record.get('user'))
|
||||
if (avatarInput) {
|
||||
await mergeImage(user, 'AVATAR_IMAGE', avatarInput, { transaction })
|
||||
}
|
||||
return user
|
||||
})
|
||||
try {
|
||||
const [user] = await writeTxResultPromise
|
||||
const user = await writeTxResultPromise
|
||||
await createOrUpdateLocations(params.id, params.locationName, session)
|
||||
return user
|
||||
} catch (error) {
|
||||
@ -173,51 +178,67 @@ export default {
|
||||
}
|
||||
},
|
||||
DeleteUser: async (object, params, context, resolveInfo) => {
|
||||
const { resource, id } = params
|
||||
const { resource, id: userId } = params
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
|
||||
const deleteUserTxResultPromise = session.writeTransaction(async transaction => {
|
||||
if (resource && resource.length) {
|
||||
await session.writeTransaction(transaction => {
|
||||
resource.map(node => {
|
||||
return transaction.run(
|
||||
await Promise.all(
|
||||
resource.map(async node => {
|
||||
const txResult = await transaction.run(
|
||||
`
|
||||
MATCH (resource:${node})<-[:WROTE]-(author:User {id: $userId})
|
||||
OPTIONAL MATCH (resource)<-[:COMMENTS]-(comment:Comment)
|
||||
SET resource.deleted = true
|
||||
SET resource.content = 'UNAVAILABLE'
|
||||
SET resource.contentExcerpt = 'UNAVAILABLE'
|
||||
SET comment.deleted = true
|
||||
RETURN author
|
||||
`,
|
||||
MATCH (resource:${node})<-[:WROTE]-(author:User {id: $userId})
|
||||
OPTIONAL MATCH (resource)<-[:COMMENTS]-(comment:Comment)
|
||||
SET resource.deleted = true
|
||||
SET resource.content = 'UNAVAILABLE'
|
||||
SET resource.contentExcerpt = 'UNAVAILABLE'
|
||||
SET resource.language = 'UNAVAILABLE'
|
||||
SET resource.createdAt = 'UNAVAILABLE'
|
||||
SET resource.updatedAt = 'UNAVAILABLE'
|
||||
SET comment.deleted = true
|
||||
RETURN resource {.*}
|
||||
`,
|
||||
{
|
||||
userId: id,
|
||||
userId,
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
return Promise.all(
|
||||
txResult.records
|
||||
.map(record => record.get('resource'))
|
||||
.map(resource => deleteImage(resource, 'HERO_IMAGE', { transaction })),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const deleteUserTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const deleteUserTransactionResponse = await transaction.run(
|
||||
`
|
||||
const deleteUserTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (user:User {id: $userId})
|
||||
SET user.deleted = true
|
||||
SET user.name = 'UNAVAILABLE'
|
||||
SET user.about = 'UNAVAILABLE'
|
||||
SET user.lastActiveAt = 'UNAVAILABLE'
|
||||
SET user.createdAt = 'UNAVAILABLE'
|
||||
SET user.updatedAt = 'UNAVAILABLE'
|
||||
SET user.termsAndConditionsAgreedVersion = 'UNAVAILABLE'
|
||||
SET user.encryptedPassword = null
|
||||
WITH user
|
||||
OPTIONAL MATCH (user)<-[:BELONGS_TO]-(email:EmailAddress)
|
||||
DETACH DELETE email
|
||||
WITH user
|
||||
OPTIONAL MATCH (user)<-[:OWNED_BY]-(socialMedia:SocialMedia)
|
||||
DETACH DELETE socialMedia
|
||||
RETURN user
|
||||
RETURN user {.*}
|
||||
`,
|
||||
{ userId: id },
|
||||
)
|
||||
log(deleteUserTransactionResponse)
|
||||
return deleteUserTransactionResponse.records.map(record => record.get('user').properties)
|
||||
})
|
||||
const [user] = await deleteUserTxResultPromise
|
||||
{ userId },
|
||||
)
|
||||
log(deleteUserTransactionResponse)
|
||||
const [user] = deleteUserTransactionResponse.records.map(record => record.get('user'))
|
||||
await deleteImage(user, 'AVATAR_IMAGE', { transaction })
|
||||
return user
|
||||
})
|
||||
try {
|
||||
const user = await deleteUserTxResultPromise
|
||||
return user
|
||||
} finally {
|
||||
session.close()
|
||||
@ -236,8 +257,6 @@ export default {
|
||||
...Resolver('User', {
|
||||
undefinedToNull: [
|
||||
'actorId',
|
||||
'avatar',
|
||||
'coverImg',
|
||||
'deleted',
|
||||
'disabled',
|
||||
'locationName',
|
||||
@ -271,6 +290,7 @@ export default {
|
||||
badgesCount: '<-[:REWARDED]-(related:Badge)',
|
||||
},
|
||||
hasOne: {
|
||||
avatar: '-[:AVATAR_IMAGE]->(related:Image)',
|
||||
invitedBy: '<-[:INVITED]-(related:User)',
|
||||
location: '-[:IS_IN]->(related:Location)',
|
||||
},
|
||||
|
||||
@ -29,7 +29,7 @@ beforeAll(() => {
|
||||
mutate = createTestClient(server).mutate
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
beforeEach(async () => {
|
||||
await cleanDatabase()
|
||||
})
|
||||
|
||||
@ -273,189 +273,142 @@ describe('DeleteUser', () => {
|
||||
}
|
||||
}
|
||||
`
|
||||
beforeEach(async () => {
|
||||
variables = { id: ' u343', resource: [] }
|
||||
describe('as another user', () => {
|
||||
beforeEach(async () => {
|
||||
variables = { id: ' u343', resource: [] }
|
||||
|
||||
user = await Factory.build('user', {
|
||||
name: 'My name should be deleted',
|
||||
about: 'along with my about',
|
||||
id: 'u343',
|
||||
user = await Factory.build('user', {
|
||||
name: 'My name should be deleted',
|
||||
about: 'along with my about',
|
||||
id: 'u343',
|
||||
})
|
||||
})
|
||||
await Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'not-my-account',
|
||||
},
|
||||
{
|
||||
email: 'friends-account@example.org',
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
beforeEach(async () => {
|
||||
const anotherUser = await Factory.build(
|
||||
'user',
|
||||
{
|
||||
role: 'user',
|
||||
},
|
||||
{
|
||||
email: 'user@example.org',
|
||||
password: '1234',
|
||||
},
|
||||
)
|
||||
|
||||
authenticatedUser = await anotherUser.toJson()
|
||||
})
|
||||
|
||||
it("an ordinary user has no authorization to delete another user's account", async () => {
|
||||
const { errors } = await mutate({ mutation: deleteUserMutation, variables })
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
describe('as moderator', () => {
|
||||
beforeEach(async () => {
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
variables = { id: ' u343', resource: [] }
|
||||
|
||||
describe("attempting to delete another user's account", () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, id: 'not-my-account' }
|
||||
})
|
||||
|
||||
it('throws an authorization error', async () => {
|
||||
const { errors } = await mutate({ mutation: deleteUserMutation, variables })
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
user = await Factory.build('user', {
|
||||
name: 'My name should be deleted',
|
||||
about: 'along with my about',
|
||||
id: 'u343',
|
||||
})
|
||||
})
|
||||
|
||||
describe('attempting to delete my own account', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, id: 'u343' }
|
||||
beforeEach(async () => {
|
||||
const moderator = await Factory.build(
|
||||
'user',
|
||||
{
|
||||
role: 'moderator',
|
||||
},
|
||||
{
|
||||
email: 'moderator@example.org',
|
||||
password: '1234',
|
||||
},
|
||||
)
|
||||
|
||||
authenticatedUser = await moderator.toJson()
|
||||
})
|
||||
|
||||
it('moderator is not allowed to delete other user accounts', async () => {
|
||||
const { errors } = await mutate({ mutation: deleteUserMutation, variables })
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('as admin', () => {
|
||||
beforeEach(async () => {
|
||||
variables = { id: ' u343', resource: [] }
|
||||
|
||||
user = await Factory.build('user', {
|
||||
name: 'My name should be deleted',
|
||||
about: 'along with my about',
|
||||
id: 'u343',
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated as Admin', () => {
|
||||
beforeEach(async () => {
|
||||
const admin = await Factory.build(
|
||||
'user',
|
||||
{
|
||||
role: 'admin',
|
||||
},
|
||||
{
|
||||
email: 'admin@example.org',
|
||||
password: '1234',
|
||||
},
|
||||
)
|
||||
authenticatedUser = await admin.toJson()
|
||||
})
|
||||
|
||||
describe('given posts and comments', () => {
|
||||
beforeEach(async () => {
|
||||
await Factory.build('category', {
|
||||
id: 'cat9',
|
||||
name: 'Democracy & Politics',
|
||||
icon: 'university',
|
||||
})
|
||||
await Factory.build(
|
||||
'post',
|
||||
{
|
||||
id: 'p139',
|
||||
content: 'Post by user u343',
|
||||
},
|
||||
{
|
||||
author: user,
|
||||
categoryIds,
|
||||
},
|
||||
)
|
||||
await Factory.build(
|
||||
'comment',
|
||||
{
|
||||
id: 'c155',
|
||||
content: 'Comment by user u343',
|
||||
},
|
||||
{
|
||||
author: user,
|
||||
},
|
||||
)
|
||||
await Factory.build(
|
||||
'comment',
|
||||
{
|
||||
id: 'c156',
|
||||
content: "A comment by someone else on user u343's post",
|
||||
},
|
||||
{
|
||||
postId: 'p139',
|
||||
},
|
||||
)
|
||||
describe('deleting a user account', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, id: 'u343' }
|
||||
})
|
||||
|
||||
it("deletes my account, but doesn't delete posts or comments by default", async () => {
|
||||
const expectedResponse = {
|
||||
data: {
|
||||
DeleteUser: {
|
||||
id: 'u343',
|
||||
name: 'UNAVAILABLE',
|
||||
about: 'UNAVAILABLE',
|
||||
deleted: true,
|
||||
contributions: [
|
||||
{
|
||||
id: 'p139',
|
||||
content: 'Post by user u343',
|
||||
contentExcerpt: 'Post by user u343',
|
||||
deleted: false,
|
||||
comments: [
|
||||
{
|
||||
id: 'c156',
|
||||
content: "A comment by someone else on user u343's post",
|
||||
contentExcerpt: "A comment by someone else on user u343's post",
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
comments: [
|
||||
{
|
||||
id: 'c155',
|
||||
content: 'Comment by user u343',
|
||||
contentExcerpt: 'Comment by user u343',
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: deleteUserMutation, variables })).resolves.toMatchObject(
|
||||
expectedResponse,
|
||||
)
|
||||
})
|
||||
|
||||
describe('deletion of all post requested', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, resource: ['Post'] }
|
||||
})
|
||||
|
||||
describe("marks user's posts as deleted", () => {
|
||||
it('posts on request', async () => {
|
||||
const expectedResponse = {
|
||||
data: {
|
||||
DeleteUser: {
|
||||
id: 'u343',
|
||||
name: 'UNAVAILABLE',
|
||||
about: 'UNAVAILABLE',
|
||||
deleted: true,
|
||||
contributions: [
|
||||
{
|
||||
id: 'p139',
|
||||
content: 'UNAVAILABLE',
|
||||
contentExcerpt: 'UNAVAILABLE',
|
||||
deleted: true,
|
||||
comments: [
|
||||
{
|
||||
id: 'c156',
|
||||
content: 'UNAVAILABLE',
|
||||
contentExcerpt: 'UNAVAILABLE',
|
||||
deleted: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
comments: [
|
||||
{
|
||||
id: 'c155',
|
||||
content: 'Comment by user u343',
|
||||
contentExcerpt: 'Comment by user u343',
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(
|
||||
mutate({ mutation: deleteUserMutation, variables }),
|
||||
).resolves.toMatchObject(expectedResponse)
|
||||
describe('given posts and comments', () => {
|
||||
beforeEach(async () => {
|
||||
await Factory.build('category', {
|
||||
id: 'cat9',
|
||||
name: 'Democracy & Politics',
|
||||
icon: 'university',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('deletion of all comments requested', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, resource: ['Comment'] }
|
||||
await Factory.build(
|
||||
'post',
|
||||
{
|
||||
id: 'p139',
|
||||
content: 'Post by user u343',
|
||||
},
|
||||
{
|
||||
author: user,
|
||||
categoryIds,
|
||||
},
|
||||
)
|
||||
await Factory.build(
|
||||
'comment',
|
||||
{
|
||||
id: 'c155',
|
||||
content: 'Comment by user u343',
|
||||
},
|
||||
{
|
||||
author: user,
|
||||
},
|
||||
)
|
||||
await Factory.build(
|
||||
'comment',
|
||||
{
|
||||
id: 'c156',
|
||||
content: "A comment by someone else on user u343's post",
|
||||
},
|
||||
{
|
||||
postId: 'p139',
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
it('marks comments as deleted', async () => {
|
||||
it("deletes account, but doesn't delete posts or comments by default", async () => {
|
||||
const expectedResponse = {
|
||||
data: {
|
||||
DeleteUser: {
|
||||
@ -482,9 +435,9 @@ describe('DeleteUser', () => {
|
||||
comments: [
|
||||
{
|
||||
id: 'c155',
|
||||
content: 'UNAVAILABLE',
|
||||
contentExcerpt: 'UNAVAILABLE',
|
||||
deleted: true,
|
||||
content: 'Comment by user u343',
|
||||
contentExcerpt: 'Comment by user u343',
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -495,14 +448,263 @@ describe('DeleteUser', () => {
|
||||
mutate({ mutation: deleteUserMutation, variables }),
|
||||
).resolves.toMatchObject(expectedResponse)
|
||||
})
|
||||
})
|
||||
|
||||
describe('deletion of all post and comments requested', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, resource: ['Post', 'Comment'] }
|
||||
describe('deletion of all post requested', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, resource: ['Post'] }
|
||||
})
|
||||
|
||||
describe("marks user's posts as deleted", () => {
|
||||
it('on request', async () => {
|
||||
const expectedResponse = {
|
||||
data: {
|
||||
DeleteUser: {
|
||||
id: 'u343',
|
||||
name: 'UNAVAILABLE',
|
||||
about: 'UNAVAILABLE',
|
||||
deleted: true,
|
||||
contributions: [
|
||||
{
|
||||
id: 'p139',
|
||||
content: 'UNAVAILABLE',
|
||||
contentExcerpt: 'UNAVAILABLE',
|
||||
deleted: true,
|
||||
comments: [
|
||||
{
|
||||
id: 'c156',
|
||||
content: 'UNAVAILABLE',
|
||||
contentExcerpt: 'UNAVAILABLE',
|
||||
deleted: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
comments: [
|
||||
{
|
||||
id: 'c155',
|
||||
content: 'Comment by user u343',
|
||||
contentExcerpt: 'Comment by user u343',
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(
|
||||
mutate({ mutation: deleteUserMutation, variables }),
|
||||
).resolves.toMatchObject(expectedResponse)
|
||||
})
|
||||
|
||||
it('deletes user avatar and post hero images', async () => {
|
||||
await expect(neode.all('Image')).resolves.toHaveLength(22)
|
||||
await mutate({ mutation: deleteUserMutation, variables })
|
||||
await expect(neode.all('Image')).resolves.toHaveLength(20)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('marks posts and comments as deleted', async () => {
|
||||
describe('deletion of all comments requested', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, resource: ['Comment'] }
|
||||
})
|
||||
|
||||
it('marks comments as deleted', async () => {
|
||||
const expectedResponse = {
|
||||
data: {
|
||||
DeleteUser: {
|
||||
id: 'u343',
|
||||
name: 'UNAVAILABLE',
|
||||
about: 'UNAVAILABLE',
|
||||
deleted: true,
|
||||
contributions: [
|
||||
{
|
||||
id: 'p139',
|
||||
content: 'Post by user u343',
|
||||
contentExcerpt: 'Post by user u343',
|
||||
deleted: false,
|
||||
comments: [
|
||||
{
|
||||
id: 'c156',
|
||||
content: "A comment by someone else on user u343's post",
|
||||
contentExcerpt: "A comment by someone else on user u343's post",
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
comments: [
|
||||
{
|
||||
id: 'c155',
|
||||
content: 'UNAVAILABLE',
|
||||
contentExcerpt: 'UNAVAILABLE',
|
||||
deleted: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(
|
||||
mutate({ mutation: deleteUserMutation, variables }),
|
||||
).resolves.toMatchObject(expectedResponse)
|
||||
})
|
||||
})
|
||||
|
||||
describe('deletion of all posts and comments requested', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, resource: ['Post', 'Comment'] }
|
||||
})
|
||||
|
||||
it('marks posts and comments as deleted', async () => {
|
||||
const expectedResponse = {
|
||||
data: {
|
||||
DeleteUser: {
|
||||
id: 'u343',
|
||||
name: 'UNAVAILABLE',
|
||||
about: 'UNAVAILABLE',
|
||||
deleted: true,
|
||||
contributions: [
|
||||
{
|
||||
id: 'p139',
|
||||
content: 'UNAVAILABLE',
|
||||
contentExcerpt: 'UNAVAILABLE',
|
||||
deleted: true,
|
||||
comments: [
|
||||
{
|
||||
id: 'c156',
|
||||
content: 'UNAVAILABLE',
|
||||
contentExcerpt: 'UNAVAILABLE',
|
||||
deleted: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
comments: [
|
||||
{
|
||||
id: 'c155',
|
||||
content: 'UNAVAILABLE',
|
||||
contentExcerpt: 'UNAVAILABLE',
|
||||
deleted: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(
|
||||
mutate({ mutation: deleteUserMutation, variables }),
|
||||
).resolves.toMatchObject(expectedResponse)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('connected `EmailAddress` nodes', () => {
|
||||
it('will be removed completely', async () => {
|
||||
await expect(neode.all('EmailAddress')).resolves.toHaveLength(2)
|
||||
await mutate({ mutation: deleteUserMutation, variables })
|
||||
await expect(neode.all('EmailAddress')).resolves.toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('connected `SocialMedia` nodes', () => {
|
||||
beforeEach(async () => {
|
||||
const socialMedia = await Factory.build('socialMedia')
|
||||
await socialMedia.relateTo(user, 'ownedBy')
|
||||
})
|
||||
|
||||
it('will be removed completely', async () => {
|
||||
await expect(neode.all('SocialMedia')).resolves.toHaveLength(1)
|
||||
await mutate({ mutation: deleteUserMutation, variables })
|
||||
await expect(neode.all('SocialMedia')).resolves.toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('user deletes his own account', () => {
|
||||
beforeEach(async () => {
|
||||
variables = { id: 'u343', resource: [] }
|
||||
|
||||
user = await Factory.build('user', {
|
||||
name: 'My name should be deleted',
|
||||
about: 'along with my about',
|
||||
id: 'u343',
|
||||
})
|
||||
await Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'not-my-account',
|
||||
},
|
||||
{
|
||||
email: 'friends-account@example.org',
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
beforeEach(async () => {
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
describe("attempting to delete another user's account", () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, id: 'not-my-account' }
|
||||
})
|
||||
|
||||
it('throws an authorization error', async () => {
|
||||
const { errors } = await mutate({ mutation: deleteUserMutation, variables })
|
||||
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('attempting to delete my own account', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, id: 'u343' }
|
||||
})
|
||||
|
||||
describe('given posts and comments', () => {
|
||||
beforeEach(async () => {
|
||||
await Factory.build('category', {
|
||||
id: 'cat9',
|
||||
name: 'Democracy & Politics',
|
||||
icon: 'university',
|
||||
})
|
||||
await Factory.build(
|
||||
'post',
|
||||
{
|
||||
id: 'p139',
|
||||
content: 'Post by user u343',
|
||||
},
|
||||
{
|
||||
author: user,
|
||||
categoryIds,
|
||||
},
|
||||
)
|
||||
await Factory.build(
|
||||
'comment',
|
||||
{
|
||||
id: 'c155',
|
||||
content: 'Comment by user u343',
|
||||
},
|
||||
{
|
||||
author: user,
|
||||
},
|
||||
)
|
||||
await Factory.build(
|
||||
'comment',
|
||||
{
|
||||
id: 'c156',
|
||||
content: "A comment by someone else on user u343's post",
|
||||
},
|
||||
{
|
||||
postId: 'p139',
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
it("deletes my account, but doesn't delete posts or comments by default", async () => {
|
||||
const expectedResponse = {
|
||||
data: {
|
||||
DeleteUser: {
|
||||
@ -513,15 +715,15 @@ describe('DeleteUser', () => {
|
||||
contributions: [
|
||||
{
|
||||
id: 'p139',
|
||||
content: 'UNAVAILABLE',
|
||||
contentExcerpt: 'UNAVAILABLE',
|
||||
deleted: true,
|
||||
content: 'Post by user u343',
|
||||
contentExcerpt: 'Post by user u343',
|
||||
deleted: false,
|
||||
comments: [
|
||||
{
|
||||
id: 'c156',
|
||||
content: 'UNAVAILABLE',
|
||||
contentExcerpt: 'UNAVAILABLE',
|
||||
deleted: true,
|
||||
content: "A comment by someone else on user u343's post",
|
||||
contentExcerpt: "A comment by someone else on user u343's post",
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -529,9 +731,9 @@ describe('DeleteUser', () => {
|
||||
comments: [
|
||||
{
|
||||
id: 'c155',
|
||||
content: 'UNAVAILABLE',
|
||||
contentExcerpt: 'UNAVAILABLE',
|
||||
deleted: true,
|
||||
content: 'Comment by user u343',
|
||||
contentExcerpt: 'Comment by user u343',
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -542,28 +744,176 @@ describe('DeleteUser', () => {
|
||||
mutate({ mutation: deleteUserMutation, variables }),
|
||||
).resolves.toMatchObject(expectedResponse)
|
||||
})
|
||||
|
||||
describe('deletion of all post requested', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, resource: ['Post'] }
|
||||
})
|
||||
|
||||
describe("marks user's posts as deleted", () => {
|
||||
it('posts on request', async () => {
|
||||
const expectedResponse = {
|
||||
data: {
|
||||
DeleteUser: {
|
||||
id: 'u343',
|
||||
name: 'UNAVAILABLE',
|
||||
about: 'UNAVAILABLE',
|
||||
deleted: true,
|
||||
contributions: [
|
||||
{
|
||||
id: 'p139',
|
||||
content: 'UNAVAILABLE',
|
||||
contentExcerpt: 'UNAVAILABLE',
|
||||
deleted: true,
|
||||
comments: [
|
||||
{
|
||||
id: 'c156',
|
||||
content: 'UNAVAILABLE',
|
||||
contentExcerpt: 'UNAVAILABLE',
|
||||
deleted: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
comments: [
|
||||
{
|
||||
id: 'c155',
|
||||
content: 'Comment by user u343',
|
||||
contentExcerpt: 'Comment by user u343',
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(
|
||||
mutate({ mutation: deleteUserMutation, variables }),
|
||||
).resolves.toMatchObject(expectedResponse)
|
||||
})
|
||||
|
||||
it('deletes user avatar and post hero images', async () => {
|
||||
await expect(neode.all('Image')).resolves.toHaveLength(22)
|
||||
await mutate({ mutation: deleteUserMutation, variables })
|
||||
await expect(neode.all('Image')).resolves.toHaveLength(20)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('deletion of all comments requested', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, resource: ['Comment'] }
|
||||
})
|
||||
|
||||
it('marks comments as deleted', async () => {
|
||||
const expectedResponse = {
|
||||
data: {
|
||||
DeleteUser: {
|
||||
id: 'u343',
|
||||
name: 'UNAVAILABLE',
|
||||
about: 'UNAVAILABLE',
|
||||
deleted: true,
|
||||
contributions: [
|
||||
{
|
||||
id: 'p139',
|
||||
content: 'Post by user u343',
|
||||
contentExcerpt: 'Post by user u343',
|
||||
deleted: false,
|
||||
comments: [
|
||||
{
|
||||
id: 'c156',
|
||||
content: "A comment by someone else on user u343's post",
|
||||
contentExcerpt: "A comment by someone else on user u343's post",
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
comments: [
|
||||
{
|
||||
id: 'c155',
|
||||
content: 'UNAVAILABLE',
|
||||
contentExcerpt: 'UNAVAILABLE',
|
||||
deleted: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(
|
||||
mutate({ mutation: deleteUserMutation, variables }),
|
||||
).resolves.toMatchObject(expectedResponse)
|
||||
})
|
||||
})
|
||||
describe('deletion of all post and comments requested', () => {
|
||||
beforeEach(() => {
|
||||
variables = { ...variables, resource: ['Post', 'Comment'] }
|
||||
})
|
||||
|
||||
it('marks posts and comments as deleted', async () => {
|
||||
const expectedResponse = {
|
||||
data: {
|
||||
DeleteUser: {
|
||||
id: 'u343',
|
||||
name: 'UNAVAILABLE',
|
||||
about: 'UNAVAILABLE',
|
||||
deleted: true,
|
||||
contributions: [
|
||||
{
|
||||
id: 'p139',
|
||||
content: 'UNAVAILABLE',
|
||||
contentExcerpt: 'UNAVAILABLE',
|
||||
deleted: true,
|
||||
comments: [
|
||||
{
|
||||
id: 'c156',
|
||||
content: 'UNAVAILABLE',
|
||||
contentExcerpt: 'UNAVAILABLE',
|
||||
deleted: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
comments: [
|
||||
{
|
||||
id: 'c155',
|
||||
content: 'UNAVAILABLE',
|
||||
contentExcerpt: 'UNAVAILABLE',
|
||||
deleted: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(
|
||||
mutate({ mutation: deleteUserMutation, variables }),
|
||||
).resolves.toMatchObject(expectedResponse)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('connected `EmailAddress` nodes', () => {
|
||||
it('will be removed completely', async () => {
|
||||
await expect(neode.all('EmailAddress')).resolves.toHaveLength(2)
|
||||
await mutate({ mutation: deleteUserMutation, variables })
|
||||
await expect(neode.all('EmailAddress')).resolves.toHaveLength(1)
|
||||
})
|
||||
describe('connected `EmailAddress` nodes', () => {
|
||||
it('will be removed completely', async () => {
|
||||
await expect(neode.all('EmailAddress')).resolves.toHaveLength(2)
|
||||
await mutate({ mutation: deleteUserMutation, variables })
|
||||
await expect(neode.all('EmailAddress')).resolves.toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('connected `SocialMedia` nodes', () => {
|
||||
beforeEach(async () => {
|
||||
const socialMedia = await Factory.build('socialMedia')
|
||||
await socialMedia.relateTo(user, 'ownedBy')
|
||||
})
|
||||
|
||||
describe('connected `SocialMedia` nodes', () => {
|
||||
beforeEach(async () => {
|
||||
const socialMedia = await Factory.build('socialMedia')
|
||||
await socialMedia.relateTo(user, 'ownedBy')
|
||||
})
|
||||
|
||||
it('will be removed completely', async () => {
|
||||
await expect(neode.all('SocialMedia')).resolves.toHaveLength(1)
|
||||
await mutate({ mutation: deleteUserMutation, variables })
|
||||
await expect(neode.all('SocialMedia')).resolves.toHaveLength(0)
|
||||
})
|
||||
it('will be removed completely', async () => {
|
||||
await expect(neode.all('SocialMedia')).resolves.toHaveLength(1)
|
||||
await mutate({ mutation: deleteUserMutation, variables })
|
||||
await expect(neode.all('SocialMedia')).resolves.toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -8,28 +8,6 @@ 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) {
|
||||
@ -38,51 +16,51 @@ const updateUserMutation = gql`
|
||||
}
|
||||
`
|
||||
|
||||
let newlyCreatedNodesWithLocales = [
|
||||
const newlyCreatedNodesWithLocales = [
|
||||
{
|
||||
city: {
|
||||
lat: 41.1534,
|
||||
id: expect.stringContaining('place'),
|
||||
type: 'place',
|
||||
name: 'Hamburg',
|
||||
nameEN: 'Hamburg',
|
||||
nameDE: 'Hamburg',
|
||||
namePT: 'Hamburg',
|
||||
nameES: 'Hamburg',
|
||||
nameFR: 'Hamburg',
|
||||
nameIT: 'Hamburg',
|
||||
nameEN: 'Hamburg',
|
||||
type: 'place',
|
||||
namePT: 'Hamburg',
|
||||
nameRU: 'Хамбург',
|
||||
nameDE: 'Hamburg',
|
||||
nameNL: 'Hamburg',
|
||||
name: 'Hamburg',
|
||||
namePL: 'Hamburg',
|
||||
id: 'place.5977106083398860',
|
||||
lng: -74.5763,
|
||||
lat: 41.1534,
|
||||
},
|
||||
state: {
|
||||
namePT: 'Nova Jérsia',
|
||||
nameRU: 'Нью-Джерси',
|
||||
nameDE: 'New Jersey',
|
||||
nameNL: 'New Jersey',
|
||||
nameES: 'Nueva Jersey',
|
||||
id: expect.stringContaining('region'),
|
||||
type: 'region',
|
||||
name: 'New Jersey',
|
||||
namePL: 'New Jersey',
|
||||
nameEN: 'New Jersey',
|
||||
nameDE: 'New Jersey',
|
||||
namePT: 'Nova Jérsia',
|
||||
nameES: 'Nueva Jersey',
|
||||
nameFR: 'New Jersey',
|
||||
nameIT: 'New Jersey',
|
||||
id: 'region.14919479731700330',
|
||||
nameEN: 'New Jersey',
|
||||
type: 'region',
|
||||
nameRU: 'Нью-Джерси',
|
||||
nameNL: 'New Jersey',
|
||||
namePL: 'New Jersey',
|
||||
},
|
||||
country: {
|
||||
namePT: 'Estados Unidos',
|
||||
nameRU: 'Соединённые Штаты Америки',
|
||||
nameDE: 'Vereinigte Staaten',
|
||||
nameNL: 'Verenigde Staten van Amerika',
|
||||
nameES: 'Estados Unidos',
|
||||
namePL: 'Stany Zjednoczone',
|
||||
id: expect.stringContaining('country'),
|
||||
type: 'country',
|
||||
name: 'United States of America',
|
||||
nameEN: 'United States of America',
|
||||
nameDE: 'Vereinigte Staaten',
|
||||
namePT: 'Estados Unidos',
|
||||
nameES: 'Estados Unidos',
|
||||
nameFR: 'États-Unis',
|
||||
nameIT: "Stati Uniti d'America",
|
||||
id: 'country.9053006287256050',
|
||||
nameEN: 'United States of America',
|
||||
type: 'country',
|
||||
nameRU: 'Соединённые Штаты Америки',
|
||||
nameNL: 'Verenigde Staten van Amerika',
|
||||
namePL: 'Stany Zjednoczone',
|
||||
},
|
||||
},
|
||||
]
|
||||
@ -105,82 +83,12 @@ beforeEach(() => {
|
||||
authenticatedUser = null
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanDatabase()
|
||||
})
|
||||
afterEach(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
|
||||
beforeEach(async () => {
|
||||
newlyCreatedNodesWithLocales = [
|
||||
{
|
||||
city: {
|
||||
lat: 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',
|
||||
lng: 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',
|
||||
},
|
||||
},
|
||||
]
|
||||
user = await Factory.build('user', {
|
||||
id: 'updating-user',
|
||||
})
|
||||
@ -192,17 +100,18 @@ describe('userMiddleware', () => {
|
||||
...variables,
|
||||
id: 'updating-user',
|
||||
name: 'Updating user',
|
||||
locationName: 'Hamburg, Germany',
|
||||
locationName: 'Hamburg, New Jersey, United States of America',
|
||||
}
|
||||
await mutate({ mutation: updateUserMutation, variables })
|
||||
const locations = await neode.cypher(
|
||||
`MATCH (city:Location)-[:IS_IN]->(country:Location) return city, country`,
|
||||
`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,
|
||||
country: record.get('country').properties,
|
||||
city: record.get('city'),
|
||||
state: record.get('state'),
|
||||
country: record.get('country'),
|
||||
}
|
||||
}),
|
||||
).toEqual(newlyCreatedNodesWithLocales)
|
||||
|
||||
@ -9,14 +9,10 @@ type Mutation {
|
||||
SignupByInvitation(email: String!, token: String!): EmailAddress
|
||||
SignupVerification(
|
||||
nonce: String!
|
||||
name: String!
|
||||
email: String!
|
||||
name: String!
|
||||
password: String!
|
||||
slug: String
|
||||
avatar: String
|
||||
coverImg: String
|
||||
avatarUpload: Upload
|
||||
locationName: String
|
||||
about: String
|
||||
termsAndConditionsAgreedVersion: String!
|
||||
locale: String
|
||||
|
||||
18
backend/src/schema/types/type/Image.gql
Normal file
18
backend/src/schema/types/type/Image.gql
Normal file
@ -0,0 +1,18 @@
|
||||
type Image {
|
||||
url: ID!,
|
||||
# urlW34: String,
|
||||
# urlW160: String,
|
||||
# urlW320: String,
|
||||
# urlW640: String,
|
||||
# urlW1024: String,
|
||||
alt: String,
|
||||
sensitive: Boolean,
|
||||
aspectRatio: Float,
|
||||
}
|
||||
|
||||
input ImageInput {
|
||||
alt: String,
|
||||
upload: Upload,
|
||||
sensitive: Boolean,
|
||||
aspectRatio: Float,
|
||||
}
|
||||
@ -40,7 +40,6 @@ input _PostFilter {
|
||||
content_not_starts_with: String
|
||||
content_ends_with: String
|
||||
content_not_ends_with: String
|
||||
image: String
|
||||
visibility: Visibility
|
||||
visibility_not: Visibility
|
||||
visibility_in: [Visibility!]
|
||||
@ -82,7 +81,6 @@ input _PostFilter {
|
||||
emotions_none: _PostEMOTEDFilter
|
||||
emotions_single: _PostEMOTEDFilter
|
||||
emotions_every: _PostEMOTEDFilter
|
||||
imageBlurred: Boolean
|
||||
}
|
||||
|
||||
enum _PostOrdering {
|
||||
@ -94,8 +92,6 @@ enum _PostOrdering {
|
||||
slug_desc
|
||||
content_asc
|
||||
content_desc
|
||||
image_asc
|
||||
image_desc
|
||||
visibility_asc
|
||||
visibility_desc
|
||||
createdAt_asc
|
||||
@ -118,9 +114,7 @@ type Post {
|
||||
slug: String!
|
||||
content: String!
|
||||
contentExcerpt: String
|
||||
image: String
|
||||
imageUpload: Upload
|
||||
imageAspectRatio: Float
|
||||
image: Image @relation(name: "HERO_IMAGE", direction: "OUT")
|
||||
visibility: Visibility
|
||||
deleted: Boolean
|
||||
disabled: Boolean
|
||||
@ -128,7 +122,6 @@ type Post {
|
||||
createdAt: String
|
||||
updatedAt: String
|
||||
language: String
|
||||
imageBlurred: Boolean
|
||||
pinnedAt: String @cypher(
|
||||
statement: "MATCH (this)<-[pinned:PINNED]-(:User) WHERE NOT this.deleted = true AND NOT this.disabled = true RETURN pinned.createdAt"
|
||||
)
|
||||
@ -178,14 +171,11 @@ type Mutation {
|
||||
title: String!
|
||||
slug: String
|
||||
content: String!
|
||||
image: String
|
||||
imageUpload: Upload
|
||||
image: ImageInput,
|
||||
visibility: Visibility
|
||||
language: String
|
||||
categoryIds: [ID]
|
||||
contentExcerpt: String
|
||||
imageBlurred: Boolean
|
||||
imageAspectRatio: Float
|
||||
): Post
|
||||
UpdatePost(
|
||||
id: ID!
|
||||
@ -193,13 +183,10 @@ type Mutation {
|
||||
slug: String
|
||||
content: String!
|
||||
contentExcerpt: String
|
||||
image: String
|
||||
imageUpload: Upload
|
||||
image: ImageInput,
|
||||
visibility: Visibility
|
||||
language: String
|
||||
categoryIds: [ID]
|
||||
imageBlurred: Boolean
|
||||
imageAspectRatio: Float
|
||||
): Post
|
||||
DeletePost(id: ID!): Post
|
||||
AddPostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED
|
||||
@ -214,7 +201,6 @@ type Query {
|
||||
title: String
|
||||
slug: String
|
||||
content: String
|
||||
image: String
|
||||
visibility: Visibility
|
||||
pinned: Boolean
|
||||
createdAt: String
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
union SearchResult = Post | User
|
||||
union SearchResult = Post | User | Tag
|
||||
|
||||
type Query {
|
||||
findResources(query: String!, limit: Int = 5): [SearchResult]!
|
||||
|
||||
@ -5,10 +5,6 @@ enum _UserOrdering {
|
||||
name_desc
|
||||
slug_asc
|
||||
slug_desc
|
||||
avatar_asc
|
||||
avatar_desc
|
||||
coverImg_asc
|
||||
coverImg_desc
|
||||
role_asc
|
||||
role_desc
|
||||
locationName_asc
|
||||
@ -29,8 +25,7 @@ type User {
|
||||
name: String
|
||||
email: String! @cypher(statement: "MATCH (this)-[:PRIMARY_EMAIL]->(e:EmailAddress) RETURN e.email")
|
||||
slug: String!
|
||||
avatar: String
|
||||
coverImg: String
|
||||
avatar: Image @relation(name: "AVATAR_IMAGE", direction: "OUT")
|
||||
deleted: Boolean
|
||||
disabled: Boolean
|
||||
role: UserGroup!
|
||||
@ -161,8 +156,6 @@ type Query {
|
||||
email: String # admins need to search for a user sometimes
|
||||
name: String
|
||||
slug: String
|
||||
avatar: String
|
||||
coverImg: String
|
||||
role: UserGroup
|
||||
locationName: String
|
||||
about: String
|
||||
@ -203,9 +196,7 @@ type Mutation {
|
||||
name: String
|
||||
email: String
|
||||
slug: String
|
||||
avatar: String
|
||||
coverImg: String
|
||||
avatarUpload: Upload
|
||||
avatar: ImageInput
|
||||
locationName: String
|
||||
about: String
|
||||
termsAndConditionsAgreedVersion: String
|
||||
|
||||
@ -795,14 +795,7 @@
|
||||
core-js-pure "^3.0.0"
|
||||
regenerator-runtime "^0.13.2"
|
||||
|
||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.5.5":
|
||||
version "7.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.6.2.tgz#c3d6e41b304ef10dcf13777a33e7694ec4a9a6dd"
|
||||
integrity sha512-EXxN64agfUqqIGeEjI5dL5z0Sw0ZwWo1mLTi4mQowCZ42O59b7DRpZAnTC6OqdF28wMBMFKNb/4uFGrVaigSpg==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.2"
|
||||
|
||||
"@babel/runtime@^7.8.4":
|
||||
"@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
|
||||
version "7.8.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.7.tgz#8fefce9802db54881ba59f90bb28719b4996324d"
|
||||
integrity sha512-+AATMUFppJDw6aiR5NVPHqIQBlV/Pj8wY/EZH+lmvRdUo9xBaz/rF3alAwFJQavvKfeOlPE7oaaDHVbcySbCsg==
|
||||
@ -887,10 +880,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.2.tgz#1c794cd6dbf2354d1eb1ef10e0303f573e1c7222"
|
||||
integrity sha512-O4QDrx+JoGKZc6aN64L04vqa7e41tIiLU+OvKdcYaEMP97UttL0f9GIi9/0A4WAMx0uBd6SidDIhktZhgOcN8Q==
|
||||
|
||||
"@hapi/address@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-4.0.0.tgz#36affb4509b5a6adc628bcc394450f2a7d51d111"
|
||||
integrity sha512-GDDpkCdSUfkQCznmWUHh9dDN85BWf/V8TFKQ2JLuHdGB4Yy3YTEGBzZxoBNxfNBEvreSR/o+ZxBBSNNEVzY+lQ==
|
||||
"@hapi/address@^4.0.1":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-4.0.1.tgz#267301ddf7bc453718377a6fb3832a2f04a721dd"
|
||||
integrity sha512-0oEP5UiyV4f3d6cBL8F3Z5S7iWSX39Knnl0lY8i+6gfmmIBj44JCBNtcMgwyS+5v7j3VYavNay0NFHDS+UGQcw==
|
||||
dependencies:
|
||||
"@hapi/hoek" "^9.0.0"
|
||||
|
||||
@ -924,12 +917,12 @@
|
||||
"@hapi/hoek" "8.x.x"
|
||||
"@hapi/topo" "3.x.x"
|
||||
|
||||
"@hapi/joi@^17.1.0":
|
||||
version "17.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-17.1.0.tgz#cc4000b6c928a6a39b9bef092151b6bdee10ce55"
|
||||
integrity sha512-ob67RcPlwRWxBzLCnWvcwx5qbwf88I3ykD7gcJLWOTRfLLgosK7r6aeChz4thA3XRvuBfI0KB1tPVl2EQFlPXw==
|
||||
"@hapi/joi@^17.1.0", "@hapi/joi@^17.1.1":
|
||||
version "17.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-17.1.1.tgz#9cc8d7e2c2213d1e46708c6260184b447c661350"
|
||||
integrity sha512-p4DKeZAoeZW4g3u7ZeRo+vCDuSDgSvtsB/NpfjXEHTUjSeINAi/RrVOWiVQ1isaoLzMvFEhe8n5065mQq1AdQg==
|
||||
dependencies:
|
||||
"@hapi/address" "^4.0.0"
|
||||
"@hapi/address" "^4.0.1"
|
||||
"@hapi/formula" "^2.0.0"
|
||||
"@hapi/hoek" "^9.0.0"
|
||||
"@hapi/pinpoint" "^2.0.0"
|
||||
@ -1162,7 +1155,7 @@
|
||||
url-regex "~4.1.1"
|
||||
video-extensions "~1.1.0"
|
||||
|
||||
"@metascraper/helpers@^5.11.1", "@metascraper/helpers@^5.11.6":
|
||||
"@metascraper/helpers@^5.11.6":
|
||||
version "5.11.6"
|
||||
resolved "https://registry.yarnpkg.com/@metascraper/helpers/-/helpers-5.11.6.tgz#2fef2f420f06f4f8903cc6f699ccb79195950a60"
|
||||
integrity sha512-DKCJMz5Q4wrBPZVfJdeNarmW2WHm3Y7D6M78KKA/D0mcXPikKLoiBxjyPtjc5tEE/5er+PYFijDBmyTT60M2bg==
|
||||
@ -1262,83 +1255,83 @@
|
||||
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
|
||||
integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=
|
||||
|
||||
"@sentry/apm@5.13.1":
|
||||
version "5.13.1"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/apm/-/apm-5.13.1.tgz#152a7a54b06f344112477cb376e8554860a6af86"
|
||||
integrity sha512-be6M8/TOA/K7jQNZEm1YC0Y9+LdM0jyX5LMwy9NWwhneE6Iq8xvsU/pYZByj6+AAs0tIpiFd9QFxFKNUtKIRUQ==
|
||||
"@sentry/apm@5.14.2":
|
||||
version "5.14.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/apm/-/apm-5.14.2.tgz#b05b91a8da6826fdd20532cb745c0f9ef5c55656"
|
||||
integrity sha512-51yeQ04mKEsx2WiXbMlUSXhmG/D+YFiNJXxKuFopJkKkT02qr7B3QH0vHkS9OX2oniYoBTWZVCKYUAgJUSsIug==
|
||||
dependencies:
|
||||
"@sentry/browser" "5.13.0"
|
||||
"@sentry/hub" "5.13.0"
|
||||
"@sentry/minimal" "5.13.0"
|
||||
"@sentry/types" "5.12.4"
|
||||
"@sentry/utils" "5.13.0"
|
||||
"@sentry/browser" "5.14.2"
|
||||
"@sentry/hub" "5.14.2"
|
||||
"@sentry/minimal" "5.14.2"
|
||||
"@sentry/types" "5.14.2"
|
||||
"@sentry/utils" "5.14.2"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/browser@5.13.0":
|
||||
version "5.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.13.0.tgz#399b0a09d6603726d787b746bcc70659010bc50c"
|
||||
integrity sha512-adiW9gG/gCrl6FQAA6Fk8osXMHxP3pYltszRK0mr55O7GcTC8RQNI3mEW/YuQV9IySUL8dFWQ0v8n0lfssHf/A==
|
||||
"@sentry/browser@5.14.2":
|
||||
version "5.14.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.14.2.tgz#b0d1bf7bd771e64de0f9f801fa6625e47fced016"
|
||||
integrity sha512-Vuuy2E5mt2VQKeHpFqtowZdKUe1Ui7J2KmgZQCduVilM7dFmprdXfv/mQ3Uv+73VIiCd22PpxojR3peDksb/Gg==
|
||||
dependencies:
|
||||
"@sentry/core" "5.13.0"
|
||||
"@sentry/types" "5.12.4"
|
||||
"@sentry/utils" "5.13.0"
|
||||
"@sentry/core" "5.14.2"
|
||||
"@sentry/types" "5.14.2"
|
||||
"@sentry/utils" "5.14.2"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/core@5.13.0":
|
||||
version "5.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.13.0.tgz#144beb2d48b53244774a7fd809f9b5b672920971"
|
||||
integrity sha512-e0olbaHBmANO1RIBc7xynSkBZ6BsK7drycz0TawLUnx+0H3aEau3K9U2QVdbjwLNPdydcIS+UgYfTBtXfe0E+A==
|
||||
"@sentry/core@5.14.2":
|
||||
version "5.14.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.14.2.tgz#950709a2281086c64f1ba60f2c3290dc81c19659"
|
||||
integrity sha512-B2XjUMCmVu4H3s5hapgynhb28MSc+irt9wRI9j0Lbjx2cxsCUr/YFGL8GuEuYwf4zXNKnh2ke6t+I37OlSaGOg==
|
||||
dependencies:
|
||||
"@sentry/hub" "5.13.0"
|
||||
"@sentry/minimal" "5.13.0"
|
||||
"@sentry/types" "5.12.4"
|
||||
"@sentry/utils" "5.13.0"
|
||||
"@sentry/hub" "5.14.2"
|
||||
"@sentry/minimal" "5.14.2"
|
||||
"@sentry/types" "5.14.2"
|
||||
"@sentry/utils" "5.14.2"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/hub@5.13.0":
|
||||
version "5.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.13.0.tgz#f48e3e4e273f40316391cd6190e22ea69cb20c7e"
|
||||
integrity sha512-MeytooJ5g91zxq4/LU1LHj7KxpggAEn1dybEsWG31QVy67J4a40zIGfYgGGIVAFSv0WVlk5Ei5C159LhgW59/w==
|
||||
"@sentry/hub@5.14.2":
|
||||
version "5.14.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.14.2.tgz#24a0990a901d49f8a362dfd404cb7cd33e429d60"
|
||||
integrity sha512-0ckTDnhCANkuY+VepMPz5vl/dkFQnWmzlJiCIxgM5fCgAF8dfNd9VhGn0qVQXnzKPGoW9zxs/uAmH3/XFqqmNA==
|
||||
dependencies:
|
||||
"@sentry/types" "5.12.4"
|
||||
"@sentry/utils" "5.13.0"
|
||||
"@sentry/types" "5.14.2"
|
||||
"@sentry/utils" "5.14.2"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/minimal@5.13.0":
|
||||
version "5.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.13.0.tgz#ee906191e3c2a1f7d0925fbfa0a4e96261013764"
|
||||
integrity sha512-6D2Mu4TrmJmGlvb+z1Pp6yI2fUmdY1RvwK0MqmBP+QJdrd0as7cpGuwFSXgUs6CLUflDzlpn3n6WcgGV8oEDYA==
|
||||
"@sentry/minimal@5.14.2":
|
||||
version "5.14.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.14.2.tgz#9fa39cc6432a05aae22e892a1be3cc314c3b77c4"
|
||||
integrity sha512-uih9a8KwFCQrWaGb3UxkrSntxMRT4EIlud158ZKlrsLaCOE6i08unOR4xWqlrXlKPySq16H4wjbBFQ56ogOWdQ==
|
||||
dependencies:
|
||||
"@sentry/hub" "5.13.0"
|
||||
"@sentry/types" "5.12.4"
|
||||
"@sentry/hub" "5.14.2"
|
||||
"@sentry/types" "5.14.2"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/node@^5.13.1":
|
||||
version "5.13.1"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-5.13.1.tgz#41d2eec02bc718a0f5aa59698635242d585470f2"
|
||||
integrity sha512-6/HaewN2kX0za3LncYwp6nlvm/6i0S0/D/HO7VDHMSpc8z/8/Em6xTZy7hLV3phosMoLIa5P3CRXvLVybBTrpg==
|
||||
"@sentry/node@^5.14.2":
|
||||
version "5.14.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-5.14.2.tgz#18c25a0ca34b6ea4e3d917a819e97d086d8b1c2c"
|
||||
integrity sha512-8s9JAKc/oid6lIFbYLtCLDwLhUpsgeU1WdNbs1eUJQSArb6WHS6EREVBuGr3RMfe+SkwEMg1rtPKnyj4C/WRig==
|
||||
dependencies:
|
||||
"@sentry/apm" "5.13.1"
|
||||
"@sentry/core" "5.13.0"
|
||||
"@sentry/hub" "5.13.0"
|
||||
"@sentry/types" "5.12.4"
|
||||
"@sentry/utils" "5.13.0"
|
||||
"@sentry/apm" "5.14.2"
|
||||
"@sentry/core" "5.14.2"
|
||||
"@sentry/hub" "5.14.2"
|
||||
"@sentry/types" "5.14.2"
|
||||
"@sentry/utils" "5.14.2"
|
||||
cookie "^0.3.1"
|
||||
https-proxy-agent "^4.0.0"
|
||||
lru_map "^0.3.3"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/types@5.12.4":
|
||||
version "5.12.4"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.12.4.tgz#6e52639bc3b4e136e9a0da5385890f8f78bb7697"
|
||||
integrity sha512-JoN3YIp7Z+uxUZArj2B6NcEoXFQDhd0kqO0QpfiHZyg4Dhx2/E2aHuVx0H6Fndk+60iEZSECaCBXe2MOPo4fqA==
|
||||
"@sentry/types@5.14.2":
|
||||
version "5.14.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.14.2.tgz#43c3723b2f5b31234892fbe6a28b293ad050faac"
|
||||
integrity sha512-NtB/o+/whR/mJJf67Nvdab7E2+/THgAUY114FWFqDLHMaoiIVWy9J/yLKtQWymwuQslh7zpPxjA1AhqTJerVCg==
|
||||
|
||||
"@sentry/utils@5.13.0":
|
||||
version "5.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.13.0.tgz#6463e53b6178dbbd3b90e671517cbca82744b055"
|
||||
integrity sha512-BcmNQN+IfFbVWGnEwXHku69zqJc97sjBRYVxpStKMaO/4aLVIQcOJCMWxVJtVoSVAHQaigBZmFutWH7EJMRJxg==
|
||||
"@sentry/utils@5.14.2":
|
||||
version "5.14.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.14.2.tgz#2e812f2788a00ca4e6e35acbeb86000792f53473"
|
||||
integrity sha512-DV9/kw/O8o5xqvQYwITm0lBaBqS4RKicjguWYJQ/+F94P/SKxuXor7EE0iMDYvUGslvPz8TlgB7r+nb/YRl+Fg==
|
||||
dependencies:
|
||||
"@sentry/types" "5.12.4"
|
||||
"@sentry/types" "5.14.2"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sindresorhus/is@^0.14.0":
|
||||
@ -1594,10 +1587,10 @@
|
||||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@types/yup@0.26.32":
|
||||
version "0.26.32"
|
||||
resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.32.tgz#bd356fb405f3d641eff963854edf7ad854a8e829"
|
||||
integrity sha512-55WFAq8lNYXdRzSP1cenMFFXtPRe7PWsqn5y9ibqKHOQZ/cSLErkcnB1LE89M7W2TSXVDFtx+T7eFePkGoB+xw==
|
||||
"@types/yup@0.26.33":
|
||||
version "0.26.33"
|
||||
resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.33.tgz#301faab47b952a4a5f9a06246942cc7cbd09cd95"
|
||||
integrity sha512-QzgcNfDtRIph8CjfoWiu+MJiOUp25Yo7FthuOHLVbtCTyonjOo2YRsFzKo3csDWbTXlw5NedOFH0Nje7yipCrA==
|
||||
|
||||
"@types/zen-observable@^0.8.0":
|
||||
version "0.8.0"
|
||||
@ -1676,9 +1669,9 @@ acorn-walk@^6.0.1:
|
||||
integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==
|
||||
|
||||
acorn@^6.0.1:
|
||||
version "6.3.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.3.0.tgz#0087509119ffa4fc0a0041d1e93a417e68cb856e"
|
||||
integrity sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==
|
||||
version "6.4.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474"
|
||||
integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==
|
||||
|
||||
acorn@^7.1.0:
|
||||
version "7.1.0"
|
||||
@ -2979,10 +2972,10 @@ create-error-class@^3.0.0:
|
||||
dependencies:
|
||||
capture-stack-trace "^1.0.0"
|
||||
|
||||
cross-env@~7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.1.tgz#c8e03412ea0e1370fe3f0066929a70b8e1e90c39"
|
||||
integrity sha512-1+DmLosu38kC4s1H4HzNkcolwdANifu9+5bE6uKQCV4L6jvVdV9qdRAk8vV3GoWRe0x4z+K2fFhgoDMqwNsPqQ==
|
||||
cross-env@~7.0.2:
|
||||
version "7.0.2"
|
||||
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.2.tgz#bd5ed31339a93a3418ac4f3ca9ca3403082ae5f9"
|
||||
integrity sha512-KZP/bMEOJEDCkDQAyRhu3RL2ZO/SUVrxQVI0G3YEQ+OLbRA3c6zgixe8Mq8a/z7+HKlNEjo8oiLUs8iRijY2Rw==
|
||||
dependencies:
|
||||
cross-spawn "^7.0.1"
|
||||
|
||||
@ -3127,10 +3120,10 @@ data-urls@^1.1.0:
|
||||
whatwg-mimetype "^2.2.0"
|
||||
whatwg-url "^7.0.0"
|
||||
|
||||
date-fns@2.10.0:
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.10.0.tgz#abd10604d8bafb0bcbd2ba2e9b0563b922ae4b6b"
|
||||
integrity sha512-EhfEKevYGWhWlZbNeplfhIU/+N+x0iCIx7VzKlXma2EdQyznVlZhCptXUY+BegNpPW2kjdx15Rvq503YcXXrcA==
|
||||
date-fns@2.11.0:
|
||||
version "2.11.0"
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.11.0.tgz#ec2b44977465b9dcb370021d5e6c019b19f36d06"
|
||||
integrity sha512-8P1cDi8ebZyDxUyUprBXwidoEtiQAawYPGvpfb+Dg0G6JrQ+VozwOmm91xYC0vAv1+0VmLehEPb+isg4BGUFfA==
|
||||
|
||||
dateformat@^2.0.0:
|
||||
version "2.2.0"
|
||||
@ -3664,10 +3657,10 @@ eslint-plugin-import@~2.20.1:
|
||||
read-pkg-up "^2.0.0"
|
||||
resolve "^1.12.0"
|
||||
|
||||
eslint-plugin-jest@~23.8.1:
|
||||
version "23.8.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-23.8.1.tgz#247025e8a51b3a25a4cc41166369b0bfb4db83b7"
|
||||
integrity sha512-OycLNqPo/2EfO6kTqnmsu1khz1gTIOxGl3ThIVwL5/oycDF4pm5uNDyvFelNLdpr4COUuM8PVi3963NEG1Efpw==
|
||||
eslint-plugin-jest@~23.8.2:
|
||||
version "23.8.2"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-23.8.2.tgz#6f28b41c67ef635f803ebd9e168f6b73858eb8d4"
|
||||
integrity sha512-xwbnvOsotSV27MtAe7s8uGWOori0nUsrXh2f1EnpmXua8sDfY6VZhHAhHg2sqK7HBNycRQExF074XSZ7DvfoFg==
|
||||
dependencies:
|
||||
"@typescript-eslint/experimental-utils" "^2.5.0"
|
||||
|
||||
@ -4155,10 +4148,10 @@ flatted@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08"
|
||||
integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==
|
||||
|
||||
fn-name@~2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fn-name/-/fn-name-2.0.1.tgz#5214d7537a4d06a4a301c0cc262feb84188002e7"
|
||||
integrity sha1-UhTXU3pNBqSjAcDMJi/rhBiAAuc=
|
||||
fn-name@~3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fn-name/-/fn-name-3.0.0.tgz#0596707f635929634d791f452309ab41558e3c5c"
|
||||
integrity sha512-eNMNr5exLoavuAMhIUVsOKF79SWd/zG104ef6sxBTSw+cZc6BXdQXDvYcGvp0VbxVVSp1XDUNoz7mg1xMtSznA==
|
||||
|
||||
for-in@^1.0.2:
|
||||
version "1.0.2"
|
||||
@ -4505,14 +4498,14 @@ graphql-redis-subscriptions@^2.2.1:
|
||||
optionalDependencies:
|
||||
ioredis "^4.6.3"
|
||||
|
||||
graphql-shield@~7.0.14:
|
||||
version "7.0.14"
|
||||
resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-7.0.14.tgz#3cbbf2722f2e3393fed7f47d866a1324bc3ce76a"
|
||||
integrity sha512-YVedaL+4pITisSGRqMVeGX8ydOLSTQlHQN6o0Jly7z2cSy1wOzGJIRpfofETJtGLhBnPHHy1otINzuAyjGJO/g==
|
||||
graphql-shield@~7.2.1:
|
||||
version "7.2.1"
|
||||
resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-7.2.1.tgz#baca0c31a19593ede41a4bb4d476f9edd1d0eb78"
|
||||
integrity sha512-EEoVYvXuqAGXGH1i9Aot4MJIUlRABVycwKRGaBreabys7yTd+0zGwudKB9yZiW3SEMIjHkz3a0r/ILhYzq0uiw==
|
||||
dependencies:
|
||||
"@types/yup" "0.26.32"
|
||||
"@types/yup" "0.26.33"
|
||||
object-hash "^2.0.3"
|
||||
yup "^0.28.1"
|
||||
yup "^0.28.3"
|
||||
|
||||
graphql-subscriptions@^1.0.0:
|
||||
version "1.1.0"
|
||||
@ -6064,7 +6057,7 @@ lodash.isstring@^4.0.1:
|
||||
resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
|
||||
integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=
|
||||
|
||||
lodash.mergewith@^4.6.1:
|
||||
lodash.mergewith@^4.6.2:
|
||||
version "4.6.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55"
|
||||
integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==
|
||||
@ -6223,12 +6216,12 @@ merge2@^1.3.0:
|
||||
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81"
|
||||
integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==
|
||||
|
||||
metascraper-audio@^5.11.1:
|
||||
version "5.11.1"
|
||||
resolved "https://registry.yarnpkg.com/metascraper-audio/-/metascraper-audio-5.11.1.tgz#46a45fc8d9c4ccc1c24340d46a8c25dc3685d7b9"
|
||||
integrity sha512-L5eGfw5cOww4/f3ppMa/k+bix3LdICKcKJ2WVTLgz1QkKTWt5IQrgdW+kRfwUdaUTH6w0Tco+nOO7yUCaWytAQ==
|
||||
metascraper-audio@^5.11.6:
|
||||
version "5.11.6"
|
||||
resolved "https://registry.yarnpkg.com/metascraper-audio/-/metascraper-audio-5.11.6.tgz#392b4b84309ac017bce4b4d0d52948f3a17d7ecc"
|
||||
integrity sha512-X1nEPP+bgTUStXmWuy/s/h5dix2Smuphx8VdH47/uqXRkifGByQ4nHt9Rd+rg3BNOI15bCib/Nc56awtUOG+3Q==
|
||||
dependencies:
|
||||
"@metascraper/helpers" "^5.11.1"
|
||||
"@metascraper/helpers" "^5.11.6"
|
||||
|
||||
metascraper-author@^5.11.6:
|
||||
version "5.11.6"
|
||||
@ -6252,12 +6245,12 @@ metascraper-date@^5.11.6:
|
||||
dependencies:
|
||||
"@metascraper/helpers" "^5.11.6"
|
||||
|
||||
metascraper-description@^5.11.1:
|
||||
version "5.11.1"
|
||||
resolved "https://registry.yarnpkg.com/metascraper-description/-/metascraper-description-5.11.1.tgz#55a90f165e73dae9289fb922e5269f902cc57d6b"
|
||||
integrity sha512-xClk2kxYYeAY5yHgrUXQXiSz8I6qa3JNPietWIk4dtUQ+DxHyUOBo6B6pQmlbfX/BjCLh35m0srAqzZ7+r1s2g==
|
||||
metascraper-description@^5.11.6:
|
||||
version "5.11.6"
|
||||
resolved "https://registry.yarnpkg.com/metascraper-description/-/metascraper-description-5.11.6.tgz#515368b5ca88cb5fa1fd92c0946458795b03960f"
|
||||
integrity sha512-rGD6hEWLHPlZ/091htCoFAJGft2oRDAaAoDcnafMdTBcuukBIpZ3QNOR3rYpliWHbpS8cwiX2Q9IawyqB4iK0g==
|
||||
dependencies:
|
||||
"@metascraper/helpers" "^5.11.1"
|
||||
"@metascraper/helpers" "^5.11.6"
|
||||
|
||||
metascraper-image@^5.11.6:
|
||||
version "5.11.6"
|
||||
@ -6275,12 +6268,12 @@ metascraper-lang-detector@^4.10.2:
|
||||
franc "~4.0.0"
|
||||
iso-639-3 "~1.1.0"
|
||||
|
||||
metascraper-lang@^5.11.1:
|
||||
version "5.11.1"
|
||||
resolved "https://registry.yarnpkg.com/metascraper-lang/-/metascraper-lang-5.11.1.tgz#494d080f30c76bfe2d10ce2c50a55f64bd534e1d"
|
||||
integrity sha512-g0XjUFbaMwKa5ws09/gJj5WOM6FsB2v0P9gWLIj2bYo9+QXXGZqn9MusYXGMHiCNELkesPyFXQY8oTjJ7z2hMA==
|
||||
metascraper-lang@^5.11.6:
|
||||
version "5.11.6"
|
||||
resolved "https://registry.yarnpkg.com/metascraper-lang/-/metascraper-lang-5.11.6.tgz#bf8b38c220c472bf46fb1adb78e36b4d2a223cfe"
|
||||
integrity sha512-gIHrmR6YE1+W2FUmYMggiD8NU7oCuQUYG8fYE0zKxdoAseM2qQcmZ+aGgYAFL4RlgMQwm+9zxhcLX+Te4oMRDw==
|
||||
dependencies:
|
||||
"@metascraper/helpers" "^5.11.1"
|
||||
"@metascraper/helpers" "^5.11.6"
|
||||
|
||||
metascraper-logo@^5.11.6:
|
||||
version "5.11.6"
|
||||
@ -6296,20 +6289,20 @@ metascraper-publisher@^5.11.6:
|
||||
dependencies:
|
||||
"@metascraper/helpers" "^5.11.6"
|
||||
|
||||
metascraper-soundcloud@^5.11.5:
|
||||
version "5.11.5"
|
||||
resolved "https://registry.yarnpkg.com/metascraper-soundcloud/-/metascraper-soundcloud-5.11.5.tgz#989665c8d4177e0b687e12de3d951b79d69704bc"
|
||||
integrity sha512-SbDtLkp/Uyg+gykNdmF+6Hy15TbNBnBWuVbRk99x+9XDoLbsK98kp1pJm0lU//RlRJAPfeWXPyMjhBMbDbeE/A==
|
||||
metascraper-soundcloud@^5.11.7:
|
||||
version "5.11.7"
|
||||
resolved "https://registry.yarnpkg.com/metascraper-soundcloud/-/metascraper-soundcloud-5.11.7.tgz#bfada9cd3189dc223f04e2219bc4e146d75ec1da"
|
||||
integrity sha512-GPMxSHb1fayrTovLCxBd2HmzHs6eJFgEm37294NBsvzdsb5TwcjyDCd7O9CIARY4Bcw7yL60k1WathSdeAUqgA==
|
||||
dependencies:
|
||||
"@metascraper/helpers" "^5.11.1"
|
||||
tldts "~5.6.9"
|
||||
"@metascraper/helpers" "^5.11.6"
|
||||
tldts "~5.6.12"
|
||||
|
||||
metascraper-title@^5.11.1:
|
||||
version "5.11.1"
|
||||
resolved "https://registry.yarnpkg.com/metascraper-title/-/metascraper-title-5.11.1.tgz#fb32da17e71b2c64dc45d12c3602ca9330ab4bd5"
|
||||
integrity sha512-fZ8qjf+d5ntwz/75X2r3BYD8X+tioZS1s+k9G3gfc+6ByJvrL9/ey6YzlDlU4zYqX73Vdx7+zUyy2ct0aJqWFA==
|
||||
metascraper-title@^5.11.6:
|
||||
version "5.11.6"
|
||||
resolved "https://registry.yarnpkg.com/metascraper-title/-/metascraper-title-5.11.6.tgz#286d6b0e4a9ee51c54524ea441be50629d4e91f3"
|
||||
integrity sha512-oIVEo+erZZ1s/3E4VGh1Dk9AGeunHQaR0bslkBT3OuwO0hqf94n5pA0fMmbhmv/Pdon+ZTTL6JV+uEYQnV6EKw==
|
||||
dependencies:
|
||||
"@metascraper/helpers" "^5.11.1"
|
||||
"@metascraper/helpers" "^5.11.6"
|
||||
lodash "~4.17.15"
|
||||
|
||||
metascraper-url@^5.11.6:
|
||||
@ -6319,12 +6312,12 @@ metascraper-url@^5.11.6:
|
||||
dependencies:
|
||||
"@metascraper/helpers" "^5.11.6"
|
||||
|
||||
metascraper-video@^5.11.1:
|
||||
version "5.11.1"
|
||||
resolved "https://registry.yarnpkg.com/metascraper-video/-/metascraper-video-5.11.1.tgz#4018a635d816f3123c7ba97fe7669e4d61af2196"
|
||||
integrity sha512-g8x6R4ntX7pt7ntuRCzL1+xIRd0JFAp/LoVPYFvdwn/D78u9GMJi+JvrNuLIEcrmtb6re6rE9MIOy8qMo1g3qA==
|
||||
metascraper-video@^5.11.6:
|
||||
version "5.11.6"
|
||||
resolved "https://registry.yarnpkg.com/metascraper-video/-/metascraper-video-5.11.6.tgz#da8a2f81f07891391b1245346750a5450a3ae8c6"
|
||||
integrity sha512-jxcLqSTvkPku1OMz/x8epDs6mnN3/IgBbcffC2TIzM7yJxcHpzxOGfVcUZ4igwKlz70lk8P8V5gIHMYAFDNdrQ==
|
||||
dependencies:
|
||||
"@metascraper/helpers" "^5.11.1"
|
||||
"@metascraper/helpers" "^5.11.6"
|
||||
lodash "~4.17.15"
|
||||
|
||||
metascraper-youtube@^5.11.6:
|
||||
@ -6498,10 +6491,10 @@ ms@^2.1.1:
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
||||
|
||||
mustache@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.0.0.tgz#7f02465dbb5b435859d154831c032acdfbbefb31"
|
||||
integrity sha512-FJgjyX/IVkbXBXYUwH+OYwQKqWpFPLaLVESd70yHjSDunwzV2hZOoTBvPf4KLoxesUzzyfTH6F784Uqd7Wm5yA==
|
||||
mustache@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.0.1.tgz#d99beb031701ad433338e7ea65e0489416c854a2"
|
||||
integrity sha512-yL5VE97+OXn4+Er3THSmTdCFCtx5hHWzrolvH+JObZnUYwuaG7XV+Ch4fR2cIrcYI0tFHxS7iyFYl14bW8y2sA==
|
||||
|
||||
mute-stream@0.0.8:
|
||||
version "0.0.8"
|
||||
@ -6572,10 +6565,10 @@ neo4j-driver@^1.7.6:
|
||||
text-encoding-utf-8 "^1.0.2"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
neo4j-driver@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-4.0.1.tgz#b25ffde0f16602e94c46d097e16a8bacbd773d5a"
|
||||
integrity sha512-SqBhXyyyayVs5gV/6BrgdKbcmU5AsYQXkFAiYO74XAE8XPLJ1HVR/Hu4wjonAX7+70DsalkWEiFN1c6UaCVzlQ==
|
||||
neo4j-driver@^4.0.1, neo4j-driver@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-4.0.2.tgz#78de3b91e91572bcbd9d2e02554322fe1ab399ea"
|
||||
integrity sha512-xQN4BZZsweaNNac7FDYAV6f/JybghwY3lk4fwblS8V5KQ+DBMPe4Pthh672mp+wEYZGyzPalq5CfpcBrWaZ4Gw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.5.5"
|
||||
rxjs "^6.5.2"
|
||||
@ -6692,9 +6685,9 @@ nodemailer-html-to-text@^3.1.0:
|
||||
html-to-text "^5.1.1"
|
||||
|
||||
nodemailer@^6.4.4:
|
||||
version "6.4.4"
|
||||
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.4.4.tgz#f4bb26a833786e8908b3ac8afbf2d0382ac24feb"
|
||||
integrity sha512-2GqGu5o3FBmDibczU3+LZh9lCEiKmNx7LvHl512p8Kj+Kn5FQVOICZv85MDFz/erK0BDd5EJp3nqQLpWCZD1Gg==
|
||||
version "6.4.5"
|
||||
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.4.5.tgz#45614c6454d1a947242105eeddae03df87e29916"
|
||||
integrity sha512-NH7aNVQyZLAvGr2+EOto7znvz+qJ02Cb/xpou98ApUt5tEAUSVUxhvHvgV/8I5dhjKTYqUw0nasoKzLNBJKrDQ==
|
||||
|
||||
nodemon@~2.0.2:
|
||||
version "2.0.2"
|
||||
@ -7367,10 +7360,10 @@ prop-types@^15.6.2:
|
||||
object-assign "^4.1.1"
|
||||
react-is "^16.8.1"
|
||||
|
||||
property-expr@^1.5.0:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-1.5.1.tgz#22e8706894a0c8e28d58735804f6ba3a3673314f"
|
||||
integrity sha512-CGuc0VUTGthpJXL36ydB6jnbyOf/rAHFvmVrJlH+Rg0DqqLFQGAP6hIaxD/G0OAmBJPhXDHuEJigrp0e0wFV6g==
|
||||
property-expr@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.2.tgz#fff2a43919135553a3bc2fdd94bdb841965b2330"
|
||||
integrity sha512-bc/5ggaYZxNkFKj374aLbEDqVADdYaLcFo8XBkishUWbaAdjlphaBFns9TvRA2pUseVL/wMFmui9X3IdNDU37g==
|
||||
|
||||
proxy-addr@~2.0.5:
|
||||
version "2.0.5"
|
||||
@ -7896,9 +7889,9 @@ sane@^4.0.3:
|
||||
walker "~1.0.5"
|
||||
|
||||
sanitize-html@~1.22.0:
|
||||
version "1.22.0"
|
||||
resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.22.0.tgz#9df779c53cf5755adb2322943c21c1c1dffca7bf"
|
||||
integrity sha512-3RPo65mbTKpOAdAYWU496MSty1YbB3Y5bjwL5OclgaSSMtv65xvM7RW/EHRumzaZ1UddEJowCbSdK0xl5sAu0A==
|
||||
version "1.22.1"
|
||||
resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.22.1.tgz#5b36c92ab27917ddd2775396815c2bc1a6268310"
|
||||
integrity sha512-++IMC00KfMQc45UWZJlhWOlS9eMrME38sFG9GXfR+k6oBo9JXSYQgTOZCl9j3v/smFTRNT9XNwz5DseFdMY+2Q==
|
||||
dependencies:
|
||||
chalk "^2.4.1"
|
||||
htmlparser2 "^4.1.0"
|
||||
@ -7906,7 +7899,7 @@ sanitize-html@~1.22.0:
|
||||
lodash.escaperegexp "^4.1.2"
|
||||
lodash.isplainobject "^4.0.6"
|
||||
lodash.isstring "^4.0.1"
|
||||
lodash.mergewith "^4.6.1"
|
||||
lodash.mergewith "^4.6.2"
|
||||
postcss "^7.0.27"
|
||||
srcset "^2.0.1"
|
||||
xtend "^4.0.1"
|
||||
@ -8548,10 +8541,10 @@ symbol-tree@^3.2.2:
|
||||
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
|
||||
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
|
||||
|
||||
synchronous-promise@^2.0.6:
|
||||
version "2.0.9"
|
||||
resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.9.tgz#b83db98e9e7ae826bf9c8261fd8ac859126c780a"
|
||||
integrity sha512-LO95GIW16x69LuND1nuuwM4pjgFGupg7pZ/4lU86AmchPKrhk0o2tpMU2unXRrqo81iAFe1YJ0nAGEVwsrZAgg==
|
||||
synchronous-promise@^2.0.10:
|
||||
version "2.0.10"
|
||||
resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.10.tgz#e64c6fd3afd25f423963353043f4a68ebd397fd8"
|
||||
integrity sha512-6PC+JRGmNjiG3kJ56ZMNWDPL8hjyghF5cMXIFOKg+NiwwEZZIvxTWd0pinWKyD227odg9ygF8xVhhz7gb8Uq7A==
|
||||
|
||||
table@^5.2.3:
|
||||
version "5.4.6"
|
||||
@ -8667,17 +8660,17 @@ tlds@^1.187.0, tlds@^1.203.0:
|
||||
resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.203.1.tgz#4dc9b02f53de3315bc98b80665e13de3edfc1dfc"
|
||||
integrity sha512-7MUlYyGJ6rSitEZ3r1Q1QNV8uSIzapS8SmmhSusBuIc7uIxPPwsKllEP0GRp1NS6Ik6F+fRZvnjDWm3ecv2hDw==
|
||||
|
||||
tldts-core@^5.6.9:
|
||||
version "5.6.9"
|
||||
resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-5.6.9.tgz#70e9b1d944b3b93008e0b31ffd8d1de6ce690bc9"
|
||||
integrity sha512-MOvSUUxUyNmNK7R7cJKBvWys1rufuobWpPaVcGJd8EEsIyJzSVOlA5Y9l9V1Z0FdzlPMBAYPcpKuoQkvH7pfRg==
|
||||
tldts-core@^5.6.12:
|
||||
version "5.6.12"
|
||||
resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-5.6.12.tgz#5774086a65cf9d5fbf0c828dffe2c830dcbe8c17"
|
||||
integrity sha512-QdqPwO8aBWpLb3SixPijbbWhQyVeDTZJe8UQe9IdiH6F8NCOWl89EGftTeiz/RH74LTY5CWLwh6vcPj7lhqH8A==
|
||||
|
||||
tldts@~5.6.9:
|
||||
version "5.6.9"
|
||||
resolved "https://registry.yarnpkg.com/tldts/-/tldts-5.6.9.tgz#6fb3a10161aae568b1a862e4c96101a79adb557f"
|
||||
integrity sha512-Dt5c+gD6tC3hvcUZEI/yMvru1gbnFzx95s7Di+GWZUrfEjI6vQg+vthje2Apduv9RHXXPEr5BLA51OsAwPvhWQ==
|
||||
tldts@~5.6.12:
|
||||
version "5.6.12"
|
||||
resolved "https://registry.yarnpkg.com/tldts/-/tldts-5.6.12.tgz#ae7096e70fda948753bac9f2731da4663d01b45c"
|
||||
integrity sha512-jq3re1oUJpCTGCj3PbBgBQum2Lvsrkt/D9Tj/HB8e6ET+xi7yIBYzcXddJYwNBC7Vit3XQz4Jj8nGIeuOlRFQA==
|
||||
dependencies:
|
||||
tldts-core "^5.6.9"
|
||||
tldts-core "^5.6.12"
|
||||
|
||||
tmp@^0.0.33:
|
||||
version "0.0.33"
|
||||
@ -9110,10 +9103,10 @@ validate-npm-package-license@^3.0.1:
|
||||
spdx-correct "^3.0.0"
|
||||
spdx-expression-parse "^3.0.0"
|
||||
|
||||
validator@^12.2.0:
|
||||
version "12.2.0"
|
||||
resolved "https://registry.yarnpkg.com/validator/-/validator-12.2.0.tgz#660d47e96267033fd070096c3b1a6f2db4380a0a"
|
||||
integrity sha512-jJfE/DW6tIK1Ek8nCfNFqt8Wb3nzMoAbocBF6/Icgg1ZFSBpObdnwVY2jQj6qUqzhx5jc71fpvBWyLGO7Xl+nQ==
|
||||
validator@^13.0.0:
|
||||
version "13.0.0"
|
||||
resolved "https://registry.yarnpkg.com/validator/-/validator-13.0.0.tgz#0fb6c6bb5218ea23d368a8347e6d0f5a70e3bcab"
|
||||
integrity sha512-anYx5fURbgF04lQV18nEQWZ/3wHGnxiKdG4aL8J+jEDsm98n/sU/bey+tYk6tnGJzm7ioh5FoqrAiQ6m03IgaA==
|
||||
|
||||
vary@^1, vary@~1.1.2:
|
||||
version "1.1.2"
|
||||
@ -9385,17 +9378,17 @@ yargs@^15.0.0:
|
||||
y18n "^4.0.0"
|
||||
yargs-parser "^16.1.0"
|
||||
|
||||
yup@^0.28.1:
|
||||
version "0.28.1"
|
||||
resolved "https://registry.yarnpkg.com/yup/-/yup-0.28.1.tgz#60c0725be7057ed7a9ae61561333809332a63d47"
|
||||
integrity sha512-xSHMZA7UyecSG/CCTDCtnYZMjBrYDR/C7hu0fMsZ6UcS/ngko4qCVFbw+CAmNtHlbItKkvQ3YXITODeTj/dUkw==
|
||||
yup@^0.28.3:
|
||||
version "0.28.3"
|
||||
resolved "https://registry.yarnpkg.com/yup/-/yup-0.28.3.tgz#1ca607405a8adf24a5ac51f54bd09d527555f0ba"
|
||||
integrity sha512-amVkCgFWe5bGjrrUiODkbIzrSwtB8JpZrQYSrfj2YsbRdrV+tn9LquWdZDlfOx2HXyfEA8FGnlwidE/bFDxO7Q==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.0.0"
|
||||
fn-name "~2.0.1"
|
||||
lodash "^4.17.11"
|
||||
"@babel/runtime" "^7.8.7"
|
||||
fn-name "~3.0.0"
|
||||
lodash "^4.17.15"
|
||||
lodash-es "^4.17.11"
|
||||
property-expr "^1.5.0"
|
||||
synchronous-promise "^2.0.6"
|
||||
property-expr "^2.0.0"
|
||||
synchronous-promise "^2.0.10"
|
||||
toposort "^2.0.2"
|
||||
|
||||
zen-observable-ts@^0.8.20:
|
||||
|
||||
@ -3,8 +3,6 @@ import locales from '../../../webapp/locales'
|
||||
import orderBy from 'lodash/orderBy'
|
||||
|
||||
const languages = orderBy(locales, 'name')
|
||||
const narratorAvatar =
|
||||
"https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg";
|
||||
|
||||
When("I type in a comment with {int} characters", size => {
|
||||
var c="";
|
||||
@ -32,9 +30,11 @@ Then("my comment should be successfully created", () => {
|
||||
Then("I should see my comment", () => {
|
||||
cy.get("article.comment-card p")
|
||||
.should("contain", "Human Connection rocks")
|
||||
.get(".user-teaser span.slug")
|
||||
.should("contain", "@peter-pan") // specific enough
|
||||
.get(".user-avatar img")
|
||||
.should("have.attr", "src")
|
||||
.and("contain", narratorAvatar)
|
||||
.and("contain", 'https://') // some url
|
||||
.get(".user-teaser > .info > .text")
|
||||
.should("contain", "today at");
|
||||
});
|
||||
|
||||
@ -24,7 +24,6 @@ const narratorParams = {
|
||||
id: 'id-of-peter-pan',
|
||||
name: "Peter Pan",
|
||||
slug: "peter-pan",
|
||||
avatar: "https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg",
|
||||
...termsAndConditionsAgreedVersion,
|
||||
};
|
||||
|
||||
@ -422,14 +421,13 @@ When("mention {string} in the text", mention => {
|
||||
.click();
|
||||
});
|
||||
|
||||
Then("the notification gets marked as read", () => {
|
||||
cy.get(".notifications-menu-popover .notification")
|
||||
.first()
|
||||
.should("have.class", "--read");
|
||||
Then("the unread counter is removed", () => {
|
||||
cy.get('.notifications-menu .counter-icon').should('not.exist');
|
||||
});
|
||||
|
||||
Then("there are no notifications in the top menu", () => {
|
||||
cy.get(".notifications-menu").should("contain", "0");
|
||||
Then("the notification menu button links to the all notifications page", () => {
|
||||
cy.get(".notifications-menu").click();
|
||||
cy.location("pathname").should("contain", "/notifications");
|
||||
});
|
||||
|
||||
Given("there is an annoying user called {string}", name => {
|
||||
|
||||
@ -24,6 +24,6 @@ Feature: Notification for a mention
|
||||
And see 1 unread notifications in the top menu
|
||||
And open the notification menu and click on the first item
|
||||
Then I get to the post page of ".../hey-matt"
|
||||
And the notification gets marked as read
|
||||
But when I refresh the page
|
||||
Then there are no notifications in the top menu
|
||||
And the unread counter is removed
|
||||
And the notification menu button links to the all notifications page
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ Feature: Delete Teaser Image
|
||||
Given I have a user account
|
||||
Given I am logged in
|
||||
Given we have the following posts in our database:
|
||||
| authorId | id | title | content |
|
||||
| authorId | id | title | content |
|
||||
| id-of-peter-pan | p1 | Post to be updated | successfully updated |
|
||||
|
||||
Scenario: Delete existing image
|
||||
|
||||
@ -7,7 +7,7 @@ Feature: Search
|
||||
Given I have a user account
|
||||
And we have the following posts in our database:
|
||||
| id | title | content |
|
||||
| p1 | 101 Essays that will change the way you think | 101 Essays, of course! |
|
||||
| p1 | 101 Essays that will change the way you think | 101 Essays, of course (PR)! |
|
||||
| p2 | No searched for content | will be found in this post, I guarantee |
|
||||
And we have the following user accounts:
|
||||
| slug | name | id |
|
||||
@ -24,7 +24,7 @@ Feature: Search
|
||||
| 101 Essays that will change the way you think |
|
||||
|
||||
Scenario: Press enter starts search
|
||||
When I type "Es" and press Enter
|
||||
When I type "PR" and press Enter
|
||||
Then I should have one item in the select dropdown
|
||||
Then I should see the following posts in the select dropdown:
|
||||
| title |
|
||||
|
||||
10
package.json
10
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "human-connection",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.0",
|
||||
"description": "Fullstack and API tests with cypress and cucumber for Human Connection",
|
||||
"author": "Human Connection gGmbh",
|
||||
"license": "MIT",
|
||||
@ -32,13 +32,13 @@
|
||||
"auto-changelog": "^1.16.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"codecov": "^3.6.5",
|
||||
"cross-env": "^6.0.3",
|
||||
"cross-env": "^7.0.2",
|
||||
"cucumber": "^6.0.5",
|
||||
"cypress": "^4.1.0",
|
||||
"cypress": "^4.2.0",
|
||||
"cypress-cucumber-preprocessor": "^2.0.1",
|
||||
"cypress-file-upload": "^3.5.3",
|
||||
"cypress-plugin-retries": "^1.5.2",
|
||||
"date-fns": "^2.10.0",
|
||||
"date-fns": "^2.11.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"expect": "^25.1.0",
|
||||
"faker": "Marak/faker.js#master",
|
||||
@ -46,7 +46,7 @@
|
||||
"import": "^0.0.6",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mock-socket": "^9.0.3",
|
||||
"neo4j-driver": "^4.0.1",
|
||||
"neo4j-driver": "^4.0.2",
|
||||
"neode": "^0.3.7",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"rosie": "^2.0.1",
|
||||
|
||||
@ -72,7 +72,7 @@ You can then visit the Storybook playground on `http://localhost:3002`
|
||||
After starting the application following the above guidelines, open new terminal windows and navigate to the `/webapp` directory for each of these commands:
|
||||
|
||||
```bash
|
||||
# run eslint in /webapp
|
||||
# run eslint in /webapp (use option --fix to normalize the files)
|
||||
$ yarn lint
|
||||
```
|
||||
|
||||
@ -81,6 +81,11 @@ $ yarn lint
|
||||
$ yarn test
|
||||
```
|
||||
|
||||
```bash
|
||||
# run locales in /webapp (use option --fix to sort the locales)
|
||||
$ yarn locales
|
||||
```
|
||||
|
||||
```bash
|
||||
# start storybook in /webapp
|
||||
$ yarn storybook
|
||||
|
||||
@ -2,20 +2,29 @@ import { shallowMount } from '@vue/test-utils'
|
||||
import Badges from './Badges.vue'
|
||||
|
||||
describe('Badges.vue', () => {
|
||||
let wrapper
|
||||
let propsData
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(Badges, {})
|
||||
propsData = {}
|
||||
})
|
||||
|
||||
it('renders', () => {
|
||||
expect(wrapper.is('div')).toBe(true)
|
||||
})
|
||||
describe('shallowMount', () => {
|
||||
const Wrapper = () => {
|
||||
return shallowMount(Badges, { propsData })
|
||||
}
|
||||
|
||||
it('has class "hc-badges"', () => {
|
||||
expect(wrapper.contains('.hc-badges')).toBe(true)
|
||||
})
|
||||
it('has class "hc-badges"', () => {
|
||||
expect(Wrapper().contains('.hc-badges')).toBe(true)
|
||||
})
|
||||
|
||||
// TODO: add similar software tests for other components
|
||||
// TODO: add more test cases in this file
|
||||
describe('given a badge', () => {
|
||||
beforeEach(() => {
|
||||
propsData.badges = [{ id: '1', icon: '/path/to/some/icon' }]
|
||||
})
|
||||
|
||||
it('proxies badge icon, which is just a URL without metadata', () => {
|
||||
expect(Wrapper().contains('img[src="/api/path/to/some/icon"]')).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -17,8 +17,10 @@ const comment = {
|
||||
disabled: false,
|
||||
author: {
|
||||
id: '1',
|
||||
avatar:
|
||||
'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/db/dbc9e03ebcc384b920c31542af2d27dd8eea9dc2_full.jpg',
|
||||
avatar: {
|
||||
url:
|
||||
'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/db/dbc9e03ebcc384b920c31542af2d27dd8eea9dc2_full.jpg',
|
||||
},
|
||||
slug: 'jenny-rostock',
|
||||
name: 'Rainer Unsinn',
|
||||
disabled: false,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<ds-form v-model="form" @submit="handleSubmit" class="comment-form">
|
||||
<template slot-scope="{ errors }">
|
||||
<template #default="{ errors }">
|
||||
<base-card>
|
||||
<hc-editor ref="editor" :users="users" :value="form.content" @input="updateEditorContent" />
|
||||
<div class="buttons">
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<dropdown class="content-menu" :placement="placement" offset="5">
|
||||
<template slot="default" slot-scope="{ toggleMenu }">
|
||||
<template #default="{ toggleMenu }">
|
||||
<slot name="button" :toggleMenu="toggleMenu">
|
||||
<base-button
|
||||
data-test="content-menu-button"
|
||||
@ -12,20 +12,22 @@
|
||||
/>
|
||||
</slot>
|
||||
</template>
|
||||
<div slot="popover" slot-scope="{ toggleMenu }" class="content-menu-popover">
|
||||
<ds-menu :routes="routes">
|
||||
<ds-menu-item
|
||||
slot="menuitem"
|
||||
slot-scope="item"
|
||||
:route="item.route"
|
||||
:parents="item.parents"
|
||||
@click.stop.prevent="openItem(item.route, toggleMenu)"
|
||||
>
|
||||
<base-icon :name="item.route.icon" />
|
||||
{{ item.route.label }}
|
||||
</ds-menu-item>
|
||||
</ds-menu>
|
||||
</div>
|
||||
<template #popover="{ toggleMenu }">
|
||||
<div class="content-menu-popover">
|
||||
<ds-menu :routes="routes">
|
||||
<template #menuitem="item">
|
||||
<ds-menu-item
|
||||
:route="item.route"
|
||||
:parents="item.parents"
|
||||
@click.stop.prevent="openItem(item.route, toggleMenu)"
|
||||
>
|
||||
<base-icon :name="item.route.icon" />
|
||||
{{ item.route.label }}
|
||||
</ds-menu-item>
|
||||
</template>
|
||||
</ds-menu>
|
||||
</div>
|
||||
</template>
|
||||
</dropdown>
|
||||
</template>
|
||||
|
||||
|
||||
@ -70,7 +70,7 @@ describe('ContributionForm.vue', () => {
|
||||
},
|
||||
url: 'someUrlToImage',
|
||||
}
|
||||
const image = '/uploads/1562010976466-avataaars'
|
||||
const image = { sensitive: false, url: '/uploads/1562010976466-avataaars', aspectRatio: 1 }
|
||||
beforeEach(() => {
|
||||
mocks = {
|
||||
$t: jest.fn(),
|
||||
@ -199,10 +199,7 @@ describe('ContributionForm.vue', () => {
|
||||
language: 'en',
|
||||
id: null,
|
||||
categoryIds: ['cat12'],
|
||||
imageUpload: null,
|
||||
imageAspectRatio: null,
|
||||
image: null,
|
||||
imageBlurred: false,
|
||||
},
|
||||
}
|
||||
postTitleInput = wrapper.find('.ds-input')
|
||||
@ -233,8 +230,16 @@ describe('ContributionForm.vue', () => {
|
||||
})
|
||||
|
||||
it('supports adding a teaser image', async () => {
|
||||
const spy = jest.spyOn(FileReader.prototype, 'readAsDataURL').mockImplementation(() => {})
|
||||
expectedParams.variables.imageUpload = imageUpload
|
||||
expectedParams.variables.image = {
|
||||
aspectRatio: null,
|
||||
sensitive: false,
|
||||
upload: imageUpload,
|
||||
}
|
||||
const spy = jest
|
||||
.spyOn(FileReader.prototype, 'readAsDataURL')
|
||||
.mockImplementation(function() {
|
||||
this.onload({ target: { result: 'someUrlToImage' } })
|
||||
})
|
||||
wrapper.find(ImageUploader).vm.$emit('addHeroImage', imageUpload)
|
||||
await wrapper.find('form').trigger('submit')
|
||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
|
||||
@ -317,7 +322,6 @@ describe('ContributionForm.vue', () => {
|
||||
name: 'Democracy & Politics',
|
||||
},
|
||||
],
|
||||
imageAspectRatio: 1,
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
@ -354,10 +358,9 @@ describe('ContributionForm.vue', () => {
|
||||
language: propsData.contribution.language,
|
||||
id: propsData.contribution.id,
|
||||
categoryIds: ['cat12'],
|
||||
image,
|
||||
imageUpload: null,
|
||||
imageAspectRatio: 1,
|
||||
imageBlurred: false,
|
||||
image: {
|
||||
sensitive: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
@ -383,8 +386,7 @@ describe('ContributionForm.vue', () => {
|
||||
|
||||
it('supports deleting a teaser image', async () => {
|
||||
expectedParams.variables.image = null
|
||||
expectedParams.variables.imageAspectRatio = null
|
||||
propsData.contribution.image = '/uploads/someimage.png'
|
||||
propsData.contribution.image = { url: '/uploads/someimage.png' }
|
||||
wrapper = Wrapper()
|
||||
wrapper.find('[data-test="delete-button"]').trigger('click')
|
||||
await wrapper.find('form').trigger('submit')
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
:schema="formSchema"
|
||||
@submit="submit"
|
||||
>
|
||||
<template slot-scope="{ errors }">
|
||||
<template #default="{ errors }">
|
||||
<base-card>
|
||||
<template #heroImage>
|
||||
<img
|
||||
@ -106,27 +106,20 @@ export default {
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const {
|
||||
title,
|
||||
content,
|
||||
image,
|
||||
imageAspectRatio,
|
||||
imageBlurred,
|
||||
language,
|
||||
categories,
|
||||
} = this.contribution
|
||||
const { title, content, image, language, categories } = this.contribution
|
||||
|
||||
const languageOptions = orderBy(locales, 'name').map(locale => {
|
||||
return { label: locale.name, value: locale.code }
|
||||
})
|
||||
const { sensitive: imageBlurred = false, aspectRatio: imageAspectRatio = null } = image || {}
|
||||
|
||||
return {
|
||||
formData: {
|
||||
title: title || '',
|
||||
content: content || '',
|
||||
image: image || null,
|
||||
imageAspectRatio: imageAspectRatio || null,
|
||||
imageBlurred: imageBlurred || false,
|
||||
imageAspectRatio,
|
||||
imageBlurred,
|
||||
language: languageOptions.find(option => option.value === language) || null,
|
||||
categoryIds: categories ? categories.map(category => category.id) : [],
|
||||
},
|
||||
@ -163,16 +156,28 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
let image = null
|
||||
const { title, content, categoryIds } = this.formData
|
||||
if (this.formData.image) {
|
||||
image = {
|
||||
sensitive: this.formData.imageBlurred,
|
||||
}
|
||||
if (this.imageUpload) {
|
||||
image.upload = this.imageUpload
|
||||
image.aspectRatio = this.formData.imageAspectRatio
|
||||
}
|
||||
}
|
||||
this.loading = true
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: this.contribution.id ? PostMutations().UpdatePost : PostMutations().CreatePost,
|
||||
variables: {
|
||||
...this.formData,
|
||||
title,
|
||||
content,
|
||||
categoryIds,
|
||||
id: this.contribution.id || null,
|
||||
language: this.formData.language.value,
|
||||
image: this.imageUpload ? null : this.formData.image,
|
||||
imageUpload: this.imageUpload,
|
||||
image,
|
||||
},
|
||||
})
|
||||
.then(({ data }) => {
|
||||
@ -198,10 +203,13 @@ export default {
|
||||
if (file) {
|
||||
const reader = new FileReader()
|
||||
reader.onload = ({ target }) => {
|
||||
this.formData.image = target.result
|
||||
this.formData.image = {
|
||||
...this.formData.image,
|
||||
url: target.result,
|
||||
}
|
||||
}
|
||||
this.imageUpload = file
|
||||
reader.readAsDataURL(file)
|
||||
this.imageUpload = file
|
||||
}
|
||||
},
|
||||
addImageAspectRatio(aspectRatio) {
|
||||
|
||||
@ -48,11 +48,23 @@ export default {
|
||||
if (isOpen) {
|
||||
this.$nextTick(() => {
|
||||
setTimeout(() => {
|
||||
document.getElementsByTagName('body')[0].classList.add('dropdown-open')
|
||||
const paddingRightStyle = `${window.innerWidth -
|
||||
document.documentElement.clientWidth}px`
|
||||
const navigationElement = document.querySelector('.main-navigation')
|
||||
document.body.style.paddingRight = paddingRightStyle
|
||||
document.body.classList.add('dropdown-open')
|
||||
if (navigationElement) {
|
||||
navigationElement.style.paddingRight = paddingRightStyle
|
||||
}
|
||||
}, 20)
|
||||
})
|
||||
} else {
|
||||
document.getElementsByTagName('body')[0].classList.remove('dropdown-open')
|
||||
const navigationElement = document.querySelector('.main-navigation')
|
||||
document.body.style.paddingRight = null
|
||||
document.body.classList.remove('dropdown-open')
|
||||
if (navigationElement) {
|
||||
navigationElement.style.paddingRight = null
|
||||
}
|
||||
}
|
||||
} catch (err) {}
|
||||
},
|
||||
|
||||
@ -1,35 +1,32 @@
|
||||
<template>
|
||||
<dropdown offset="8">
|
||||
<a
|
||||
:v-model="selected"
|
||||
slot="default"
|
||||
slot-scope="{ toggleMenu }"
|
||||
name="dropdown"
|
||||
class="dropdown-filter"
|
||||
href="#"
|
||||
@click.prevent="toggleMenu()"
|
||||
>
|
||||
<base-icon name="filter" />
|
||||
<label class="label" for="dropdown">{{ selected }}</label>
|
||||
<base-icon class="dropdown-arrow" name="angle-down" />
|
||||
</a>
|
||||
<ds-menu
|
||||
slot="popover"
|
||||
slot-scope="{ toggleMenu }"
|
||||
class="dropdown-menu-popover"
|
||||
:routes="filterOptions"
|
||||
>
|
||||
<ds-menu-item
|
||||
slot="menuitem"
|
||||
slot-scope="item"
|
||||
class="dropdown-menu-item"
|
||||
:route="item.route"
|
||||
:parents="item.parents"
|
||||
@click.stop.prevent="filter(item.route, toggleMenu)"
|
||||
<template #default="{ toggleMenu }">
|
||||
<a
|
||||
:v-model="selected"
|
||||
name="dropdown"
|
||||
class="dropdown-filter"
|
||||
href="#"
|
||||
@click.prevent="toggleMenu()"
|
||||
>
|
||||
{{ item.route.label }}
|
||||
</ds-menu-item>
|
||||
</ds-menu>
|
||||
<base-icon name="filter" />
|
||||
<label class="label" for="dropdown">{{ selected }}</label>
|
||||
<base-icon class="dropdown-arrow" name="angle-down" />
|
||||
</a>
|
||||
</template>
|
||||
<template #popover="{ toggleMenu }">
|
||||
<ds-menu class="dropdown-menu-popover" :routes="filterOptions">
|
||||
<template #menuitem="item">
|
||||
<ds-menu-item
|
||||
class="dropdown-menu-item"
|
||||
:route="item.route"
|
||||
:parents="item.parents"
|
||||
@click.stop.prevent="filter(item.route, toggleMenu)"
|
||||
>
|
||||
{{ item.route.label }}
|
||||
</ds-menu-item>
|
||||
</template>
|
||||
</ds-menu>
|
||||
</template>
|
||||
</dropdown>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<dropdown ref="menu" :placement="placement" :offset="offset">
|
||||
<base-button
|
||||
slot="default"
|
||||
icon="filter"
|
||||
:filled="filterActive"
|
||||
:ghost="!filterActive"
|
||||
slot-scope="{ toggleMenu }"
|
||||
@click.prevent="toggleMenu()"
|
||||
>
|
||||
<base-icon class="dropdown-arrow" name="angle-down" />
|
||||
</base-button>
|
||||
<template #default="{ toggleMenu }">
|
||||
<base-button
|
||||
icon="filter"
|
||||
:filled="filterActive"
|
||||
:ghost="!filterActive"
|
||||
@click.prevent="toggleMenu()"
|
||||
>
|
||||
<base-icon class="dropdown-arrow" name="angle-down" />
|
||||
</base-button>
|
||||
</template>
|
||||
<template slot="popover">
|
||||
<ds-container>
|
||||
<categories-filter-menu-items :chunk="chunk" />
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ds-tag>
|
||||
<ds-tag class="hc-hashtag">
|
||||
<nuxt-link :to="hashtagUrl">#{{ id }}</nuxt-link>
|
||||
</ds-tag>
|
||||
</template>
|
||||
|
||||
@ -1,35 +1,27 @@
|
||||
<template>
|
||||
<client-only>
|
||||
<dropdown ref="menu" :placement="placement" :offset="offset">
|
||||
<a
|
||||
slot="default"
|
||||
slot-scope="{ toggleMenu }"
|
||||
class="locale-menu"
|
||||
href="#"
|
||||
@click.prevent="toggleMenu()"
|
||||
>
|
||||
<base-icon name="globe" />
|
||||
<span class="label">{{ current.code.toUpperCase() }}</span>
|
||||
<base-icon class="dropdown-arrow" name="angle-down" />
|
||||
</a>
|
||||
<ds-menu
|
||||
slot="popover"
|
||||
slot-scope="{ toggleMenu }"
|
||||
class="locale-menu-popover"
|
||||
:matcher="matcher"
|
||||
:routes="routes"
|
||||
>
|
||||
<ds-menu-item
|
||||
slot="menuitem"
|
||||
slot-scope="item"
|
||||
class="locale-menu-item"
|
||||
:route="item.route"
|
||||
:parents="item.parents"
|
||||
@click.stop.prevent="changeLanguage(item.route.path, toggleMenu)"
|
||||
>
|
||||
{{ item.route.name }}
|
||||
</ds-menu-item>
|
||||
</ds-menu>
|
||||
<template #default="{ toggleMenu }">
|
||||
<a class="locale-menu" href="#" @click.prevent="toggleMenu()">
|
||||
<base-icon name="globe" />
|
||||
<span class="label">{{ current.code.toUpperCase() }}</span>
|
||||
<base-icon class="dropdown-arrow" name="angle-down" />
|
||||
</a>
|
||||
</template>
|
||||
<template #popover="{ toggleMenu }">
|
||||
<ds-menu class="locale-menu-popover" :matcher="matcher" :routes="routes">
|
||||
<template #menuitem="item">
|
||||
<ds-menu-item
|
||||
class="locale-menu-item"
|
||||
:route="item.route"
|
||||
:parents="item.parents"
|
||||
@click.stop.prevent="changeLanguage(item.route.path, toggleMenu)"
|
||||
>
|
||||
{{ item.route.name }}
|
||||
</ds-menu-item>
|
||||
</template>
|
||||
</ds-menu>
|
||||
</template>
|
||||
</dropdown>
|
||||
</client-only>
|
||||
</template>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { config, mount } from '@vue/test-utils'
|
||||
import { config, mount, RouterLinkStub } from '@vue/test-utils'
|
||||
import NotificationMenu from './NotificationMenu'
|
||||
|
||||
const localVue = global.localVue
|
||||
@ -11,6 +11,7 @@ describe('NotificationMenu.vue', () => {
|
||||
let wrapper
|
||||
let mocks
|
||||
let data
|
||||
let stubs
|
||||
beforeEach(() => {
|
||||
mocks = {
|
||||
$t: jest.fn(),
|
||||
@ -20,6 +21,9 @@ describe('NotificationMenu.vue', () => {
|
||||
notifications: [],
|
||||
}
|
||||
}
|
||||
stubs = {
|
||||
NuxtLink: RouterLinkStub,
|
||||
}
|
||||
})
|
||||
|
||||
describe('mount', () => {
|
||||
@ -28,12 +32,14 @@ describe('NotificationMenu.vue', () => {
|
||||
data,
|
||||
mocks,
|
||||
localVue,
|
||||
stubs,
|
||||
})
|
||||
}
|
||||
|
||||
it('counter displays 0', () => {
|
||||
it('renders as link without counter', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.find('.count').text()).toEqual('0')
|
||||
expect(wrapper.is('a.notifications-menu')).toBe(true)
|
||||
expect(() => wrapper.get('.count')).toThrow()
|
||||
})
|
||||
|
||||
it('no dropdown is rendered', () => {
|
||||
@ -41,7 +47,7 @@ describe('NotificationMenu.vue', () => {
|
||||
expect(wrapper.contains('.dropdown')).toBe(false)
|
||||
})
|
||||
|
||||
describe('given only unread notifications', () => {
|
||||
describe('given only read notifications', () => {
|
||||
beforeEach(() => {
|
||||
data = () => {
|
||||
return {
|
||||
@ -65,14 +71,15 @@ describe('NotificationMenu.vue', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('counter displays 0', () => {
|
||||
it('renders as link without counter', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.find('.count').text()).toEqual('0')
|
||||
expect(wrapper.is('a.notifications-menu')).toBe(true)
|
||||
expect(() => wrapper.get('.count')).toThrow()
|
||||
})
|
||||
|
||||
it('counter is not colored', () => {
|
||||
it('no dropdown is rendered', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.find('.count').classes()).toContain('--inactive')
|
||||
expect(wrapper.contains('.dropdown')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
<template>
|
||||
<base-button v-if="!notifications.length" class="notifications-menu" disabled ghost circle>
|
||||
<counter-icon icon="bell" :count="unreadNotificationsCount" danger />
|
||||
</base-button>
|
||||
<nuxt-link
|
||||
v-if="!unreadNotificationsCount"
|
||||
class="notifications-menu"
|
||||
:to="{ name: 'notifications' }"
|
||||
>
|
||||
<base-button icon="bell" ghost circle />
|
||||
</nuxt-link>
|
||||
<dropdown v-else class="notifications-menu" offset="8" :placement="placement">
|
||||
<template slot="default" slot-scope="{ toggleMenu }">
|
||||
<template #default="{ toggleMenu }">
|
||||
<base-button @click="toggleMenu" ghost circle>
|
||||
<counter-icon icon="bell" :count="unreadNotificationsCount" danger />
|
||||
</base-button>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<ds-form v-model="formData" :schema="formSchema" @submit="handleSubmit">
|
||||
<template slot-scope="{ errors }">
|
||||
<template #default="{ errors }">
|
||||
<ds-input
|
||||
id="oldPassword"
|
||||
model="oldPassword"
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
@submit="handleSubmitPassword"
|
||||
class="change-password"
|
||||
>
|
||||
<template slot-scope="{ errors }">
|
||||
<template #default="{ errors }">
|
||||
<ds-input
|
||||
id="password"
|
||||
model="password"
|
||||
|
||||
@ -16,8 +16,10 @@ export const post = {
|
||||
image: null,
|
||||
author: {
|
||||
id: 'u3',
|
||||
avatar:
|
||||
'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/db/dbc9e03ebcc384b920c31542af2d27dd8eea9dc2_full.jpg',
|
||||
avatar: {
|
||||
url:
|
||||
'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/db/dbc9e03ebcc384b920c31542af2d27dd8eea9dc2_full.jpg',
|
||||
},
|
||||
slug: 'jenny-rostock',
|
||||
name: 'Rainer Unsinn',
|
||||
disabled: false,
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
:lang="post.language"
|
||||
:class="{
|
||||
'disabled-content': post.disabled,
|
||||
'--blur-image': post.imageBlurred,
|
||||
'--blur-image': post.image && post.image.sensitive,
|
||||
}"
|
||||
:highlight="isPinned"
|
||||
>
|
||||
@ -93,8 +93,10 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const { image } = this.post
|
||||
if (!image) return
|
||||
const width = this.$el.offsetWidth
|
||||
const height = Math.min(width / this.post.imageAspectRatio, 2000)
|
||||
const height = Math.min(width / image.aspectRatio, 2000)
|
||||
const imageElement = this.$el.querySelector('.hero-image')
|
||||
if (imageElement) {
|
||||
imageElement.style.height = `${height}px`
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<vue-dropzone
|
||||
id="customdropzone"
|
||||
:key="user.avatar"
|
||||
:key="avatarUrl"
|
||||
ref="el"
|
||||
:use-custom-slot="true"
|
||||
:options="dropzoneOptions"
|
||||
@ -41,6 +41,12 @@ export default {
|
||||
hover: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
avatarUrl() {
|
||||
const { avatar } = this.user
|
||||
return avatar && avatar.url
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
error() {
|
||||
const that = this
|
||||
@ -64,7 +70,9 @@ export default {
|
||||
.mutate({
|
||||
mutation: updateUserMutation(),
|
||||
variables: {
|
||||
avatarUpload,
|
||||
avatar: {
|
||||
upload: avatarUpload,
|
||||
},
|
||||
id: this.user.id,
|
||||
},
|
||||
})
|
||||
|
||||
@ -12,7 +12,7 @@ describe('Upload', () => {
|
||||
mutate: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
data: { UpdateUser: { id: 'upload1', avatar: '/upload/avatar.jpg' } },
|
||||
data: { UpdateUser: { id: 'upload1', avatar: { url: '/upload/avatar.jpg' } } },
|
||||
})
|
||||
.mockRejectedValue({
|
||||
message: 'File upload unsuccessful! Whatcha gonna do?',
|
||||
@ -27,7 +27,7 @@ describe('Upload', () => {
|
||||
|
||||
const propsData = {
|
||||
user: {
|
||||
avatar: '/api/generic.jpg',
|
||||
avatar: { url: '/api/generic.jpg' },
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,9 @@ export const user = {
|
||||
id: 'u6',
|
||||
slug: 'louie',
|
||||
name: 'Louie',
|
||||
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/designervzm/128.jpg',
|
||||
avatar: {
|
||||
url: 'https://s3.amazonaws.com/uifaces/faces/twitter/designervzm/128.jpg',
|
||||
},
|
||||
about:
|
||||
'Illum in et velit soluta voluptatem architecto consequuntur enim placeat. Eum excepturi est ratione rerum in voluptatum corporis. Illum consequatur minus. Modi incidunt velit.',
|
||||
disabled: false,
|
||||
@ -28,7 +30,9 @@ export const user = {
|
||||
id: 'u3',
|
||||
slug: 'jenny-rostock',
|
||||
name: 'Jenny Rostock',
|
||||
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/bowbrick/128.jpg',
|
||||
avatar: {
|
||||
url: 'https://s3.amazonaws.com/uifaces/faces/twitter/bowbrick/128.jpg',
|
||||
},
|
||||
disabled: false,
|
||||
deleted: false,
|
||||
followedByCount: 2,
|
||||
@ -83,7 +87,7 @@ storiesOf('UserTeaser', module)
|
||||
<template #dateTime>
|
||||
- HEY! I'm edited
|
||||
</template>
|
||||
</user>
|
||||
</user-teaser>
|
||||
`,
|
||||
}))
|
||||
.add('anonymous', () => ({
|
||||
|
||||
@ -66,7 +66,9 @@ describe('UserAvatar.vue', () => {
|
||||
propsData = {
|
||||
user: {
|
||||
name: 'Not Anonymous',
|
||||
avatar: '/avatar.jpg',
|
||||
avatar: {
|
||||
url: '/avatar.jpg',
|
||||
},
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
@ -82,7 +84,9 @@ describe('UserAvatar.vue', () => {
|
||||
propsData = {
|
||||
user: {
|
||||
name: 'Not Anonymous',
|
||||
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/sawalazar/128.jpg',
|
||||
avatar: {
|
||||
url: 'https://s3.amazonaws.com/uifaces/faces/twitter/sawalazar/128.jpg',
|
||||
},
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
|
||||
@ -106,6 +106,16 @@ describe('SearchableInput.vue', () => {
|
||||
params: { id: 'u2', slug: 'bob-der-baumeister' },
|
||||
})
|
||||
})
|
||||
|
||||
it('pushes hashtag query params', async () => {
|
||||
select.element.value = 'Hash'
|
||||
select.trigger('input')
|
||||
const tags = wrapper.findAll('.hc-hashtag')
|
||||
const tag = tags.filter(item => item.text().match(/#Hashtag/))
|
||||
tag.trigger('click')
|
||||
await Vue.nextTick()
|
||||
expect(mocks.$router.push).toHaveBeenCalledWith('?hashtag=Hashtag')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -69,35 +69,47 @@ export const searchResults = [
|
||||
{
|
||||
id: 'u1',
|
||||
__typename: 'User',
|
||||
avatar:
|
||||
'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/db/dbc9e03ebcc384b920c31542af2d27dd8eea9dc2_full.jpg',
|
||||
avatar: {
|
||||
url:
|
||||
'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/db/dbc9e03ebcc384b920c31542af2d27dd8eea9dc2_full.jpg',
|
||||
},
|
||||
name: 'Peter Lustig',
|
||||
slug: 'peter-lustig',
|
||||
},
|
||||
{
|
||||
id: 'cdbca762-0632-4564-b646-415a0c42d8b8',
|
||||
__typename: 'User',
|
||||
avatar:
|
||||
'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/db/dbc9e03ebcc384b920c31542af2d27dd8eea9dc2_full.jpg',
|
||||
avatar: {
|
||||
url:
|
||||
'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/db/dbc9e03ebcc384b920c31542af2d27dd8eea9dc2_full.jpg',
|
||||
},
|
||||
name: 'Herbert Schultz',
|
||||
slug: 'herbert-schultz',
|
||||
},
|
||||
{
|
||||
id: 'u2',
|
||||
__typename: 'User',
|
||||
avatar:
|
||||
'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/db/dbc9e03ebcc384b920c31542af2d27dd8eea9dc2_full.jpg',
|
||||
avatar: {
|
||||
url:
|
||||
'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/db/dbc9e03ebcc384b920c31542af2d27dd8eea9dc2_full.jpg',
|
||||
},
|
||||
name: 'Bob der Baumeister',
|
||||
slug: 'bob-der-baumeister',
|
||||
},
|
||||
{
|
||||
id: '7b654f72-f4da-4315-8bed-39de0859754b',
|
||||
__typename: 'User',
|
||||
avatar:
|
||||
'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/db/dbc9e03ebcc384b920c31542af2d27dd8eea9dc2_full.jpg',
|
||||
avatar: {
|
||||
url:
|
||||
'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/db/dbc9e03ebcc384b920c31542af2d27dd8eea9dc2_full.jpg',
|
||||
},
|
||||
name: 'Tonya Mohr',
|
||||
slug: 'tonya-mohr',
|
||||
},
|
||||
{
|
||||
id: 'Hashtag',
|
||||
__typename: 'Tag',
|
||||
},
|
||||
]
|
||||
|
||||
storiesOf('Search Field', module)
|
||||
|
||||
@ -35,6 +35,12 @@
|
||||
>
|
||||
<search-post :option="option" />
|
||||
</p>
|
||||
<p
|
||||
v-if="option.__typename === 'Tag'"
|
||||
:class="{ 'option-with-heading': isFirstOfType(option) }"
|
||||
>
|
||||
<hc-hashtag :id="option.id" />
|
||||
</p>
|
||||
</template>
|
||||
</ds-select>
|
||||
<base-button v-if="isActive" icon="close" circle ghost size="small" @click="clear" />
|
||||
@ -45,6 +51,7 @@
|
||||
import { isEmpty } from 'lodash'
|
||||
import SearchHeading from '~/components/generic/SearchHeading/SearchHeading.vue'
|
||||
import SearchPost from '~/components/generic/SearchPost/SearchPost.vue'
|
||||
import HcHashtag from '~/components/Hashtag/Hashtag.vue'
|
||||
import UserTeaser from '~/components/UserTeaser/UserTeaser.vue'
|
||||
|
||||
export default {
|
||||
@ -52,6 +59,7 @@ export default {
|
||||
components: {
|
||||
SearchHeading,
|
||||
SearchPost,
|
||||
HcHashtag,
|
||||
UserTeaser,
|
||||
},
|
||||
props: {
|
||||
@ -138,12 +146,19 @@ export default {
|
||||
isPost(item) {
|
||||
return item.__typename === 'Post'
|
||||
},
|
||||
isTag(item) {
|
||||
return item.__typename === 'Tag'
|
||||
},
|
||||
goToResource(item) {
|
||||
this.$nextTick(() => {
|
||||
this.$router.push({
|
||||
name: this.isPost(item) ? 'post-id-slug' : 'profile-id-slug',
|
||||
params: { id: item.id, slug: item.slug },
|
||||
})
|
||||
if (!this.isTag(item)) {
|
||||
this.$router.push({
|
||||
name: this.isPost(item) ? 'post-id-slug' : 'profile-id-slug',
|
||||
params: { id: item.id, slug: item.slug },
|
||||
})
|
||||
} else {
|
||||
this.$router.push('?hashtag=' + item.id)
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
@ -17,7 +17,9 @@ export default i18n => {
|
||||
id
|
||||
slug
|
||||
name
|
||||
avatar
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
disabled
|
||||
deleted
|
||||
shoutedCount
|
||||
@ -47,7 +49,9 @@ export default i18n => {
|
||||
id
|
||||
slug
|
||||
name
|
||||
avatar
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
disabled
|
||||
deleted
|
||||
}
|
||||
@ -67,7 +71,9 @@ export default i18n => {
|
||||
id
|
||||
slug
|
||||
name
|
||||
avatar
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
disabled
|
||||
deleted
|
||||
shoutedCount
|
||||
|
||||
@ -12,7 +12,9 @@ export default app => {
|
||||
id
|
||||
slug
|
||||
name
|
||||
avatar
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
disabled
|
||||
deleted
|
||||
shoutedCount
|
||||
|
||||
@ -5,7 +5,9 @@ export const userFragment = gql`
|
||||
id
|
||||
slug
|
||||
name
|
||||
avatar
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
disabled
|
||||
deleted
|
||||
}
|
||||
@ -44,14 +46,16 @@ export const postFragment = gql`
|
||||
disabled
|
||||
deleted
|
||||
slug
|
||||
image
|
||||
language
|
||||
imageBlurred
|
||||
image {
|
||||
url
|
||||
sensitive
|
||||
aspectRatio
|
||||
}
|
||||
author {
|
||||
...user
|
||||
}
|
||||
pinnedAt
|
||||
imageAspectRatio
|
||||
pinned
|
||||
}
|
||||
`
|
||||
|
||||
@ -8,25 +8,24 @@ export default () => {
|
||||
$content: String!
|
||||
$language: String
|
||||
$categoryIds: [ID]
|
||||
$imageUpload: Upload
|
||||
$imageBlurred: Boolean
|
||||
$imageAspectRatio: Float
|
||||
$image: ImageInput
|
||||
) {
|
||||
CreatePost(
|
||||
title: $title
|
||||
content: $content
|
||||
language: $language
|
||||
categoryIds: $categoryIds
|
||||
imageUpload: $imageUpload
|
||||
imageBlurred: $imageBlurred
|
||||
imageAspectRatio: $imageAspectRatio
|
||||
image: $image
|
||||
) {
|
||||
title
|
||||
slug
|
||||
content
|
||||
contentExcerpt
|
||||
language
|
||||
imageBlurred
|
||||
image {
|
||||
url
|
||||
sensitive
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
@ -36,22 +35,16 @@ export default () => {
|
||||
$title: String!
|
||||
$content: String!
|
||||
$language: String
|
||||
$imageUpload: Upload
|
||||
$image: ImageInput
|
||||
$categoryIds: [ID]
|
||||
$image: String
|
||||
$imageBlurred: Boolean
|
||||
$imageAspectRatio: Float
|
||||
) {
|
||||
UpdatePost(
|
||||
id: $id
|
||||
title: $title
|
||||
content: $content
|
||||
language: $language
|
||||
imageUpload: $imageUpload
|
||||
categoryIds: $categoryIds
|
||||
image: $image
|
||||
imageBlurred: $imageBlurred
|
||||
imageAspectRatio: $imageAspectRatio
|
||||
categoryIds: $categoryIds
|
||||
) {
|
||||
id
|
||||
title
|
||||
@ -59,13 +52,16 @@ export default () => {
|
||||
content
|
||||
contentExcerpt
|
||||
language
|
||||
imageBlurred
|
||||
image {
|
||||
url
|
||||
sensitive
|
||||
aspectRatio
|
||||
}
|
||||
pinnedBy {
|
||||
id
|
||||
name
|
||||
role
|
||||
}
|
||||
imageAspectRatio
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
@ -19,6 +19,9 @@ export const findResourcesQuery = gql`
|
||||
... on User {
|
||||
...user
|
||||
}
|
||||
... on Tag {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -53,7 +53,9 @@ export const minimisedUserQuery = () => {
|
||||
id
|
||||
slug
|
||||
name
|
||||
avatar
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -223,7 +225,7 @@ export const updateUserMutation = () => {
|
||||
$allowEmbedIframes: Boolean
|
||||
$showShoutsPublicly: Boolean
|
||||
$termsAndConditionsAgreedVersion: String
|
||||
$avatarUpload: Upload
|
||||
$avatar: ImageInput
|
||||
) {
|
||||
UpdateUser(
|
||||
id: $id
|
||||
@ -234,7 +236,7 @@ export const updateUserMutation = () => {
|
||||
allowEmbedIframes: $allowEmbedIframes
|
||||
showShoutsPublicly: $showShoutsPublicly
|
||||
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
|
||||
avatarUpload: $avatarUpload
|
||||
avatar: $avatar
|
||||
) {
|
||||
id
|
||||
slug
|
||||
@ -245,7 +247,9 @@ export const updateUserMutation = () => {
|
||||
showShoutsPublicly
|
||||
locale
|
||||
termsAndConditionsAgreedVersion
|
||||
avatar
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -7,7 +7,9 @@ export const blockedUsers = () => {
|
||||
id
|
||||
name
|
||||
slug
|
||||
avatar
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
about
|
||||
disabled
|
||||
deleted
|
||||
|
||||
@ -7,7 +7,9 @@ export const mutedUsers = () => {
|
||||
id
|
||||
name
|
||||
slug
|
||||
avatar
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
about
|
||||
disabled
|
||||
deleted
|
||||
|
||||
@ -599,6 +599,7 @@
|
||||
"failed": "Nichts gefunden",
|
||||
"heading": {
|
||||
"Post": "Beiträge",
|
||||
"Tag": "Hashtags",
|
||||
"User": "Benutzer"
|
||||
},
|
||||
"hint": "Wonach suchst Du?",
|
||||
|
||||
@ -599,6 +599,7 @@
|
||||
"failed": "Nothing found",
|
||||
"heading": {
|
||||
"Post": "Posts",
|
||||
"Tag": "Hashtags",
|
||||
"User": "Users"
|
||||
},
|
||||
"hint": "What are you searching for?",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "human-connection-webapp",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.0",
|
||||
"description": "Human Connection Frontend",
|
||||
"authors": [
|
||||
"Grzegorz Leoniec (appinteractive)",
|
||||
@ -68,16 +68,16 @@
|
||||
"accounting": "~0.4.1",
|
||||
"apollo-cache-inmemory": "~1.6.5",
|
||||
"apollo-client": "~2.6.8",
|
||||
"cookie-universal-nuxt": "~2.1.2",
|
||||
"cookie-universal-nuxt": "~2.1.3",
|
||||
"cropperjs": "^1.5.5",
|
||||
"cross-env": "~7.0.2",
|
||||
"date-fns": "2.10.0",
|
||||
"date-fns": "2.11.0",
|
||||
"express": "~4.17.1",
|
||||
"graphql": "~14.6.0",
|
||||
"jsonwebtoken": "~8.5.1",
|
||||
"linkify-it": "~2.2.0",
|
||||
"node-fetch": "^2.6.0",
|
||||
"nuxt": "~2.11.0",
|
||||
"nuxt": "~2.12.0",
|
||||
"nuxt-dropzone": "^1.0.4",
|
||||
"nuxt-env": "~0.1.0",
|
||||
"stack-utils": "^2.0.1",
|
||||
@ -86,7 +86,7 @@
|
||||
"tiptap-extensions": "~1.28.6",
|
||||
"trunc-html": "^1.1.2",
|
||||
"v-tooltip": "~2.0.3",
|
||||
"validator": "^12.2.0",
|
||||
"validator": "^13.0.0",
|
||||
"vue-count-to": "~1.0.13",
|
||||
"vue-infinite-loading": "^2.4.5",
|
||||
"vue-izitoast": "^1.2.1",
|
||||
@ -100,15 +100,15 @@
|
||||
"@babel/core": "~7.8.7",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/preset-env": "~7.8.6",
|
||||
"@storybook/addon-a11y": "^5.3.14",
|
||||
"@storybook/addon-actions": "^5.3.14",
|
||||
"@storybook/addon-notes": "^5.3.14",
|
||||
"@storybook/vue": "~5.3.14",
|
||||
"@storybook/addon-a11y": "^5.3.17",
|
||||
"@storybook/addon-actions": "^5.3.17",
|
||||
"@storybook/addon-notes": "^5.3.17",
|
||||
"@storybook/vue": "~5.3.17",
|
||||
"@vue/cli-shared-utils": "~4.2.3",
|
||||
"@vue/eslint-config-prettier": "~6.0.0",
|
||||
"@vue/server-test-utils": "~1.0.0-beta.31",
|
||||
"@vue/test-utils": "~1.0.0-beta.31",
|
||||
"async-validator": "^3.2.3",
|
||||
"async-validator": "^3.2.4",
|
||||
"babel-core": "~7.0.0-bridge.0",
|
||||
"babel-eslint": "~10.1.0",
|
||||
"babel-jest": "~25.1.0",
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('admin.categories.name') }}</h2>
|
||||
<ds-table :data="Category" :fields="fields" condensed>
|
||||
<template slot="icon" slot-scope="scope">
|
||||
<template #icon="scope">
|
||||
<base-icon :name="scope.row.icon" />
|
||||
</template>
|
||||
</ds-table>
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('admin.hashtags.name') }}</h2>
|
||||
<ds-table :data="Tag" :fields="fields" condensed>
|
||||
<template slot="index" slot-scope="scope">{{ scope.index + 1 }}.</template>
|
||||
<template slot="id" slot-scope="scope">
|
||||
<template #index="scope">{{ scope.index + 1 }}.</template>
|
||||
<template #id="scope">
|
||||
<nuxt-link :to="{ path: '/', query: { hashtag: encodeURI(scope.row.id) } }">
|
||||
<b>#{{ scope.row.id | truncate(20) }}</b>
|
||||
</nuxt-link>
|
||||
|
||||
@ -19,8 +19,8 @@
|
||||
</base-card>
|
||||
<base-card v-if="User && User.length">
|
||||
<ds-table :data="User" :fields="fields" condensed>
|
||||
<template slot="index" slot-scope="scope">{{ scope.row.index + 1 }}.</template>
|
||||
<template slot="name" slot-scope="scope">
|
||||
<template #index="scope">{{ scope.row.index + 1 }}.</template>
|
||||
<template #name="scope">
|
||||
<nuxt-link
|
||||
:to="{
|
||||
name: 'profile-id-slug',
|
||||
@ -30,12 +30,12 @@
|
||||
<b>{{ scope.row.name | truncate(20) }}</b>
|
||||
</nuxt-link>
|
||||
</template>
|
||||
<template slot="email" slot-scope="scope">
|
||||
<template #email="scope">
|
||||
<a :href="`mailto:${scope.row.email}`">
|
||||
<b>{{ scope.row.email }}</b>
|
||||
</a>
|
||||
</template>
|
||||
<template slot="slug" slot-scope="scope">
|
||||
<template #slug="scope">
|
||||
<nuxt-link
|
||||
:to="{
|
||||
name: 'profile-id-slug',
|
||||
@ -45,7 +45,7 @@
|
||||
<b>{{ scope.row.slug | truncate(20) }}</b>
|
||||
</nuxt-link>
|
||||
</template>
|
||||
<template slot="createdAt" slot-scope="scope">
|
||||
<template #createdAt="scope">
|
||||
{{ scope.row.createdAt | dateTime }}
|
||||
</template>
|
||||
</ds-table>
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
<masonry-grid-item
|
||||
v-for="post in posts"
|
||||
:key="post.id"
|
||||
:imageAspectRatio="post.imageAspectRatio"
|
||||
:imageAspectRatio="post.image && post.image.aspectRatio"
|
||||
>
|
||||
<post-teaser
|
||||
:post="post"
|
||||
@ -130,6 +130,7 @@ export default {
|
||||
return this.$apollo.loading || (this.posts && this.posts.length > 0)
|
||||
},
|
||||
},
|
||||
watchQuery: ['hashtag'],
|
||||
methods: {
|
||||
...mapMutations({
|
||||
selectOrder: 'posts/SELECT_ORDER',
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
>
|
||||
<template #heroImage v-if="post.image">
|
||||
<img :src="post.image | proxyApiUrl" class="image" />
|
||||
<aside v-show="post.imageBlurred" class="blur-toggle">
|
||||
<aside v-show="post.image && post.image.sensitive" class="blur-toggle">
|
||||
<img v-show="blurred" :src="post.image | proxyApiUrl" class="preview" />
|
||||
<base-button
|
||||
:icon="blurred ? 'eye' : 'eye-slash'"
|
||||
@ -235,8 +235,9 @@ export default {
|
||||
update({ Post }) {
|
||||
this.post = Post[0] || {}
|
||||
this.title = this.post.title
|
||||
this.blurred = this.post.imageBlurred
|
||||
const { image } = this.post
|
||||
this.postAuthor = this.post.author
|
||||
this.blurred = image && image.sensitive
|
||||
},
|
||||
fetchPolicy: 'cache-and-network',
|
||||
},
|
||||
|
||||
@ -238,7 +238,7 @@
|
||||
<masonry-grid-item
|
||||
v-for="post in posts"
|
||||
:key="post.id"
|
||||
:imageAspectRatio="post.imageAspectRatio"
|
||||
:imageAspectRatio="post.image && post.image.aspectRatio"
|
||||
>
|
||||
<post-teaser
|
||||
:post="post"
|
||||
|
||||
@ -48,7 +48,7 @@ describe('blocked-users.vue', () => {
|
||||
|
||||
describe('given a list of blocked users', () => {
|
||||
beforeEach(() => {
|
||||
const blockedUsers = [{ id: 'u1', name: 'John Doe', slug: 'john-doe', avatar: '' }]
|
||||
const blockedUsers = [{ id: 'u1', name: 'John Doe', slug: 'john-doe' }]
|
||||
wrapper.setData({ blockedUsers })
|
||||
})
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<ds-form v-model="form" :schema="formSchema" @submit="submit">
|
||||
<template slot-scope="{ errors }">
|
||||
<template #default="{ errors }">
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('settings.data.name') }}</h2>
|
||||
<ds-input
|
||||
|
||||
@ -48,7 +48,7 @@ describe('muted-users.vue', () => {
|
||||
|
||||
describe('given a list of muted users', () => {
|
||||
beforeEach(() => {
|
||||
const mutedUsers = [{ id: 'u1', name: 'John Doe', slug: 'john-doe', avatar: '' }]
|
||||
const mutedUsers = [{ id: 'u1', name: 'John Doe', slug: 'john-doe' }]
|
||||
wrapper.setData({ mutedUsers })
|
||||
})
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<ds-form v-model="form" :schema="formSchema" @submit="submit">
|
||||
<template slot-scope="{ errors }">
|
||||
<template #default="{ errors }">
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('settings.email.name') }}</h2>
|
||||
<ds-input
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
<ds-text v-html="submitMessage" />
|
||||
</base-card>
|
||||
<ds-form v-else v-model="form" :schema="formSchema" @submit="submit">
|
||||
<template slot-scope="{ errors }">
|
||||
<template #default="{ errors }">
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('settings.email.name') }}</h2>
|
||||
<ds-input
|
||||
|
||||
@ -81,7 +81,8 @@ export default ({ app = {} }) => {
|
||||
|
||||
return contentExcerpt
|
||||
},
|
||||
proxyApiUrl: url => {
|
||||
proxyApiUrl: input => {
|
||||
const url = input && (input.url || input)
|
||||
if (!url) return url
|
||||
return url.startsWith('/') ? url.replace('/', '/api/') : url
|
||||
},
|
||||
|
||||
@ -11,7 +11,9 @@ const currentUser = {
|
||||
name: 'Jenny Rostock',
|
||||
slug: 'jenny-rostock',
|
||||
email: 'user@example.org',
|
||||
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/mutu_krish/128.jpg',
|
||||
avatar: {
|
||||
url: 'https://s3.amazonaws.com/uifaces/faces/twitter/mutu_krish/128.jpg',
|
||||
},
|
||||
role: 'user',
|
||||
locale: 'de',
|
||||
}
|
||||
@ -125,7 +127,9 @@ describe('actions', () => {
|
||||
name: 'Jenny Rostock',
|
||||
slug: 'jenny-rostock',
|
||||
email: 'user@example.org',
|
||||
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/mutu_krish/128.jpg',
|
||||
avatar: {
|
||||
url: 'https://s3.amazonaws.com/uifaces/faces/twitter/mutu_krish/128.jpg',
|
||||
},
|
||||
role: 'user',
|
||||
locale: 'de',
|
||||
},
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
## We use single-file components
|
||||
|
||||
Each component lives in a single file, containing:
|
||||
|
||||
- its `template` (the DOM structure)
|
||||
- its `script` (including `props`, `data` and `methods` among other things)
|
||||
- its `style` (defining the look of the component)
|
||||
@ -10,6 +11,7 @@ Each component lives in a single file, containing:
|
||||
See the [Vue.js docs](https://vuejs.org/v2/guide/single-file-components.html) for more details.
|
||||
|
||||
Placed in the same folder are also:
|
||||
|
||||
- the test file (e.g. `MyComponent.spec.js`)
|
||||
- the storybook file (e.g. `MyComponent.story.js`)
|
||||
|
||||
@ -20,6 +22,7 @@ Vue.js allows us to define component props either as strings or as objects (with
|
||||
Also: only (and always!) define a `default` for props that are _not required_.
|
||||
|
||||
Why?
|
||||
|
||||
- it makes our code more robust – a warning will be shown when passing a wrong prop type
|
||||
- it clearly defines the component API and tells other developers how to use it
|
||||
|
||||
@ -43,11 +46,14 @@ For more complex use cases see the [official Vue.js documentation](https://vuejs
|
||||
## We use shorthands
|
||||
|
||||
For better readability we prefer
|
||||
|
||||
- `:something` over `v-bind:something`
|
||||
- `@click` over `v-on:click`
|
||||
- `#slotSame` over `v-slot:slotName`
|
||||
- `#default` over `v-slot`
|
||||
|
||||
Read more in the [official Vue.js docs](https://vuejs.org/v2/guide/syntax.html#Shorthands)
|
||||
Read more in the [official Vue.js docs](https://vuejs.org/v2/guide/syntax.html#Shorthands) (for [slots](https://vuejs.org/v2/guide/components-slots.html#Named-Slots-Shorthand))
|
||||
|
||||
## Recommended reads
|
||||
|
||||
The [Vue.js component style guide](https://pablohpsilva.github.io/vuejs-component-style-guide/#/?id=harness-your-component-props) offers a whole list of best-practices for writing Vue components.
|
||||
The [Vue.js component style guide](https://pablohpsilva.github.io/vuejs-component-style-guide/#/?id=harness-your-component-props) offers a whole list of best-practices for writing Vue components.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user