diff --git a/.travis.yml b/.travis.yml index f6fdffebf..70481b06a 100644 --- a/.travis.yml +++ b/.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: diff --git a/CHANGELOG.md b/CHANGELOG.md index bcf2da9c3..a6e517fce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,87 @@ 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.6.0](https://github.com/Human-Connection/Human-Connection/compare/v0.4.2...v0.6.0) + +> 27 March 2020 + +- refactor: FilterMenu as a first step to remove ds-flex [`#3257`](https://github.com/Human-Connection/Human-Connection/pull/3257) +- build(deps): bump date-fns from 2.11.0 to 2.11.1 in /webapp [`#3355`](https://github.com/Human-Connection/Human-Connection/pull/3355) +- Remove dangling image urls [`#3343`](https://github.com/Human-Connection/Human-Connection/pull/3343) +- build(deps-dev): bump expect from 25.2.0 to 25.2.3 [`#3347`](https://github.com/Human-Connection/Human-Connection/pull/3347) +- build(deps-dev): bump babel-jest from 25.2.1 to 25.2.3 in /backend [`#3350`](https://github.com/Human-Connection/Human-Connection/pull/3350) +- build(deps-dev): bump babel-jest from 25.2.0 to 25.2.3 in /webapp [`#3353`](https://github.com/Human-Connection/Human-Connection/pull/3353) +- build(deps): bump date-fns from 2.11.0 to 2.11.1 in /backend [`#3351`](https://github.com/Human-Connection/Human-Connection/pull/3351) +- build(deps): bump @sentry/node from 5.15.0 to 5.15.2 in /backend [`#3348`](https://github.com/Human-Connection/Human-Connection/pull/3348) +- build(deps-dev): bump jest from 25.2.0 to 25.2.3 in /backend [`#3346`](https://github.com/Human-Connection/Human-Connection/pull/3346) +- build(deps-dev): bump date-fns from 2.11.0 to 2.11.1 [`#3345`](https://github.com/Human-Connection/Human-Connection/pull/3345) +- build(deps-dev): bump @babel/preset-env from 7.8.7 to 7.9.0 [`#3309`](https://github.com/Human-Connection/Human-Connection/pull/3309) +- build(deps-dev): bump @babel/preset-env from 7.8.7 to 7.9.0 in /backend [`#3314`](https://github.com/Human-Connection/Human-Connection/pull/3314) +- build(deps-dev): bump babel-jest from 25.2.0 to 25.2.1 in /backend [`#3341`](https://github.com/Human-Connection/Human-Connection/pull/3341) +- build(deps-dev): bump jest from 25.1.0 to 25.2.1 in /webapp [`#3342`](https://github.com/Human-Connection/Human-Connection/pull/3342) +- feat(backend): upload original image files on S3 object storage [`#3262`](https://github.com/Human-Connection/Human-Connection/pull/3262) +- build(deps-dev): bump babel-jest from 25.1.0 to 25.2.0 in /backend [`#3336`](https://github.com/Human-Connection/Human-Connection/pull/3336) +- build(deps): bump nuxt from 2.12.0 to 2.12.1 in /webapp [`#3338`](https://github.com/Human-Connection/Human-Connection/pull/3338) +- build(deps-dev): bump babel-jest from 25.1.0 to 25.2.0 in /webapp [`#3340`](https://github.com/Human-Connection/Human-Connection/pull/3340) +- build(deps-dev): bump jest from 25.1.0 to 25.2.0 in /backend [`#3335`](https://github.com/Human-Connection/Human-Connection/pull/3335) +- build(deps-dev): bump expect from 25.1.0 to 25.2.0 [`#3333`](https://github.com/Human-Connection/Human-Connection/pull/3333) +- build(deps): bump @nuxtjs/sentry from 3.3.1 to 4.0.0 in /webapp [`#3332`](https://github.com/Human-Connection/Human-Connection/pull/3332) +- build(deps-dev): bump @babel/core from 7.8.7 to 7.9.0 in /webapp [`#3316`](https://github.com/Human-Connection/Human-Connection/pull/3316) +- build(deps-dev): bump @babel/core from 7.8.7 to 7.9.0 in /backend [`#3313`](https://github.com/Human-Connection/Human-Connection/pull/3313) +- build(deps-dev): bump @babel/preset-env from 7.8.7 to 7.9.0 in /webapp [`#3319`](https://github.com/Human-Connection/Human-Connection/pull/3319) +- build(deps-dev): bump @babel/register from 7.8.6 to 7.9.0 in /backend [`#3305`](https://github.com/Human-Connection/Human-Connection/pull/3305) +- build(deps): bump @sentry/node from 5.14.2 to 5.15.0 in /backend [`#3331`](https://github.com/Human-Connection/Human-Connection/pull/3331) +- build(deps-dev): bump prettier from 1.19.1 to 2.0.2 in /webapp [`#3327`](https://github.com/Human-Connection/Human-Connection/pull/3327) +- build(deps-dev): bump babel-loader from 8.0.6 to 8.1.0 in /webapp [`#3315`](https://github.com/Human-Connection/Human-Connection/pull/3315) +- build(deps-dev): bump @babel/register from 7.8.6 to 7.9.0 [`#3310`](https://github.com/Human-Connection/Human-Connection/pull/3310) +- build(deps-dev): bump @babel/core from 7.8.7 to 7.9.0 [`#3304`](https://github.com/Human-Connection/Human-Connection/pull/3304) +- fix(migration): return null for Image.url [`#3323`](https://github.com/Human-Connection/Human-Connection/pull/3323) +- build(deps): bump graphql-shield from 7.2.0 to 7.2.1 in /backend [`#3296`](https://github.com/Human-Connection/Human-Connection/pull/3296) +- fix(build): make locations spec more resilient [`#3324`](https://github.com/Human-Connection/Human-Connection/pull/3324) +- fix(migration): Avoid unique constraint violation [`#3303`](https://github.com/Human-Connection/Human-Connection/pull/3303) +- chore: upgrade to v0.5.0 [`#3302`](https://github.com/Human-Connection/Human-Connection/pull/3302) +- build(deps): bump validator from 12.2.0 to 13.0.0 in /webapp [`#3301`](https://github.com/Human-Connection/Human-Connection/pull/3301) +- 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) +- Update prettier to v2 [`276ea79`](https://github.com/Human-Connection/Human-Connection/commit/276ea79e8ff2de2d02698b486671aee7cfda860e) +- refactor: CategoriesFilter to not use ds-flex [`1ffde6b`](https://github.com/Human-Connection/Human-Connection/commit/1ffde6bf1034d1a1d9b7bca62fe66fe64527314c) +- Changes requested by @mattwr18 [`9c08db2`](https://github.com/Human-Connection/Human-Connection/commit/9c08db22dcd0ca1ad6e59be8fb0f287935b45537) + #### [v0.4.2](https://github.com/Human-Connection/Human-Connection/compare/v0.4.1...v0.4.2) > 12 March 2020 @@ -230,7 +311,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - refactor TeaserImage component [`e14cbf8`](https://github.com/Human-Connection/Human-Connection/commit/e14cbf8173e3040b5285ba6a5c73e2d2d2a47860) - refactor DeleteData template and CSS [`509892b`](https://github.com/Human-Connection/Human-Connection/commit/509892b6caee6c4ca8384fb0090122ced98edfd4) -#### [v0.3.0](https://github.com/Human-Connection/Human-Connection/compare/v0.2.1...v0.3.0) +#### [v0.3.0](https://github.com/Human-Connection/Human-Connection/compare/0.2.2...v0.3.0) > 31 January 2020 @@ -298,6 +379,15 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - build(deps-dev): bump jest from 24.9.0 to 25.1.0 in /backend [`#2862`](https://github.com/Human-Connection/Human-Connection/pull/2862) - docs(deployment): Explain how to setup metrics [`#2825`](https://github.com/Human-Connection/Human-Connection/pull/2825) - refactor(styleguide): Migrate Avatar component to monorepo [`#2700`](https://github.com/Human-Connection/Human-Connection/pull/2700) +- docs(deployment): Explain how to setup metrics (#2825) [`#2411`](https://github.com/Human-Connection/Human-Connection/issues/2411) [`#2777`](https://github.com/Human-Connection/Human-Connection/issues/2777) +- Get rid of different factory files [`fc36729`](https://github.com/Human-Connection/Human-Connection/commit/fc367297e3e054f09b7f8f31788ab68d87f6babf) +- Refactor factory for comments [`2fc71d7`](https://github.com/Human-Connection/Human-Connection/commit/2fc71d75a5d5eab9c3467e94e00257ef6dd7d8a0) +- Refactor user factory [`2a79c53`](https://github.com/Human-Connection/Human-Connection/commit/2a79c53765b73f9b91691eb75f55cf8c9e48306e) + +#### [0.2.2](https://github.com/Human-Connection/Human-Connection/compare/0.2.1...0.2.2) + +> 21 January 2020 + - feat: Convert block/unblock to mute/unmute [`#2686`](https://github.com/Human-Connection/Human-Connection/pull/2686) - removed obsolete German keys [`#2845`](https://github.com/Human-Connection/Human-Connection/pull/2845) - build(deps-dev): bump @vue/server-test-utils in /webapp [`#2852`](https://github.com/Human-Connection/Human-Connection/pull/2852) @@ -318,6 +408,14 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - build(deps): bump metascraper-title from 5.10.3 to 5.10.5 in /backend [`#2835`](https://github.com/Human-Connection/Human-Connection/pull/2835) - build(deps): bump metascraper-publisher in /backend [`#2836`](https://github.com/Human-Connection/Human-Connection/pull/2836) - build(deps): bump metascraper-audio from 5.10.3 to 5.10.5 in /backend [`#2840`](https://github.com/Human-Connection/Human-Connection/pull/2840) +- Generate changelog with auto-changelog [`02367f9`](https://github.com/Human-Connection/Human-Connection/commit/02367f93e0ad635d5f43adf01695f85f06f4c0d2) +- clean up UserAvatar a little [`d20421c`](https://github.com/Human-Connection/Human-Connection/commit/d20421cb70c4bda51fdaf59f28c3a060b59bb0cd) +- display username below slug when there is no date-time [`e93332b`](https://github.com/Human-Connection/Human-Connection/commit/e93332b17361d2c59a841e7c03e93647ec76918f) + +#### [0.2.1](https://github.com/Human-Connection/Human-Connection/compare/v0.2.1...0.2.1) + +> 20 January 2020 + - build(deps): bump metascraper-author from 5.10.3 to 5.10.5 in /backend [`#2838`](https://github.com/Human-Connection/Human-Connection/pull/2838) - build(deps): bump metascraper-url from 5.10.3 to 5.10.5 in /backend [`#2832`](https://github.com/Human-Connection/Human-Connection/pull/2832) - build(deps): bump metascraper-lang from 5.10.3 to 5.10.5 in /backend [`#2831`](https://github.com/Human-Connection/Human-Connection/pull/2831) @@ -404,13 +502,12 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Update yarn.lock after dependabot update [`#2724`](https://github.com/Human-Connection/Human-Connection/pull/2724) - build(deps): bump @nuxtjs/axios from 5.8.0 to 5.9.2 in /webapp [`#2657`](https://github.com/Human-Connection/Human-Connection/pull/2657) - Update to version 0.2.1 [`#2722`](https://github.com/Human-Connection/Human-Connection/pull/2722) -- docs(deployment): Explain how to setup metrics (#2825) [`#2411`](https://github.com/Human-Connection/Human-Connection/issues/2411) [`#2777`](https://github.com/Human-Connection/Human-Connection/issues/2777) - refactor(modules): Various import fixes [`#2773`](https://github.com/Human-Connection/Human-Connection/issues/2773) [`#2774`](https://github.com/Human-Connection/Human-Connection/issues/2774) - feat(webapp): Display deployed version in footer [`#1831`](https://github.com/Human-Connection/Human-Connection/issues/1831) - fix #2229 [`#2229`](https://github.com/Human-Connection/Human-Connection/issues/2229) -- Get rid of different factory files [`fc36729`](https://github.com/Human-Connection/Human-Connection/commit/fc367297e3e054f09b7f8f31788ab68d87f6babf) -- Refactor factory for comments [`2fc71d7`](https://github.com/Human-Connection/Human-Connection/commit/2fc71d75a5d5eab9c3467e94e00257ef6dd7d8a0) -- Refactor user factory [`2a79c53`](https://github.com/Human-Connection/Human-Connection/commit/2a79c53765b73f9b91691eb75f55cf8c9e48306e) +- build(deps-dev): bump @storybook/addon-actions in /webapp [`d0124bf`](https://github.com/Human-Connection/Human-Connection/commit/d0124bf2b4b4a641c9af76d6d2f7b5aa075ade90) +- refactor: Make `db:setup` init stage of `migrate` [`b063847`](https://github.com/Human-Connection/Human-Connection/commit/b063847849a84db885337dc8e84e75ddaf87011f) +- refactor and use base-button in SearchableInput [`fcbe612`](https://github.com/Human-Connection/Human-Connection/commit/fcbe6125f35c0dd23e2ba1ae63f539f5ef5990ea) #### [v0.2.1](https://github.com/Human-Connection/Human-Connection/compare/v0.2.0...v0.2.1) diff --git a/backend/.env.template b/backend/.env.template index b4c91da9a..8531e6cd7 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -17,3 +17,9 @@ PRIVATE_KEY_PASSPHRASE="a7dsf78sadg87ad87sfagsadg78" SENTRY_DSN_BACKEND= COMMIT= PUBLIC_REGISTRATION=false + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_ENDPOINT= +AWS_REGION= +AWS_BUCKET= diff --git a/backend/package.json b/backend/package.json index 83e98b1c9..28a5a2795 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "human-connection-backend", - "version": "0.4.2", + "version": "0.6.0", "description": "GraphQL Backend for Human Connection", "main": "src/index.js", "scripts": { @@ -37,20 +37,21 @@ ] }, "dependencies": { - "@hapi/joi": "^17.1.0", - "@sentry/node": "^5.14.1", + "@hapi/joi": "^17.1.1", + "@sentry/node": "^5.15.4", "apollo-cache-inmemory": "~1.6.5", "apollo-client": "~2.6.8", "apollo-link-context": "~1.0.19", "apollo-link-http": "~1.5.16", "apollo-server": "~2.11.0", "apollo-server-express": "^2.11.0", + "aws-sdk": "^2.650.0", "babel-plugin-transform-runtime": "^6.23.0", "bcryptjs": "~2.4.3", "cheerio": "~1.0.0-rc.3", "cors": "~2.8.5", "cross-env": "~7.0.2", - "date-fns": "2.10.0", + "date-fns": "2.11.1", "debug": "~4.1.1", "dotenv": "~8.2.0", "express": "^4.17.1", @@ -61,9 +62,9 @@ "graphql-middleware": "~4.0.2", "graphql-middleware-sentry": "^3.2.1", "graphql-redis-subscriptions": "^2.2.1", - "graphql-shield": "~7.2.0", + "graphql-shield": "~7.2.2", "graphql-tag": "~2.10.3", - "helmet": "~3.21.3", + "helmet": "~3.22.0", "ioredis": "^4.16.0", "jsonwebtoken": "~8.5.1", "linkifyjs": "~2.1.8", @@ -80,56 +81,57 @@ "metascraper-lang-detector": "^4.10.2", "metascraper-logo": "^5.11.6", "metascraper-publisher": "^5.11.6", - "metascraper-soundcloud": "^5.11.6", + "metascraper-soundcloud": "^5.11.7", "metascraper-title": "^5.11.6", "metascraper-url": "^5.11.6", "metascraper-video": "^5.11.6", "metascraper-youtube": "^5.11.6", "migrate": "^1.6.2", + "mime-types": "^2.1.26", "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", - "nodemailer": "^6.4.5", + "nodemailer": "^6.4.4", "nodemailer-html-to-text": "^3.1.0", "npm-run-all": "~4.1.5", "request": "~2.88.2", - "sanitize-html": "~1.22.1", + "sanitize-html": "~1.22.0", "slug": "~2.1.1", "subscriptions-transport-ws": "^0.9.16", "trunc-html": "~1.1.2", - "uuid": "~7.0.2", - "validator": "^12.2.0", + "uuid": "~7.0.3", + "validator": "^13.0.0", "wait-on": "~4.0.1", "xregexp": "^4.3.0" }, "devDependencies": { "@babel/cli": "~7.8.4", - "@babel/core": "~7.8.7", + "@babel/core": "~7.9.0", "@babel/node": "~7.8.7", "@babel/plugin-proposal-throw-expressions": "^7.8.3", - "@babel/preset-env": "~7.8.7", - "@babel/register": "^7.8.6", + "@babel/preset-env": "~7.9.0", + "@babel/register": "^7.9.0", "apollo-server-testing": "~2.11.0", "babel-core": "~7.0.0-0", "babel-eslint": "~10.1.0", - "babel-jest": "~25.1.0", + "babel-jest": "~25.2.4", "chai": "~4.2.0", "cucumber": "~6.0.5", "eslint": "~6.8.0", - "eslint-config-prettier": "~6.10.0", - "eslint-config-standard": "~14.1.0", - "eslint-plugin-import": "~2.20.1", + "eslint-config-prettier": "~6.10.1", + "eslint-config-standard": "~14.1.1", + "eslint-plugin-import": "~2.20.2", "eslint-plugin-jest": "~23.8.2", - "eslint-plugin-node": "~11.0.0", + "eslint-plugin-node": "~11.1.0", "eslint-plugin-prettier": "~3.1.2", "eslint-plugin-promise": "~4.2.1", "eslint-plugin-standard": "~4.0.1", - "jest": "~25.1.0", + "jest": "~25.2.4", "nodemon": "~2.0.2", - "prettier": "~1.19.1", + "prettier": "~2.0.2", "rosie": "^2.0.1", "supertest": "~4.0.2" }, diff --git a/backend/src/activitypub/ActivityPub.js b/backend/src/activitypub/ActivityPub.js index 1794c2d3b..7ecc9d3fe 100644 --- a/backend/src/activitypub/ActivityPub.js +++ b/backend/src/activitypub/ActivityPub.js @@ -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') @@ -202,14 +202,14 @@ export default class ActivityPub { debug('is public addressed') const sharedInboxEndpoints = await this.dataSource.getSharedInboxEndpoints() // serve shared inbox endpoints - sharedInboxEndpoints.map(sharedInbox => { + sharedInboxEndpoints.map((sharedInbox) => { return this.trySend(activity, fromName, new URL(sharedInbox).host, sharedInbox) }) - activity.to = activity.to.filter(recipient => { + activity.to = activity.to.filter((recipient) => { return !isPublicAddressed({ to: recipient }) }) // serve the rest - activity.to.map(async recipient => { + activity.to.map(async (recipient) => { debug('serve rest') const actorObject = await this.getActorObject(recipient) return this.trySend(activity, fromName, new URL(recipient).host, actorObject.inbox) @@ -219,7 +219,7 @@ export default class ActivityPub { const actorObject = await this.getActorObject(activity.to) return this.trySend(activity, fromName, new URL(activity.to).host, actorObject.inbox) } else if (Array.isArray(activity.to)) { - activity.to.map(async recipient => { + activity.to.map(async (recipient) => { const actorObject = await this.getActorObject(recipient) return this.trySend(activity, fromName, new URL(recipient).host, actorObject.inbox) }) @@ -231,7 +231,7 @@ export default class ActivityPub { return await signAndSend(activity, fromName, host, url) } catch (e) { if (tries > 0) { - setTimeout(function() { + setTimeout(function () { return this.trySend(activity, fromName, host, url, --tries) }, 20000) } diff --git a/backend/src/activitypub/NitroDataSource.js b/backend/src/activitypub/NitroDataSource.js index dfdbf6c14..2df895f58 100644 --- a/backend/src/activitypub/NitroDataSource.js +++ b/backend/src/activitypub/NitroDataSource.js @@ -99,7 +99,7 @@ export default class NitroDataSource { followersCollection.totalItems = followersCount debug(`followers = ${JSON.stringify(followers, null, 2)}`) await Promise.all( - followers.map(async follower => { + followers.map(async (follower) => { followersCollection.orderedItems.push(constructIdFromName(follower.slug)) }), ) @@ -161,7 +161,7 @@ export default class NitroDataSource { followingCollection.totalItems = followingCount await Promise.all( - following.map(async user => { + following.map(async (user) => { followingCollection.orderedItems.push(await constructIdFromName(user.slug)) }), ) @@ -238,7 +238,7 @@ export default class NitroDataSource { const outboxCollection = createOrderedCollectionPage(slug, 'outbox') outboxCollection.totalItems = posts.length await Promise.all( - posts.map(async post => { + posts.map(async (post) => { outboxCollection.orderedItems.push( await createArticleObject( post.activityId, @@ -283,7 +283,7 @@ export default class NitroDataSource { orderedItems = onlyNewestItem ? [orderedItems.pop()] : orderedItems return Promise.all( - orderedItems.map(async follower => { + orderedItems.map(async (follower) => { debug(`follower = ${follower}`) const fromUserId = await this.ensureUser(follower) debug(`fromUserId = ${fromUserId}`) @@ -311,7 +311,7 @@ export default class NitroDataSource { const fromUserId = await this.ensureUser(constructIdFromName(fromUserName)) orderedItems = onlyNewestItem ? [orderedItems.pop()] : orderedItems return Promise.all( - orderedItems.map(async following => { + orderedItems.map(async (following) => { debug(`follower = ${following}`) const toUserId = await this.ensureUser(following) debug(`fromUserId = ${fromUserId}`) @@ -343,10 +343,7 @@ export default class NitroDataSource { } const title = postObject.summary ? postObject.summary - : postObject.content - .split(' ') - .slice(0, 5) - .join(' ') + : postObject.content.split(' ').slice(0, 5).join(' ') const postId = extractIdFromActivityId(postObject.id) debug('inside create post') let result = await this.client.mutate({ @@ -560,10 +557,7 @@ export default class NitroDataSource { debug('ensureUser: user not exists.. createUser') // user does not exist.. create it const pw = crypto.randomBytes(16).toString('hex') - const slug = name - .toLowerCase() - .split(' ') - .join('-') + const slug = name.toLowerCase().split(' ').join('-') const result = await this.client.mutate({ mutation: gql` mutation { diff --git a/backend/src/activitypub/routes/inbox.js b/backend/src/activitypub/routes/inbox.js index 18f1890af..f0f88f7e6 100644 --- a/backend/src/activitypub/routes/inbox.js +++ b/backend/src/activitypub/routes/inbox.js @@ -7,7 +7,7 @@ const router = express.Router() // Shared Inbox endpoint (federated Server) // For now its only able to handle Note Activities!! -router.post('/', async function(req, res, next) { +router.post('/', async function (req, res, next) { debug(`Content-Type = ${req.get('Content-Type')}`) debug(`body = ${JSON.stringify(req.body, null, 2)}`) debug(`Request headers = ${JSON.stringify(req.headers, null, 2)}`) diff --git a/backend/src/activitypub/routes/index.js b/backend/src/activitypub/routes/index.js index fb4037004..00ba7c22d 100644 --- a/backend/src/activitypub/routes/index.js +++ b/backend/src/activitypub/routes/index.js @@ -4,7 +4,7 @@ import express from 'express' import cors from 'cors' import verify from './verify' -export default function() { +export default function () { const router = express.Router() router.use( '/activitypub/users', diff --git a/backend/src/activitypub/routes/serveUser.js b/backend/src/activitypub/routes/serveUser.js index 6f4472235..dd7d80811 100644 --- a/backend/src/activitypub/routes/serveUser.js +++ b/backend/src/activitypub/routes/serveUser.js @@ -21,7 +21,7 @@ export async function serveUser(req, res, next) { } `, }) - .catch(reason => { + .catch((reason) => { debug(`serveUser User fetch error: ${reason}`) }) diff --git a/backend/src/activitypub/routes/user.js b/backend/src/activitypub/routes/user.js index 84486fd19..8dfdbc91d 100644 --- a/backend/src/activitypub/routes/user.js +++ b/backend/src/activitypub/routes/user.js @@ -7,7 +7,7 @@ import verify from './verify' const router = express.Router() const debug = require('debug')('ea:user') -router.get('/:name', async function(req, res, next) { +router.get('/:name', async function (req, res, next) { debug('inside user.js -> serveUser') await serveUser(req, res, next) }) @@ -45,7 +45,7 @@ router.get('/:name/outbox', (req, res) => { } }) -router.post('/:name/inbox', verify, async function(req, res, next) { +router.post('/:name/inbox', verify, async function (req, res, next) { debug(`body = ${JSON.stringify(req.body, null, 2)}`) debug(`actorId = ${req.body.actor}`) // const result = await saveActorId(req.body.actor) diff --git a/backend/src/activitypub/routes/webfinger.js b/backend/src/activitypub/routes/webfinger.js index c2cb96a6d..e624d306a 100644 --- a/backend/src/activitypub/routes/webfinger.js +++ b/backend/src/activitypub/routes/webfinger.js @@ -5,7 +5,7 @@ import cors from 'cors' const debug = require('debug')('ea:webfinger') const regex = /acct:([a-z0-9_-]*)@([a-z0-9_-]*)/ -const createWebFinger = name => { +const createWebFinger = (name) => { const { host } = new URL(CONFIG.CLIENT_URI) return { subject: `acct:${name}@${host}`, @@ -30,11 +30,11 @@ export async function handler(req, res) { const session = req.app.get('driver').session() try { - const [slug] = await session.readTransaction(async t => { + const [slug] = await session.readTransaction(async (t) => { const result = await t.run('MATCH (u:User {slug: $slug}) RETURN u.slug AS slug', { slug: name, }) - return result.records.map(record => record.get('slug')) + return result.records.map((record) => record.get('slug')) }) if (!slug) return res.status(404).json({ @@ -52,7 +52,7 @@ export async function handler(req, res) { } } -export default function() { +export default function () { const router = express.Router() router.use('/webfinger', cors(), express.urlencoded({ extended: true }), handler) return router diff --git a/backend/src/activitypub/routes/webfinger.spec.js b/backend/src/activitypub/routes/webfinger.spec.js index 3cd9613e7..213c1ab33 100644 --- a/backend/src/activitypub/routes/webfinger.spec.js +++ b/backend/src/activitypub/routes/webfinger.spec.js @@ -13,7 +13,7 @@ const request = () => { res = { status, contentType } const req = { app: { - get: key => { + get: (key) => { return { driver, }[key] diff --git a/backend/src/activitypub/security/index.js b/backend/src/activitypub/security/index.js index 9e0a63d22..399bc7d9b 100644 --- a/backend/src/activitypub/security/index.js +++ b/backend/src/activitypub/security/index.js @@ -69,7 +69,7 @@ export function verifySignature(url, headers) { const usedHeaders = headersString.split(' ') const verifyHeaders = {} - Object.keys(headers).forEach(key => { + Object.keys(headers).forEach((key) => { if (usedHeaders.includes(key.toLowerCase())) { verifyHeaders[key.toLowerCase()] = headers[key] } @@ -119,7 +119,7 @@ function httpVerify(pubKey, signature, signingString, algorithm) { // This function can be used to extract the signature,headers,algorithm etc. out of the Signature Header. // Just pass what you want as key function extractKeyValueFromSignatureHeader(signatureHeader, key) { - const keyString = signatureHeader.split(',').filter(el => { + const keyString = signatureHeader.split(',').filter((el) => { return !!el.startsWith(key) })[0] diff --git a/backend/src/activitypub/utils/collection.js b/backend/src/activitypub/utils/collection.js index 29cf69ac2..9cb71fe39 100644 --- a/backend/src/activitypub/utils/collection.js +++ b/backend/src/activitypub/utils/collection.js @@ -60,13 +60,10 @@ export function sendCollection(collectionName, req, res) { function attachThenCatch(promise, res) { return promise - .then(collection => { - res - .status(200) - .contentType('application/activity+json') - .send(collection) + .then((collection) => { + res.status(200).contentType('application/activity+json').send(collection) }) - .catch(err => { + .catch((err) => { debug(`error getting a Collection: = ${err}`) res.status(500).end() }) diff --git a/backend/src/activitypub/utils/index.js b/backend/src/activitypub/utils/index.js index 5f26c635f..aa3ff2101 100644 --- a/backend/src/activitypub/utils/index.js +++ b/backend/src/activitypub/utils/index.js @@ -53,7 +53,7 @@ export function signAndSend(activity, fromName, targetDomain, url) { } `, }) - .then(result => { + .then((result) => { if (result.error) { reject(result.error) } else { diff --git a/backend/src/config/index.js b/backend/src/config/index.js index 398bc6ff2..4c81bb181 100644 --- a/backend/src/config/index.js +++ b/backend/src/config/index.js @@ -18,6 +18,11 @@ const { SMTP_PASSWORD, SENTRY_DSN_BACKEND, COMMIT, + AWS_ACCESS_KEY_ID, + AWS_SECRET_ACCESS_KEY, + AWS_ENDPOINT, + AWS_REGION, + AWS_BUCKET, NEO4J_URI = 'bolt://localhost:7687', NEO4J_USERNAME = 'neo4j', NEO4J_PASSWORD = 'neo4j', @@ -36,7 +41,7 @@ export const requiredConfigs = { if (require.resolve) { // are we in a nodejs environment? - Object.entries(requiredConfigs).map(entry => { + Object.entries(requiredConfigs).map((entry) => { if (!entry[1]) { throw new Error(`ERROR: "${entry[0]}" env variable is missing.`) } @@ -64,7 +69,20 @@ export const developmentConfigs = { } export const sentryConfigs = { SENTRY_DSN_BACKEND, COMMIT } -export const redisConfiig = { REDIS_DOMAIN, REDIS_PORT, REDIS_PASSWORD } +export const redisConfigs = { REDIS_DOMAIN, REDIS_PORT, REDIS_PASSWORD } + +const S3_CONFIGURED = + AWS_ACCESS_KEY_ID && AWS_SECRET_ACCESS_KEY && AWS_ENDPOINT && AWS_REGION && AWS_BUCKET + +export const s3Configs = { + AWS_ACCESS_KEY_ID, + AWS_SECRET_ACCESS_KEY, + AWS_ENDPOINT, + AWS_REGION, + AWS_BUCKET, + S3_CONFIGURED, +} + export default { ...requiredConfigs, ...smtpConfigs, @@ -72,5 +90,6 @@ export default { ...serverConfigs, ...developmentConfigs, ...sentryConfigs, - ...redisConfiig, + ...redisConfigs, + ...s3Configs, } diff --git a/backend/src/db/clean.js b/backend/src/db/clean.js index e658317e2..97a21a055 100644 --- a/backend/src/db/clean.js +++ b/backend/src/db/clean.js @@ -4,7 +4,7 @@ if (process.env.NODE_ENV === 'production') { throw new Error(`You cannot clean the database in production environment!`) } -;(async function() { +;(async function () { try { await cleanDatabase() console.log('Successfully deleted all nodes and relations!') // eslint-disable-line no-console diff --git a/backend/src/db/factories.js b/backend/src/db/factories.js index 159a71a62..1ebf063ff 100644 --- a/backend/src/db/factories.js +++ b/backend/src/db/factories.js @@ -4,14 +4,21 @@ 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() try { - await session.writeTransaction(transaction => { + await session.writeTransaction((transaction) => { return transaction.run( ` MATCH (everything) @@ -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', @@ -57,53 +73,63 @@ Factory.define('userWithoutEmailAddress') .attr('slug', ['slug', 'name'], (slug, name) => { return slug || slugify(name, { lower: true }) }) - .attr('encryptedPassword', ['password'], password => { + .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 }) Factory.define('post') .option('categoryIds', []) - .option('categories', ['categoryIds'], categoryIds => { - if (categoryIds.length) return Promise.all(categoryIds.map(id => neode.find('Category', id))) + .option('categories', ['categoryIds'], (categoryIds) => { + if (categoryIds.length) return Promise.all(categoryIds.map((id) => neode.find('Category', id))) // there must be at least one category return Promise.all([Factory.build('category')]) }) .option('tagIds', []) - .option('tags', ['tagIds'], tagIds => { - return Promise.all(tagIds.map(id => neode.find('Tag', id))) + .option('tags', ['tagIds'], (tagIds) => { + return Promise.all(tagIds.map((id) => neode.find('Tag', id))) }) .option('authorId', null) - .option('author', ['authorId'], authorId => { + .option('author', ['authorId'], (authorId) => { if (authorId) return neode.find('User', authorId) 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, imageAspectRatio: 1.333, }) - .attr('pinned', ['pinned'], pinned => { + .attr('pinned', ['pinned'], (pinned) => { // Convert false to null return pinned || null }) @@ -113,21 +139,23 @@ Factory.define('post') .attr('slug', ['slug', 'title'], (slug, title) => { return slug || slugify(title, { lower: true }) }) - .attr('language', ['language'], language => { + .attr('language', ['language'], (language) => { 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, ]) await Promise.all([ post.relateTo(author, 'author'), - Promise.all(categories.map(c => c.relateTo(post, 'post'))), - Promise.all(tags.map(t => t.relateTo(post, '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') @@ -137,12 +165,12 @@ Factory.define('post') Factory.define('comment') .option('postId', null) - .option('post', ['postId'], postId => { + .option('post', ['postId'], (postId) => { if (postId) return neode.find('Post', postId) return Factory.build('post') }) .option('authorId', null) - .option('author', ['authorId'], authorId => { + .option('author', ['authorId'], (authorId) => { if (authorId) return neode.find('User', authorId) return Factory.build('user') }) diff --git a/backend/src/db/migrate/store.js b/backend/src/db/migrate/store.js index 8bc73b511..f3f7e563f 100644 --- a/backend/src/db/migrate/store.js +++ b/backend/src/db/migrate/store.js @@ -6,13 +6,14 @@ class Store { const { driver } = neode const session = driver.session() // eslint-disable-next-line no-console - const writeTxResultPromise = session.writeTransaction(async txc => { + const writeTxResultPromise = session.writeTransaction(async (txc) => { await txc.run('CALL apoc.schema.assert({},{},true)') // drop all indices return Promise.all( [ 'CALL db.index.fulltext.createNodeIndex("post_fulltext_search",["Post"],["title", "content"])', 'CALL db.index.fulltext.createNodeIndex("user_fulltext_search",["User"],["name", "slug"])', - ].map(statement => txc.run(statement)), + 'CALL db.index.fulltext.createNodeIndex("tag_fulltext_search",["Tag"],["id"])', + ].map((statement) => txc.run(statement)), ) }) try { @@ -33,11 +34,11 @@ class Store { async load(next) { const driver = getDriver() const session = driver.session() - const readTxResultPromise = session.readTransaction(async txc => { + const readTxResultPromise = session.readTransaction(async (txc) => { const result = await txc.run( 'MATCH (migration:Migration) RETURN migration {.*} ORDER BY migration.timestamp DESC', ) - return result.records.map(r => r.get('migration')) + return result.records.map((r) => r.get('migration')) }) try { const migrations = await readTxResultPromise @@ -62,9 +63,9 @@ class Store { const driver = getDriver() const session = driver.session() const { migrations } = set - const writeTxResultPromise = session.writeTransaction(txc => { + const writeTxResultPromise = session.writeTransaction((txc) => { return Promise.all( - migrations.map(async migration => { + migrations.map(async (migration) => { const { title, description, timestamp } = migration const properties = { title, description, timestamp } const migrationResult = await txc.run( diff --git a/backend/src/db/migrate/template.js b/backend/src/db/migrate/template.js index 9adb0786d..72bfc9b1b 100644 --- a/backend/src/db/migrate/template.js +++ b/backend/src/db/migrate/template.js @@ -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() } diff --git a/backend/src/db/migrations/20200123150105-merge_duplicate_user_accounts.js b/backend/src/db/migrations/20200123150105-merge_duplicate_user_accounts.js index 377f971e9..0914d6b22 100644 --- a/backend/src/db/migrations/20200123150105-merge_duplicate_user_accounts.js +++ b/backend/src/db/migrations/20200123150105-merge_duplicate_user_accounts.js @@ -18,13 +18,13 @@ export function up(next) { rxSession .beginTransaction() .pipe( - flatMap(txc => + flatMap((txc) => concat( txc .run('MATCH (email:EmailAddress) RETURN email {.email}') .records() .pipe( - map(record => { + map((record) => { const { email } = record.get('email') const normalizedEmail = normalizeEmail(email) return { email, normalizedEmail } @@ -45,7 +45,7 @@ export function up(next) { ) .records() .pipe( - map(r => ({ + map((r) => ({ oldEmail: email, email: r.get('email'), user: r.get('user'), @@ -54,7 +54,7 @@ export function up(next) { }), ), txc.commit(), - ).pipe(catchError(err => txc.rollback().pipe(throwError(err)))), + ).pipe(catchError((err) => txc.rollback().pipe(throwError(err)))), ), ) .subscribe({ @@ -72,7 +72,7 @@ export function up(next) { console.log('Merging of duplicate users completed') next() }, - error: error => { + error: (error) => { next(new Error(error), null) }, }) diff --git a/backend/src/db/migrations/20200123150110-merge_duplicate_location_nodes.js b/backend/src/db/migrations/20200123150110-merge_duplicate_location_nodes.js index b2d6b260f..66560ec51 100644 --- a/backend/src/db/migrations/20200123150110-merge_duplicate_location_nodes.js +++ b/backend/src/db/migrations/20200123150110-merge_duplicate_location_nodes.js @@ -12,7 +12,7 @@ export function up(next) { rxSession .beginTransaction() .pipe( - flatMap(transaction => + flatMap((transaction) => concat( transaction .run( @@ -23,7 +23,7 @@ export function up(next) { ) .records() .pipe( - map(record => { + map((record) => { const { id: locationId } = record.get('location') return { locationId } }), @@ -40,7 +40,7 @@ export function up(next) { ) .records() .pipe( - map(record => ({ + map((record) => ({ location: record.get('location'), updatedLocation: record.get('updatedLocation'), })), @@ -48,7 +48,7 @@ export function up(next) { }), ), transaction.commit(), - ).pipe(catchError(error => transaction.rollback().pipe(throwError(error)))), + ).pipe(catchError((error) => transaction.rollback().pipe(throwError(error)))), ), ) .subscribe({ @@ -66,7 +66,7 @@ export function up(next) { console.log('Merging of duplicate locations completed') next() }, - error: error => { + error: (error) => { next(new Error(error), null) }, }) diff --git a/backend/src/db/migrations/20200206190233-swap_latitude_with_longitude.js b/backend/src/db/migrations/20200206190233-swap_latitude_with_longitude.js index 619e30320..94a2f442d 100644 --- a/backend/src/db/migrations/20200206190233-swap_latitude_with_longitude.js +++ b/backend/src/db/migrations/20200206190233-swap_latitude_with_longitude.js @@ -8,7 +8,7 @@ values lat=10.0 and lng=53.55, which is close to the horn of Africa, but it is lat=53.55 and lng=10.0 ` -const swap = async function(next) { +const swap = async function (next) { const driver = getDriver() const session = driver.session() const transaction = session.beginTransaction() diff --git a/backend/src/db/migrations/20200207080200-fulltext_index_for_tags.js b/backend/src/db/migrations/20200207080200-fulltext_index_for_tags.js new file mode 100644 index 000000000..ffcd3d4b6 --- /dev/null +++ b/backend/src/db/migrations/20200207080200-fulltext_index_for_tags.js @@ -0,0 +1,57 @@ +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) { + const { message } = error + if (message.includes('There already exists an index')) { + // all fine + // eslint-disable-next-line no-console + console.log(message) + next() + } else { + 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() + } +} diff --git a/backend/src/db/migrations/20200213230248-add_unique_index_to_image_url.js b/backend/src/db/migrations/20200213230248-add_unique_index_to_image_url.js new file mode 100644 index 000000000..4582d938c --- /dev/null +++ b/backend/src/db/migrations/20200213230248-add_unique_index_to_image_url.js @@ -0,0 +1,60 @@ +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) { + const { message } = error + if (message.includes('There already exists an index')) { + // all fine + // eslint-disable-next-line no-console + console.log(message) + next() + } else { + await transaction.rollback() + // eslint-disable-next-line no-console + console.log('rolled back') + throw new Error(error) + } + } finally { + session.close() + } +} + +export async function down(next) { + const driver = getDriver() + const session = driver.session() + const transaction = session.beginTransaction() + + try { + // Implement your migration here. + await transaction.run(` + DROP CONSTRAINT ON ( 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() + } +} diff --git a/backend/src/db/migrations/20200312140328-bulk_upload_to_s3.js b/backend/src/db/migrations/20200312140328-bulk_upload_to_s3.js new file mode 100644 index 000000000..ee9aba023 --- /dev/null +++ b/backend/src/db/migrations/20200312140328-bulk_upload_to_s3.js @@ -0,0 +1,107 @@ +import { getDriver } from '../../db/neo4j' +import { existsSync, createReadStream } from 'fs' +import path from 'path' +import { S3 } from 'aws-sdk' +import mime from 'mime-types' +import { s3Configs } from '../../config' +import https from 'https' + +export const description = ` +Upload all image files to a S3 compatible object storage in order to reduce +load on our backend. +` + +export async function up(next) { + const driver = getDriver() + const session = driver.session() + const transaction = session.beginTransaction() + const agent = new https.Agent({ + maxSockets: 5, + }) + + const { + AWS_ENDPOINT: endpoint, + AWS_REGION: region, + AWS_BUCKET: Bucket, + S3_CONFIGURED, + } = s3Configs + + if (!S3_CONFIGURED) { + // eslint-disable-next-line no-console + console.log('No S3 given, cannot upload image files') + return + } + + const s3 = new S3({ region, endpoint, httpOptions: { agent } }) + try { + // Implement your migration here. + const { records } = await transaction.run('MATCH (image:Image) RETURN image.url as url') + let urls = records.map((r) => r.get('url')) + urls = urls.filter((url) => url.startsWith('/uploads')) + const locations = await Promise.all( + urls + .map((url) => { + return async () => { + const { pathname } = new URL(url, 'http://example.org') + const fileLocation = path.join(__dirname, `../../../public/${pathname}`) + const s3Location = `original${pathname}` + if (existsSync(fileLocation)) { + const mimeType = mime.lookup(fileLocation) + const params = { + Bucket, + Key: s3Location, + ACL: 'public-read', + ContentType: mimeType || 'image/jpeg', + Body: createReadStream(fileLocation), + } + + const data = await s3.upload(params).promise() + const { Location: spacesUrl } = data + + const updatedRecord = await transaction.run( + 'MATCH (image:Image {url: $url}) SET image.url = $spacesUrl RETURN image.url as url', + { url, spacesUrl }, + ) + const [updatedUrl] = updatedRecord.records.map((record) => record.get('url')) + return updatedUrl + } + } + }) + .map((p) => p()), + ) + // eslint-disable-next-line no-console + console.log('this is locations', locations) + 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(``) + 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() + } +} diff --git a/backend/src/db/migrations/20200320200315-refactor_all_images_to_separate_type.js b/backend/src/db/migrations/20200320200315-refactor_all_images_to_separate_type.js new file mode 100644 index 000000000..1ad5e645d --- /dev/null +++ b/backend/src/db/migrations/20200320200315-refactor_all_images_to_separate_type.js @@ -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() + } +} diff --git a/backend/src/db/migrations/20200323140300-remove_deleted_users_obsolete_attributes.js b/backend/src/db/migrations/20200323140300-remove_deleted_users_obsolete_attributes.js new file mode 100644 index 000000000..e4852f79c --- /dev/null +++ b/backend/src/db/migrations/20200323140300-remove_deleted_users_obsolete_attributes.js @@ -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() +} diff --git a/backend/src/db/migrations/20200323160336-remove_deleted_posts_obsolete_attributes.js b/backend/src/db/migrations/20200323160336-remove_deleted_posts_obsolete_attributes.js new file mode 100644 index 000000000..8c1efe5c6 --- /dev/null +++ b/backend/src/db/migrations/20200323160336-remove_deleted_posts_obsolete_attributes.js @@ -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() +} diff --git a/backend/src/db/migrations/20200326160326-remove_dangling_image_urls.js b/backend/src/db/migrations/20200326160326-remove_dangling_image_urls.js new file mode 100644 index 000000000..a77ac360c --- /dev/null +++ b/backend/src/db/migrations/20200326160326-remove_dangling_image_urls.js @@ -0,0 +1,61 @@ +import { getDriver } from '../../db/neo4j' +import { existsSync } from 'fs' + +export const description = ` + In this review: + https://github.com/Human-Connection/Human-Connection/pull/3262#discussion_r398634249 + I brought up that we may have image nodes with danling urls (urls that don't + point to local file on disk). I would prefer to remove those urls to avoid + unnecessary 404 errors. +` + +export async function up(next) { + const driver = getDriver() + const session = driver.session() + const transaction = session.beginTransaction() + + try { + // Implement your migration here. + const { records } = await transaction.run(` + MATCH(image:Image) + WHERE image.url STARTS WITH '/' + RETURN image.url as url + `) + const urls = records.map((record) => record.get('url')) + const danglingUrls = urls.filter((url) => { + const fileLocation = `public${url}` + return !existsSync(fileLocation) + }) + await transaction.run( + ` + MATCH(image:Image) + WHERE image.url IN $danglingUrls + DETACH DELETE image + `, + { danglingUrls }, + ) + await transaction.commit() + if (danglingUrls.length) { + // eslint-disable-next-line no-console + console.log(` + Removed ${danglingUrls.length} dangling urls.\n + =============================================== + ${danglingUrls.join('\n')} + `) + } + 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() { + throw new Error('Irreversible migration') +} diff --git a/backend/src/db/seed.js b/backend/src/db/seed.js index 0de797314..45db31afb 100644 --- a/backend/src/db/seed.js +++ b/backend/src/db/seed.js @@ -9,7 +9,7 @@ import { gql } from '../helpers/jest' const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] /* eslint-disable no-multi-spaces */ -;(async function() { +;(async function () { let authenticatedUser = null const driver = getDriver() const neode = getNeode() @@ -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 #QuantenFlussTheorie can explain #QuantumGravity! @peter-lustig 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,12 +594,11 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] title: 'This is post #12', content: `${mention2} ${faker.lorem.paragraph()}`, categoryIds: ['cat12'], - imageAspectRatio: 300 / 200, }, }), ]) const [p2, p7, p8, p12] = await Promise.all( - ['p2', 'p7', 'p8', 'p12'].map(id => neode.find('Post', id)), + ['p2', 'p7', 'p8', 'p12'].map((id) => neode.find('Post', id)), ) authenticatedUser = null @@ -1020,12 +1014,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(), + }), }, ), ), @@ -1074,12 +1069,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(), + }), }, ), ), @@ -1128,12 +1124,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(), + }), }, ), ), @@ -1182,12 +1179,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(), + }), }, ), ), @@ -1236,12 +1234,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(), + }), }, ), ), @@ -1290,12 +1289,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(), + }), }, ), ), diff --git a/backend/src/helpers/encryptPassword.js b/backend/src/helpers/encryptPassword.js index ae98af84f..657dee98a 100644 --- a/backend/src/helpers/encryptPassword.js +++ b/backend/src/helpers/encryptPassword.js @@ -1,6 +1,6 @@ import { hashSync } from 'bcryptjs' -export default function(args) { +export default function (args) { args.encryptedPassword = hashSync(args.password, 10) delete args.password return args diff --git a/backend/src/helpers/walkRecursive.js b/backend/src/helpers/walkRecursive.js index db9a4c703..85900eeba 100644 --- a/backend/src/helpers/walkRecursive.js +++ b/backend/src/helpers/walkRecursive.js @@ -18,7 +18,7 @@ function walkRecursive(data, fields, callback, _key) { }) } else if (data && typeof data === 'object') { // lets get some keys and stir them - Object.keys(data).forEach(k => { + Object.keys(data).forEach((k) => { data[k] = walkRecursive(data[k], fields, callback, k) }) } diff --git a/backend/src/jwt/decode.js b/backend/src/jwt/decode.js index 5433a8c76..e02dcc8d4 100644 --- a/backend/src/jwt/decode.js +++ b/backend/src/jwt/decode.js @@ -13,17 +13,17 @@ export default async (driver, authorizationHeader) => { } const session = driver.session() - const writeTxResultPromise = session.writeTransaction(async transaction => { + 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 }, ) - return updateUserLastActiveTransactionResponse.records.map(record => record.get('user')) + return updateUserLastActiveTransactionResponse.records.map((record) => record.get('user')) }) try { const [currentUser] = await writeTxResultPromise diff --git a/backend/src/jwt/decode.spec.js b/backend/src/jwt/decode.spec.js index aa8ff0674..80dfe9733 100644 --- a/backend/src/jwt/decode.spec.js +++ b/backend/src/jwt/decode.spec.js @@ -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', }) }) diff --git a/backend/src/middleware/email/emailMiddleware.js b/backend/src/middleware/email/emailMiddleware.js index f92da3368..a69530582 100644 --- a/backend/src/middleware/email/emailMiddleware.js +++ b/backend/src/middleware/email/emailMiddleware.js @@ -18,7 +18,7 @@ if (!hasEmailConfig) { console.log('Warning: Email middleware will not try to send mails.') } } else { - sendMail = async templateArgs => { + sendMail = async (templateArgs) => { const transporter = nodemailer.createTransport({ host: CONFIG.SMTP_HOST, port: CONFIG.SMTP_PORT, diff --git a/backend/src/middleware/email/templates/index.js b/backend/src/middleware/email/templates/index.js index 594cae334..b8ae01bdb 100644 --- a/backend/src/middleware/email/templates/index.js +++ b/backend/src/middleware/email/templates/index.js @@ -1,7 +1,7 @@ import fs from 'fs' import path from 'path' -const readFile = fileName => fs.readFileSync(path.join(__dirname, fileName), 'utf-8') +const readFile = (fileName) => fs.readFileSync(path.join(__dirname, fileName), 'utf-8') export const signup = readFile('./signup.html') export const passwordReset = readFile('./resetPassword.html') diff --git a/backend/src/middleware/hashtags/extractHashtags.js b/backend/src/middleware/hashtags/extractHashtags.js index 9a903e4fa..4c7667557 100644 --- a/backend/src/middleware/hashtags/extractHashtags.js +++ b/backend/src/middleware/hashtags/extractHashtags.js @@ -8,7 +8,7 @@ import { exec, build } from 'xregexp/xregexp-all.js' // 2. If it starts with a digit '0-9' than a unicode letter has to follow. const regX = build('^((\\pL+[\\pL0-9]*)|([0-9]+\\pL+[\\pL0-9]*))$') -export default function(content) { +export default function (content) { if (!content) return [] const $ = cheerio.load(content) // We can not search for class '.hashtag', because the classes are removed at the 'xss' middleware. @@ -19,7 +19,7 @@ export default function(content) { }) .get() const hashtags = [] - ids.forEach(id => { + ids.forEach((id) => { const match = exec(id, regX) if (match != null) { hashtags.push(match[1]) diff --git a/backend/src/middleware/hashtags/hashtagsMiddleware.js b/backend/src/middleware/hashtags/hashtagsMiddleware.js index 7d8593fd5..985cd3c92 100644 --- a/backend/src/middleware/hashtags/hashtagsMiddleware.js +++ b/backend/src/middleware/hashtags/hashtagsMiddleware.js @@ -5,7 +5,7 @@ const updateHashtagsOfPost = async (postId, hashtags, context) => { const session = context.driver.session() try { - await session.writeTransaction(txc => { + await session.writeTransaction((txc) => { return txc.run( ` MATCH (post:Post { id: $postId}) diff --git a/backend/src/middleware/includedFieldsMiddleware.js b/backend/src/middleware/includedFieldsMiddleware.js index cd7a74f4e..fd95029b0 100644 --- a/backend/src/middleware/includedFieldsMiddleware.js +++ b/backend/src/middleware/includedFieldsMiddleware.js @@ -2,21 +2,21 @@ import cloneDeep from 'lodash/cloneDeep' const _includeFieldsRecursively = (selectionSet, includedFields) => { if (!selectionSet) return - includedFields.forEach(includedField => { + includedFields.forEach((includedField) => { selectionSet.selections.unshift({ kind: 'Field', name: { kind: 'Name', value: includedField }, }) }) - selectionSet.selections.forEach(selection => { + selectionSet.selections.forEach((selection) => { _includeFieldsRecursively(selection.selectionSet, includedFields) }) } -const includeFieldsRecursively = includedFields => { +const includeFieldsRecursively = (includedFields) => { return (resolve, root, args, context, resolveInfo) => { const copy = cloneDeep(resolveInfo) - copy.fieldNodes.forEach(fieldNode => { + copy.fieldNodes.forEach((fieldNode) => { _includeFieldsRecursively(fieldNode.selectionSet, includedFields) }) return resolve(root, args, context, copy) diff --git a/backend/src/middleware/index.js b/backend/src/middleware/index.js index 25195b1b5..83b0104ec 100644 --- a/backend/src/middleware/index.js +++ b/backend/src/middleware/index.js @@ -15,7 +15,7 @@ import hashtags from './hashtags/hashtagsMiddleware' import email from './email/emailMiddleware' import sentry from './sentryMiddleware' -export default schema => { +export default (schema) => { const middlewares = { sentry, permissions, @@ -51,7 +51,7 @@ export default schema => { // add permisions middleware at the first position (unless we're seeding) if (CONFIG.DISABLED_MIDDLEWARES) { const disabledMiddlewares = CONFIG.DISABLED_MIDDLEWARES.split(',') - order = order.filter(key => { + order = order.filter((key) => { if (disabledMiddlewares.includes(key)) { /* eslint-disable-next-line no-console */ console.log(`Warning: Disabled "${disabledMiddlewares}" middleware.`) @@ -60,6 +60,6 @@ export default schema => { }) } - const appliedMiddlewares = order.map(key => middlewares[key]) + const appliedMiddlewares = order.map((key) => middlewares[key]) return applyMiddleware(schema, ...appliedMiddlewares) } diff --git a/backend/src/middleware/notifications/mentions/extractMentionedUsers.js b/backend/src/middleware/notifications/mentions/extractMentionedUsers.js index 3ba845043..e7e23ace7 100644 --- a/backend/src/middleware/notifications/mentions/extractMentionedUsers.js +++ b/backend/src/middleware/notifications/mentions/extractMentionedUsers.js @@ -1,6 +1,6 @@ import cheerio from 'cheerio' -export default content => { +export default (content) => { if (!content) return [] const $ = cheerio.load(content) const userIds = $('a.mention[data-mention-id]') @@ -9,7 +9,7 @@ export default content => { }) .get() return userIds - .map(id => id.trim()) - .filter(id => !!id) + .map((id) => id.trim()) + .filter((id) => !!id) .filter((id, index, allIds) => allIds.indexOf(id) === index) } diff --git a/backend/src/middleware/notifications/notificationsMiddleware.js b/backend/src/middleware/notifications/notificationsMiddleware.js index fb440e723..9b8494145 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.js +++ b/backend/src/middleware/notifications/notificationsMiddleware.js @@ -7,7 +7,7 @@ const publishNotifications = async (...promises) => { const notifications = await Promise.all(promises) notifications .flat() - .forEach(notificationAdded => pubsub.publish(NOTIFICATION_ADDED, { notificationAdded })) + .forEach((notificationAdded) => pubsub.publish(NOTIFICATION_ADDED, { notificationAdded })) } const debug = require('debug')('human-connection-backend:notificationsMiddleware') @@ -28,7 +28,7 @@ const handleContentDataOfComment = async (resolve, root, args, context, resolveI let idsOfUsers = extractMentionedUsers(content) const comment = await resolve(root, args, context, resolveInfo) const [postAuthor] = await postAuthorOfComment(comment.id, { context }) - idsOfUsers = idsOfUsers.filter(id => id !== postAuthor.id) + idsOfUsers = idsOfUsers.filter((id) => id !== postAuthor.id) await publishNotifications( notifyUsersOfMention('Comment', comment.id, idsOfUsers, 'mentioned_in_comment', context), notifyUsersOfComment('Comment', comment.id, postAuthor.id, 'commented_on_post', context), @@ -40,7 +40,7 @@ const postAuthorOfComment = async (commentId, { context }) => { const session = context.driver.session() let postAuthorId try { - postAuthorId = await session.readTransaction(transaction => { + postAuthorId = await session.readTransaction((transaction) => { return transaction.run( ` MATCH (author:User)-[:WROTE]->(:Post)<-[:COMMENTS]-(:Comment { id: $commentId }) diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index fa88d9348..2c8d7ff63 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -47,7 +47,7 @@ const isAuthor = rule({ if (!user) return false const { id: resourceId } = args const session = driver.session() - const authorReadTxPromise = session.readTransaction(async transaction => { + const authorReadTxPromise = session.readTransaction(async (transaction) => { const authorTransactionResponse = await transaction.run( ` MATCH (resource {id: $resourceId})<-[:WROTE]-(author {id: $userId}) @@ -55,7 +55,7 @@ const isAuthor = rule({ `, { resourceId, userId: user.id }, ) - return authorTransactionResponse.records.map(record => record.get('author')) + return authorTransactionResponse.records.map((record) => record.get('author')) }) try { const [author] = await authorReadTxPromise diff --git a/backend/src/middleware/sluggifyMiddleware.js b/backend/src/middleware/sluggifyMiddleware.js index 1cd3c0b9c..165235be9 100644 --- a/backend/src/middleware/sluggifyMiddleware.js +++ b/backend/src/middleware/sluggifyMiddleware.js @@ -1,10 +1,10 @@ import uniqueSlug from './slugify/uniqueSlug' const isUniqueFor = (context, type) => { - return async slug => { + return async (slug) => { const session = context.driver.session() try { - const existingSlug = await session.readTransaction(transaction => { + const existingSlug = await session.readTransaction((transaction) => { return transaction.run( ` MATCH(p:${type} {slug: $slug }) diff --git a/backend/src/middleware/slugify/uniqueSlug.spec.js b/backend/src/middleware/slugify/uniqueSlug.spec.js index e34af86a1..ff14a56ef 100644 --- a/backend/src/middleware/slugify/uniqueSlug.spec.js +++ b/backend/src/middleware/slugify/uniqueSlug.spec.js @@ -9,10 +9,7 @@ describe('uniqueSlug', () => { it('increments slugified string until unique', () => { const string = 'Hello World' - const isUnique = jest - .fn() - .mockResolvedValueOnce(false) - .mockResolvedValueOnce(true) + const isUnique = jest.fn().mockResolvedValueOnce(false).mockResolvedValueOnce(true) expect(uniqueSlug(string, isUnique)).resolves.toEqual('hello-world-1') }) diff --git a/backend/src/middleware/slugifyMiddleware.spec.js b/backend/src/middleware/slugifyMiddleware.spec.js index df011b0a5..48c4fb651 100644 --- a/backend/src/middleware/slugifyMiddleware.spec.js +++ b/backend/src/middleware/slugifyMiddleware.spec.js @@ -121,7 +121,7 @@ describe('slugifyMiddleware', () => { }) describe('but if the client specifies a slug', () => { - it('rejects CreatePost', async done => { + it('rejects CreatePost', async (done) => { variables = { ...variables, title: 'Pre-existing post', diff --git a/backend/src/middleware/softDelete/softDeleteMiddleware.js b/backend/src/middleware/softDelete/softDeleteMiddleware.js index 8be8c3d39..2e1f60251 100644 --- a/backend/src/middleware/softDelete/softDeleteMiddleware.js +++ b/backend/src/middleware/softDelete/softDeleteMiddleware.js @@ -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) } diff --git a/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js b/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js index de5626d14..63569ddb0 100644 --- a/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js +++ b/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js @@ -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', () => { diff --git a/backend/src/middleware/validation/validationMiddleware.js b/backend/src/middleware/validation/validationMiddleware.js index 948e1a73a..d36e64846 100644 --- a/backend/src/middleware/validation/validationMiddleware.js +++ b/backend/src/middleware/validation/validationMiddleware.js @@ -14,7 +14,7 @@ const validateCreateComment = async (resolve, root, args, context, info) => { } const session = context.driver.session() try { - const postQueryRes = await session.readTransaction(transaction => { + const postQueryRes = await session.readTransaction((transaction) => { return transaction.run( ` MATCH (post:Post {id: $postId}) @@ -23,7 +23,7 @@ const validateCreateComment = async (resolve, root, args, context, info) => { { postId }, ) }) - const [post] = postQueryRes.records.map(record => { + const [post] = postQueryRes.records.map((record) => { return record.get('post') }) @@ -73,7 +73,7 @@ const validateReview = async (resolve, root, args, context, info) => { const { user, driver } = context if (resourceId === user.id) throw new Error('You cannot review yourself!') const session = driver.session() - const reportReadTxPromise = session.readTransaction(async transaction => { + const reportReadTxPromise = session.readTransaction(async (transaction) => { const validateReviewTransactionResponse = await transaction.run( ` MATCH (resource {id: $resourceId}) @@ -87,7 +87,7 @@ const validateReview = async (resolve, root, args, context, info) => { submitterId: user.id, }, ) - return validateReviewTransactionResponse.records.map(record => ({ + return validateReviewTransactionResponse.records.map((record) => ({ label: record.get('label'), author: record.get('author'), filed: record.get('filed'), diff --git a/backend/src/models/Image.js b/backend/src/models/Image.js new file mode 100644 index 000000000..19824b493 --- /dev/null +++ b/backend/src/models/Image.js @@ -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() }, +} diff --git a/backend/src/models/Post.js b/backend/src/models/Post.js index 24310b62e..14489e5ae 100644 --- a/backend/src/models/Post.js +++ b/backend/src/models/Post.js @@ -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 }, createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, @@ -25,8 +30,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', diff --git a/backend/src/models/User.js b/backend/src/models/User.js index 8a27447e9..0a09a718a 100644 --- a/backend/src/models/User.js +++ b/backend/src/models/User.js @@ -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' }, diff --git a/backend/src/models/User.spec.js b/backend/src/models/User.spec.js index a45a629e5..7d7f391b0 100644 --- a/backend/src/models/User.spec.js +++ b/backend/src/models/User.spec.js @@ -28,7 +28,7 @@ describe('slug', () => { ) }) - it('must be unique', async done => { + it('must be unique', async (done) => { await neode.create('User', { slug: 'Matt' }) try { await expect(neode.create('User', { slug: 'Matt' })).rejects.toThrow('already exists') @@ -52,8 +52,8 @@ describe('slug', () => { }) describe('characters', () => { - const createUser = attrs => { - return neode.create('User', attrs).then(user => user.toJson()) + const createUser = (attrs) => { + return neode.create('User', attrs).then((user) => user.toJson()) } it('-', async () => { diff --git a/backend/src/models/index.js b/backend/src/models/index.js index dbb6a927e..c53ef89ab 100644 --- a/backend/src/models/index.js +++ b/backend/src/models/index.js @@ -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, diff --git a/backend/src/schema/resolvers/comments.js b/backend/src/schema/resolvers/comments.js index 67a6675c9..a3a0c7290 100644 --- a/backend/src/schema/resolvers/comments.js +++ b/backend/src/schema/resolvers/comments.js @@ -15,7 +15,7 @@ export default { const session = driver.session() - const writeTxResultPromise = session.writeTransaction(async transaction => { + const writeTxResultPromise = session.writeTransaction(async (transaction) => { const createCommentTransactionResponse = await transaction.run( ` MATCH (post:Post {id: $postId}) @@ -30,7 +30,7 @@ export default { { userId: user.id, postId, params }, ) return createCommentTransactionResponse.records.map( - record => record.get('comment').properties, + (record) => record.get('comment').properties, ) }) try { @@ -42,7 +42,7 @@ export default { }, UpdateComment: async (_parent, params, context, _resolveInfo) => { const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async transaction => { + const writeTxResultPromise = session.writeTransaction(async (transaction) => { const updateCommentTransactionResponse = await transaction.run( ` MATCH (comment:Comment {id: $params.id}) @@ -53,7 +53,7 @@ export default { { params }, ) return updateCommentTransactionResponse.records.map( - record => record.get('comment').properties, + (record) => record.get('comment').properties, ) }) try { @@ -65,7 +65,7 @@ export default { }, DeleteComment: async (_parent, args, context, _resolveInfo) => { const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async transaction => { + const writeTxResultPromise = session.writeTransaction(async (transaction) => { const deleteCommentTransactionResponse = await transaction.run( ` MATCH (comment:Comment {id: $commentId}) @@ -77,7 +77,7 @@ export default { { commentId: args.id }, ) return deleteCommentTransactionResponse.records.map( - record => record.get('comment').properties, + (record) => record.get('comment').properties, ) }) try { diff --git a/backend/src/schema/resolvers/donations.js b/backend/src/schema/resolvers/donations.js index 3052ff13d..15a1db812 100644 --- a/backend/src/schema/resolvers/donations.js +++ b/backend/src/schema/resolvers/donations.js @@ -4,7 +4,7 @@ export default { const { driver } = context let donations const session = driver.session() - const writeTxResultPromise = session.writeTransaction(async txc => { + const writeTxResultPromise = session.writeTransaction(async (txc) => { const updateDonationsTransactionResponse = await txc.run( ` MATCH (donations:Donations) @@ -16,7 +16,7 @@ export default { { params }, ) return updateDonationsTransactionResponse.records.map( - record => record.get('donations').properties, + (record) => record.get('donations').properties, ) }) try { diff --git a/backend/src/schema/resolvers/emails.js b/backend/src/schema/resolvers/emails.js index 8711a5996..7986f2613 100644 --- a/backend/src/schema/resolvers/emails.js +++ b/backend/src/schema/resolvers/emails.js @@ -27,7 +27,7 @@ export default { } = context const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async txc => { + const writeTxResultPromise = session.writeTransaction(async (txc) => { const result = await txc.run( ` MATCH (user:User {id: $userId}) @@ -37,7 +37,7 @@ export default { `, { userId, email: args.email, nonce }, ) - return result.records.map(record => ({ + return result.records.map((record) => ({ name: record.get('user').properties.name, ...record.get('email').properties, })) @@ -57,7 +57,7 @@ export default { } = context const { nonce, email } = args const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async txc => { + const writeTxResultPromise = session.writeTransaction(async (txc) => { const result = await txc.run( ` MATCH (user:User {id: $userId})-[:PRIMARY_EMAIL]->(previous:EmailAddress) @@ -71,7 +71,7 @@ export default { `, { userId, email, nonce }, ) - return result.records.map(record => record.get('email').properties) + return result.records.map((record) => record.get('email').properties) }) try { const txResult = await writeTxResultPromise diff --git a/backend/src/schema/resolvers/embeds.spec.js b/backend/src/schema/resolvers/embeds.spec.js index 7683505eb..b8215ed72 100644 --- a/backend/src/schema/resolvers/embeds.spec.js +++ b/backend/src/schema/resolvers/embeds.spec.js @@ -48,7 +48,7 @@ describe('Query', () => { let embedAction beforeEach(() => { - embedAction = async variables => { + embedAction = async (variables) => { const { server } = createServer({ context: () => {}, }) diff --git a/backend/src/schema/resolvers/embeds/findProvider.js b/backend/src/schema/resolvers/embeds/findProvider.js index 8575599e1..344cfed51 100644 --- a/backend/src/schema/resolvers/embeds/findProvider.js +++ b/backend/src/schema/resolvers/embeds/findProvider.js @@ -12,11 +12,11 @@ let oEmbedProvidersFile = fs.readFileSync( oEmbedProvidersFile = oEmbedProvidersFile.replace(/\{format\}/g, 'json') const oEmbedProviders = JSON.parse(oEmbedProvidersFile) -export default function(embedUrl) { +export default function (embedUrl) { for (const provider of oEmbedProviders) { for (const endpoint of provider.endpoints) { const { schemes = [], url } = endpoint - if (schemes.some(scheme => minimatch(embedUrl, scheme))) return url + if (schemes.some((scheme) => minimatch(embedUrl, scheme))) return url } const { hostname } = new URL(embedUrl) if (provider.provider_url.includes(hostname)) { diff --git a/backend/src/schema/resolvers/embeds/scraper.js b/backend/src/schema/resolvers/embeds/scraper.js index bbf4fc999..df37611f3 100644 --- a/backend/src/schema/resolvers/embeds/scraper.js +++ b/backend/src/schema/resolvers/embeds/scraper.js @@ -29,7 +29,7 @@ const metascraper = Metascraper([ // require('./rules/metascraper-embed')() ]) -const fetchEmbed = async url => { +const fetchEmbed = async (url) => { let endpointUrl = findProvider(url) if (!endpointUrl) return {} endpointUrl = new URL(endpointUrl) @@ -53,7 +53,7 @@ const fetchEmbed = async url => { } } -const fetchResource = async url => { +const fetchResource = async (url) => { const response = await fetch(url) const html = await response.text() const resource = await metascraper({ html, url }) diff --git a/backend/src/schema/resolvers/fileUpload/index.js b/backend/src/schema/resolvers/fileUpload/index.js deleted file mode 100644 index 3c41a5d11..000000000 --- a/backend/src/schema/resolvers/fileUpload/index.js +++ /dev/null @@ -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 -} diff --git a/backend/src/schema/resolvers/fileUpload/spec.js b/backend/src/schema/resolvers/fileUpload/spec.js deleted file mode 100644 index fee0bf81b..000000000 --- a/backend/src/schema/resolvers/fileUpload/spec.js +++ /dev/null @@ -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) - }) - }) - }) -}) diff --git a/backend/src/schema/resolvers/follow.spec.js b/backend/src/schema/resolvers/follow.spec.js index 953a26d65..f35795991 100644 --- a/backend/src/schema/resolvers/follow.spec.js +++ b/backend/src/schema/resolvers/follow.spec.js @@ -81,7 +81,7 @@ beforeEach(async () => { email: 'test@example.org', password: '1234', }, - ).then(user => user.toJson()) + ).then((user) => user.toJson()) user2 = await Factory.build( 'user', { @@ -92,7 +92,7 @@ beforeEach(async () => { email: 'test2@example.org', password: '1234', }, - ).then(user => user.toJson()) + ).then((user) => user.toJson()) authenticatedUser = user1 variables = { id: user2.id } @@ -146,7 +146,7 @@ describe('follow', () => { { id: 'u1' }, ) const relationshipProperties = relation.records.map( - record => record.get('relationship').properties.createdAt, + (record) => record.get('relationship').properties.createdAt, ) expect(relationshipProperties[0]).toEqual(expect.any(String)) }) diff --git a/backend/src/schema/resolvers/helpers/Resolver.js b/backend/src/schema/resolvers/helpers/Resolver.js index 64ba60f5e..f2861e7a0 100644 --- a/backend/src/schema/resolvers/helpers/Resolver.js +++ b/backend/src/schema/resolvers/helpers/Resolver.js @@ -1,9 +1,9 @@ import log from './databaseLogger' -export const undefinedToNullResolver = list => { +export const undefinedToNullResolver = (list) => { const resolvers = {} - list.forEach(key => { - resolvers[key] = async parent => { + list.forEach((key) => { + resolvers[key] = async (parent) => { return typeof parent[key] === 'undefined' ? null : parent[key] } }) @@ -25,14 +25,14 @@ export default function Resolver(type, options = {}) { if (typeof parent[key] !== 'undefined') return parent[key] const id = parent[idAttribute] const session = driver.session() - const readTxResultPromise = session.readTransaction(async txc => { + const readTxResultPromise = session.readTransaction(async (txc) => { const cypher = ` MATCH(:${type} {${idAttribute}: $id})${connection} RETURN related {.*} as related ` const result = await txc.run(cypher, { id, cypherParams }) log(result) - return result.records.map(r => r.get('related')) + return result.records.map((r) => r.get('related')) }) try { let response = await readTxResultPromise @@ -44,19 +44,19 @@ export default function Resolver(type, options = {}) { } } - const booleanResolver = obj => { + const booleanResolver = (obj) => { const resolvers = {} for (const [key, condition] of Object.entries(obj)) { resolvers[key] = async (parent, params, { cypherParams, driver }, resolveInfo) => { if (typeof parent[key] !== 'undefined') return parent[key] const id = parent[idAttribute] const session = driver.session() - const readTxResultPromise = session.readTransaction(async txc => { + const readTxResultPromise = session.readTransaction(async (txc) => { const nodeCondition = condition.replace('this', 'this {id: $id}') const cypher = `${nodeCondition} as ${key}` const result = await txc.run(cypher, { id, cypherParams }) log(result) - const [response] = result.records.map(r => r.get(key)) + const [response] = result.records.map((r) => r.get(key)) return response }) try { @@ -69,13 +69,13 @@ export default function Resolver(type, options = {}) { return resolvers } - const countResolver = obj => { + const countResolver = (obj) => { const resolvers = {} for (const [key, connection] of Object.entries(obj)) { resolvers[key] = async (parent, params, { driver, cypherParams }, resolveInfo) => { if (typeof parent[key] !== 'undefined') return parent[key] const session = driver.session() - const readTxResultPromise = session.readTransaction(async txc => { + const readTxResultPromise = session.readTransaction(async (txc) => { const id = parent[idAttribute] const cypher = ` MATCH(u:${type} {${idAttribute}: $id})${connection} @@ -83,7 +83,7 @@ export default function Resolver(type, options = {}) { ` const result = await txc.run(cypher, { id, cypherParams }) log(result) - const [response] = result.records.map(r => r.get('count').toNumber()) + const [response] = result.records.map((r) => r.get('count').toNumber()) return response }) try { @@ -96,7 +96,7 @@ export default function Resolver(type, options = {}) { return resolvers } - const hasManyResolver = obj => { + const hasManyResolver = (obj) => { const resolvers = {} for (const [key, connection] of Object.entries(obj)) { resolvers[key] = _hasResolver(resolvers, { key, connection }, { returnType: 'iterable' }) @@ -104,7 +104,7 @@ export default function Resolver(type, options = {}) { return resolvers } - const hasOneResolver = obj => { + const hasOneResolver = (obj) => { const resolvers = {} for (const [key, connection] of Object.entries(obj)) { resolvers[key] = _hasResolver(resolvers, { key, connection }, { returnType: 'object' }) diff --git a/backend/src/schema/resolvers/helpers/createPasswordReset.js b/backend/src/schema/resolvers/helpers/createPasswordReset.js index dec55c893..ec0349c18 100644 --- a/backend/src/schema/resolvers/helpers/createPasswordReset.js +++ b/backend/src/schema/resolvers/helpers/createPasswordReset.js @@ -5,7 +5,7 @@ export default async function createPasswordReset(options) { const normalizedEmail = normalizeEmail(email) const session = driver.session() try { - const createPasswordResetTxPromise = session.writeTransaction(async transaction => { + const createPasswordResetTxPromise = session.writeTransaction(async (transaction) => { const createPasswordResetTransactionResponse = await transaction.run( ` MATCH (user:User)-[:PRIMARY_EMAIL]->(email:EmailAddress {email:$email}) @@ -19,7 +19,7 @@ export default async function createPasswordReset(options) { email: normalizedEmail, }, ) - return createPasswordResetTransactionResponse.records.map(record => { + return createPasswordResetTransactionResponse.records.map((record) => { const { email } = record.get('email').properties const { nonce } = record.get('passwordReset').properties const { name } = record.get('user').properties diff --git a/backend/src/schema/resolvers/helpers/existingEmailAddress.js b/backend/src/schema/resolvers/helpers/existingEmailAddress.js index 960b2066f..717d0d904 100644 --- a/backend/src/schema/resolvers/helpers/existingEmailAddress.js +++ b/backend/src/schema/resolvers/helpers/existingEmailAddress.js @@ -3,7 +3,7 @@ import { UserInputError } from 'apollo-server' export default async function alreadyExistingMail({ args, context }) { const session = context.driver.session() try { - const existingEmailAddressTxPromise = session.writeTransaction(async transaction => { + const existingEmailAddressTxPromise = session.writeTransaction(async (transaction) => { const existingEmailAddressTransactionResponse = await transaction.run( ` MATCH (email:EmailAddress {email: $email}) @@ -12,7 +12,7 @@ export default async function alreadyExistingMail({ args, context }) { `, { email: args.email }, ) - return existingEmailAddressTransactionResponse.records.map(record => { + return existingEmailAddressTransactionResponse.records.map((record) => { return { alreadyExistingEmail: record.get('email').properties, user: record.get('user') && record.get('user').properties, diff --git a/backend/src/schema/resolvers/helpers/filterForMutedUsers.js b/backend/src/schema/resolvers/helpers/filterForMutedUsers.js index 78f461c28..5094039ee 100644 --- a/backend/src/schema/resolvers/helpers/filterForMutedUsers.js +++ b/backend/src/schema/resolvers/helpers/filterForMutedUsers.js @@ -4,7 +4,7 @@ import { mergeWith, isArray } from 'lodash' export const filterForMutedUsers = async (params, context) => { if (!context.user) return params const [mutedUsers] = await Promise.all([getMutedUsers(context)]) - const mutedUsersIds = [...mutedUsers.map(user => user.id)] + const mutedUsersIds = [...mutedUsers.map((user) => user.id)] if (!mutedUsersIds.length) return params params.filter = mergeWith( diff --git a/backend/src/schema/resolvers/helpers/normalizeEmail.js b/backend/src/schema/resolvers/helpers/normalizeEmail.js index bdd12e991..bc13467c3 100644 --- a/backend/src/schema/resolvers/helpers/normalizeEmail.js +++ b/backend/src/schema/resolvers/helpers/normalizeEmail.js @@ -1,6 +1,6 @@ import { normalizeEmail } from 'validator' -export default email => +export default (email) => normalizeEmail(email, { // gmail_remove_dots: true, default gmail_remove_subaddress: false, diff --git a/backend/src/schema/resolvers/images.js b/backend/src/schema/resolvers/images.js new file mode 100644 index 000000000..8b3f4a3e8 --- /dev/null +++ b/backend/src/schema/resolvers/images.js @@ -0,0 +1,8 @@ +import Resolver from './helpers/Resolver' +export default { + Image: { + ...Resolver('Image', { + undefinedToNull: ['sensitive', 'alt', 'aspectRatio'], + }), + }, +} diff --git a/backend/src/schema/resolvers/images/images.js b/backend/src/schema/resolvers/images/images.js new file mode 100644 index 000000000..18a3569b6 --- /dev/null +++ b/backend/src/schema/resolvers/images/images.js @@ -0,0 +1,153 @@ +import path from 'path' +import { v4 as uuid } from 'uuid' +import { S3 } from 'aws-sdk' +import slug from 'slug' +import { existsSync, unlinkSync, createWriteStream } from 'fs' +import { UserInputError } from 'apollo-server' +import { getDriver } from '../../../db/neo4j' +import { s3Configs } from '../../../config' + +// const widths = [34, 160, 320, 640, 1024] +const { AWS_ENDPOINT: endpoint, AWS_REGION: region, AWS_BUCKET: Bucket, S3_CONFIGURED } = s3Configs + +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) => { + if (!deleteCallback) { + deleteCallback = S3_CONFIGURED ? s3Delete : localFileDelete + } + const { url } = image + deleteCallback(url) + return url +} + +const uploadImageFile = async (upload, uploadCallback) => { + if (!upload) return undefined + if (!uploadCallback) { + uploadCallback = S3_CONFIGURED ? s3Upload : localFileUpload + } + const { createReadStream, filename, mimetype } = await upload + const { name, ext } = path.parse(filename) + const uniqueFilename = `${uuid()}-${slug(name)}${ext}` + return uploadCallback({ createReadStream, 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, uniqueFilename }) => { + const destination = `/uploads/${uniqueFilename}` + return new Promise((resolve, reject) => + createReadStream() + .pipe(createWriteStream(`public${destination}`)) + .on('finish', () => resolve(destination)) + .on('error', reject), + ) +} + +const s3Upload = async ({ createReadStream, uniqueFilename, mimetype }) => { + const s3 = new S3({ region, endpoint }) + const s3Location = `original/${uniqueFilename}` + + const params = { + Bucket, + Key: s3Location, + ACL: 'public-read', + ContentType: mimetype, + Body: createReadStream(), + } + const data = await s3.upload(params).promise() + const { Location } = data + return Location +} + +const localFileDelete = async (url) => { + const location = `public${url}` + if (existsSync(location)) unlinkSync(location) +} + +const s3Delete = async (url) => { + const s3 = new S3({ region, endpoint }) + let { pathname } = new URL(url, 'http://example.org') // dummy domain to avoid invalid URL error + pathname = pathname.substring(1) // remove first character '/' + const params = { + Bucket, + Key: pathname, + } + await s3.deleteObject(params).promise() +} diff --git a/backend/src/schema/resolvers/images/images.spec.js b/backend/src/schema/resolvers/images/images.spec.js new file mode 100644 index 000000000..42064621c --- /dev/null +++ b/backend/src/schema/resolvers/images/images.spec.js @@ -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(({ uniqueFilename }) => `/uploads/${uniqueFilename}`) + 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('given image.upload', () => { + beforeEach(() => { + imageInput = { + ...imageInput, + upload: { + filename: 'image.jpg', + mimetype: 'image/jpeg', + encoding: '7bit', + createReadStream: () => ({ + pipe: () => ({ + on: (_, callback) => callback(), + }), + }), + }, + } + }) + + 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() + }) + + 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, { uploadCallback, deleteCallback }) + 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', + }) + }) + }) + }) +}) diff --git a/backend/src/schema/resolvers/moderation.js b/backend/src/schema/resolvers/moderation.js index 07054d3a3..c261d187e 100644 --- a/backend/src/schema/resolvers/moderation.js +++ b/backend/src/schema/resolvers/moderation.js @@ -21,14 +21,14 @@ export default { WITH review, report, resource {.*, __typename: labels(resource)[0]} AS finalResource RETURN review {.*, report: properties(report), resource: properties(finalResource)} ` - const reviewWriteTxResultPromise = session.writeTransaction(async txc => { + const reviewWriteTxResultPromise = session.writeTransaction(async (txc) => { const reviewTransactionResponse = await txc.run(cypher, { params, moderatorId: moderator.id, dateTime: new Date().toISOString(), }) log(reviewTransactionResponse) - return reviewTransactionResponse.records.map(record => record.get('review')) + return reviewTransactionResponse.records.map((record) => record.get('review')) }) const [reviewed] = await reviewWriteTxResultPromise return reviewed || null diff --git a/backend/src/schema/resolvers/notifications.js b/backend/src/schema/resolvers/notifications.js index 7524cda32..ee08f0e4f 100644 --- a/backend/src/schema/resolvers/notifications.js +++ b/backend/src/schema/resolvers/notifications.js @@ -158,7 +158,7 @@ export default { markAsRead: async (_parent, args, context, _resolveInfo) => { const { user: currentUser } = context const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async transaction => { + const writeTxResultPromise = session.writeTransaction(async (transaction) => { const markNotificationAsReadTransactionResponse = await transaction.run( ` MATCH (resource {id: $resourceId})-[notification:NOTIFIED {read: FALSE}]->(user:User {id: $id}) @@ -172,7 +172,7 @@ export default { }, ) log(markNotificationAsReadTransactionResponse) - return markNotificationAsReadTransactionResponse.records.map(record => + return markNotificationAsReadTransactionResponse.records.map((record) => record.get('notification'), ) }) @@ -185,7 +185,7 @@ export default { }, }, NOTIFIED: { - id: async parent => { + id: async (parent) => { // serialize an ID to help the client update the cache return `${parent.reason}/${parent.from.id}/${parent.to.id}` }, diff --git a/backend/src/schema/resolvers/passwordReset.js b/backend/src/schema/resolvers/passwordReset.js index b3fc9f5c3..d1f49876b 100644 --- a/backend/src/schema/resolvers/passwordReset.js +++ b/backend/src/schema/resolvers/passwordReset.js @@ -14,7 +14,7 @@ export default { const encryptedNewPassword = await bcrypt.hashSync(newPassword, 10) const session = driver.session() try { - const passwordResetTxPromise = session.writeTransaction(async transaction => { + const passwordResetTxPromise = session.writeTransaction(async (transaction) => { const passwordResetTransactionResponse = await transaction.run( ` MATCH (passwordReset:PasswordReset {nonce: $nonce}) @@ -32,7 +32,9 @@ export default { encryptedNewPassword, }, ) - return passwordResetTransactionResponse.records.map(record => record.get('passwordReset')) + return passwordResetTransactionResponse.records.map((record) => + record.get('passwordReset'), + ) }) const [reset] = await passwordResetTxPromise return !!(reset && reset.properties.usedAt) diff --git a/backend/src/schema/resolvers/passwordReset.spec.js b/backend/src/schema/resolvers/passwordReset.spec.js index b48498ee7..fd1395c57 100644 --- a/backend/src/schema/resolvers/passwordReset.spec.js +++ b/backend/src/schema/resolvers/passwordReset.spec.js @@ -16,7 +16,7 @@ const getAllPasswordResets = async () => { const passwordResetQuery = await neode.cypher( 'MATCH (passwordReset:PasswordReset) RETURN passwordReset', ) - const resets = passwordResetQuery.records.map(record => record.get('passwordReset')) + const resets = passwordResetQuery.records.map((record) => record.get('passwordReset')) return resets } diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index 1d4c4bfaa..f209158fe 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -2,11 +2,11 @@ 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' -const maintainPinnedPosts = params => { +const maintainPinnedPosts = (params) => { const pinnedPostFilter = { pinned: true } if (isEmpty(params.filter)) { params.filter = { OR: [pinnedPostFilter, {}] } @@ -34,7 +34,7 @@ export default { PostsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => { const { postId, data } = params const session = context.driver.session() - const readTxResultPromise = session.readTransaction(async transaction => { + const readTxResultPromise = session.readTransaction(async (transaction) => { const emotionsCountTransactionResponse = await transaction.run( ` MATCH (post:Post {id: $postId})<-[emoted:EMOTED {emotion: $data.emotion}]-() @@ -43,7 +43,7 @@ export default { { postId, data }, ) return emotionsCountTransactionResponse.records.map( - record => record.get('emotionsCount').low, + (record) => record.get('emotionsCount').low, ) }) try { @@ -56,7 +56,7 @@ export default { PostsEmotionsByCurrentUser: async (object, params, context, resolveInfo) => { const { postId } = params const session = context.driver.session() - const readTxResultPromise = session.readTransaction(async transaction => { + const readTxResultPromise = session.readTransaction(async (transaction) => { const emotionsTransactionResponse = await transaction.run( ` MATCH (user:User {id: $userId})-[emoted:EMOTED]->(post:Post {id: $postId}) @@ -64,7 +64,7 @@ export default { `, { userId: context.user.id, postId }, ) - return emotionsTransactionResponse.records.map(record => record.get('emotion')) + return emotionsTransactionResponse.records.map((record) => record.get('emotion')) }) try { const [emotions] = await readTxResultPromise @@ -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 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}) @@ -130,7 +137,7 @@ export default { RETURN post, category ` - await session.writeTransaction(transaction => { + await session.writeTransaction((transaction) => { return transaction.run(cypherDeletePreviousRelations, { params }) }) @@ -142,17 +149,19 @@ export default { ` } - updatePostCypher += `RETURN post` + updatePostCypher += `RETURN post {.*}` const updatePostVariables = { categoryIds, params } try { - const writeTxResultPromise = session.writeTransaction(async transaction => { + const writeTxResultPromise = session.writeTransaction(async (transaction) => { const updatePostTransactionResponse = await transaction.run( 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() @@ -161,7 +170,7 @@ export default { DeletePost: async (object, args, context, resolveInfo) => { const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async transaction => { + const writeTxResultPromise = session.writeTransaction(async (transaction) => { const deletePostTransactionResponse = await transaction.run( ` MATCH (post:Post {id: $postId}) @@ -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() @@ -189,7 +199,7 @@ export default { const { to, data } = params const { user } = context const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async transaction => { + const writeTxResultPromise = session.writeTransaction(async (transaction) => { const addPostEmotionsTransactionResponse = await transaction.run( ` MATCH (userFrom:User {id: $user.id}), (postTo:Post {id: $to.id}) @@ -197,7 +207,7 @@ export default { RETURN userFrom, postTo, emotedRelation`, { user, to, data }, ) - return addPostEmotionsTransactionResponse.records.map(record => { + return addPostEmotionsTransactionResponse.records.map((record) => { return { from: { ...record.get('userFrom').properties }, to: { ...record.get('postTo').properties }, @@ -216,7 +226,7 @@ export default { const { to, data } = params const { id: from } = context.user const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async transaction => { + const writeTxResultPromise = session.writeTransaction(async (transaction) => { const removePostEmotionsTransactionResponse = await transaction.run( ` MATCH (userFrom:User {id: $from})-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo:Post {id: $to.id}) @@ -225,7 +235,7 @@ export default { `, { from, to, data }, ) - return removePostEmotionsTransactionResponse.records.map(record => { + return removePostEmotionsTransactionResponse.records.map((record) => { return { from: { ...record.get('userFrom').properties }, to: { ...record.get('postTo').properties }, @@ -245,7 +255,7 @@ export default { const { driver, user } = context const session = driver.session() const { id: userId } = user - let writeTxResultPromise = session.writeTransaction(async transaction => { + let writeTxResultPromise = session.writeTransaction(async (transaction) => { const deletePreviousRelationsResponse = await transaction.run( ` MATCH (:User)-[previousRelations:PINNED]->(post:Post) @@ -254,12 +264,14 @@ export default { RETURN post `, ) - return deletePreviousRelationsResponse.records.map(record => record.get('post').properties) + return deletePreviousRelationsResponse.records.map( + (record) => record.get('post').properties, + ) }) try { await writeTxResultPromise - writeTxResultPromise = session.writeTransaction(async transaction => { + writeTxResultPromise = session.writeTransaction(async (transaction) => { const pinPostTransactionResponse = await transaction.run( ` MATCH (user:User {id: $userId}) WHERE user.role = 'admin' @@ -270,7 +282,7 @@ export default { `, { userId, params }, ) - return pinPostTransactionResponse.records.map(record => ({ + return pinPostTransactionResponse.records.map((record) => ({ pinnedPost: record.get('post').properties, pinnedAt: record.get('pinnedAt'), })) @@ -289,7 +301,7 @@ export default { unpinPost: async (_parent, params, context, _resolveInfo) => { let unpinnedPost const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async transaction => { + const writeTxResultPromise = session.writeTransaction(async (transaction) => { const unpinPostTransactionResponse = await transaction.run( ` MATCH (:User)-[previousRelations:PINNED]->(post:Post {id: $params.id}) @@ -299,7 +311,7 @@ export default { `, { params }, ) - return unpinPostTransactionResponse.records.map(record => record.get('post').properties) + return unpinPostTransactionResponse.records.map((record) => record.get('post').properties) }) try { ;[unpinnedPost] = await writeTxResultPromise @@ -311,16 +323,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 +334,7 @@ export default { hasOne: { author: '<-[:WROTE]-(related:User)', pinnedBy: '<-[:PINNED]-(related:User)', + image: '-[:HERO_IMAGE]->(related:Image)', }, count: { commentsCount: @@ -349,7 +353,7 @@ export default { const { id } = parent const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async transaction => { + const writeTxResultPromise = session.writeTransaction(async (transaction) => { const relatedContributionsTransactionResponse = await transaction.run( ` MATCH (p:Post {id: $id})-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post) @@ -360,7 +364,7 @@ export default { { id }, ) return relatedContributionsTransactionResponse.records.map( - record => record.get('post').properties, + (record) => record.get('post').properties, ) }) try { diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js index 88a09843d..b24383fba 100644 --- a/backend/src/schema/resolvers/posts.spec.js +++ b/backend/src/schema/resolvers/posts.spec.js @@ -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, }, diff --git a/backend/src/schema/resolvers/registration.js b/backend/src/schema/resolvers/registration.js index 1e7708395..f1c43be21 100644 --- a/backend/src/schema/resolvers/registration.js +++ b/backend/src/schema/resolvers/registration.js @@ -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!') diff --git a/backend/src/schema/resolvers/reports.js b/backend/src/schema/resolvers/reports.js index 69ae49125..8bc707401 100644 --- a/backend/src/schema/resolvers/reports.js +++ b/backend/src/schema/resolvers/reports.js @@ -7,7 +7,7 @@ export default { const { resourceId, reasonCategory, reasonDescription } = params const { driver, user } = context const session = driver.session() - const fileReportWriteTxResultPromise = session.writeTransaction(async transaction => { + const fileReportWriteTxResultPromise = session.writeTransaction(async (transaction) => { const fileReportTransactionResponse = await transaction.run( ` MATCH (submitter:User {id: $submitterId}) @@ -30,7 +30,7 @@ export default { }, ) log(fileReportTransactionResponse) - return fileReportTransactionResponse.records.map(record => record.get('filedReport')) + return fileReportTransactionResponse.records.map((record) => record.get('filedReport')) }) try { const [filedReport] = await fileReportWriteTxResultPromise @@ -82,7 +82,7 @@ export default { params.offset && typeof params.offset === 'number' ? `SKIP ${params.offset}` : '' const limit = params.first && typeof params.first === 'number' ? `LIMIT ${params.first}` : '' - const reportsReadTxPromise = session.readTransaction(async transaction => { + const reportsReadTxPromise = session.readTransaction(async (transaction) => { const reportsTransactionResponse = await transaction.run( // !!! this Cypher query returns multiple reports on the same resource! i will create an issue for refactoring (bug fixing) ` @@ -103,7 +103,7 @@ export default { `, ) log(reportsTransactionResponse) - return reportsTransactionResponse.records.map(record => record.get('report')) + return reportsTransactionResponse.records.map((record) => record.get('report')) }) try { const reports = await reportsReadTxPromise @@ -119,7 +119,7 @@ export default { const session = context.driver.session() const { id } = parent let filed - const readTxPromise = session.readTransaction(async transaction => { + const readTxPromise = session.readTransaction(async (transaction) => { const filedReportsTransactionResponse = await transaction.run( ` MATCH (submitter:User)-[filed:FILED]->(report:Report {id: $id}) @@ -128,14 +128,14 @@ export default { { id }, ) log(filedReportsTransactionResponse) - return filedReportsTransactionResponse.records.map(record => ({ + return filedReportsTransactionResponse.records.map((record) => ({ submitter: record.get('submitter').properties, filed: record.get('filed').properties, })) }) try { const filedReports = await readTxPromise - filed = filedReports.map(reportedRecord => { + filed = filedReports.map((reportedRecord) => { const { submitter, filed } = reportedRecord const relationshipWithNestedAttributes = { ...filed, @@ -153,7 +153,7 @@ export default { const session = context.driver.session() const { id } = parent let reviewed - const readTxPromise = session.readTransaction(async transaction => { + const readTxPromise = session.readTransaction(async (transaction) => { const reviewedReportsTransactionResponse = await transaction.run( ` MATCH (resource)<-[:BELONGS_TO]-(report:Report {id: $id})<-[review:REVIEWED]-(moderator:User) @@ -163,14 +163,14 @@ export default { { id }, ) log(reviewedReportsTransactionResponse) - return reviewedReportsTransactionResponse.records.map(record => ({ + return reviewedReportsTransactionResponse.records.map((record) => ({ review: record.get('review').properties, moderator: record.get('moderator').properties, })) }) try { const reviewedReports = await readTxPromise - reviewed = reviewedReports.map(reportedRecord => { + reviewed = reviewedReports.map((reportedRecord) => { const { review, moderator } = reportedRecord const relationshipWithNestedAttributes = { ...review, diff --git a/backend/src/schema/resolvers/reports.spec.js b/backend/src/schema/resolvers/reports.spec.js index 5e1156f0c..2ecccfc23 100644 --- a/backend/src/schema/resolvers/reports.spec.js +++ b/backend/src/schema/resolvers/reports.spec.js @@ -246,7 +246,7 @@ describe('file a report on a resource', () => { }) expect(reportsCypherQueryResponse.records).toHaveLength(1) const [reportProperties] = reportsCypherQueryResponse.records.map( - record => record.get('report').properties, + (record) => record.get('report').properties, ) expect(reportProperties).toMatchObject({ rule: 'latestReviewUpdatedAtRules' }) }) @@ -264,7 +264,7 @@ describe('file a report on a resource', () => { }) expect(reportsCypherQueryResponse.records).toHaveLength(1) const [reportProperties] = reportsCypherQueryResponse.records.map( - record => record.get('report').properties, + (record) => record.get('report').properties, ) expect(reportProperties).toMatchObject({ disable: false }) }) @@ -297,7 +297,7 @@ describe('file a report on a resource', () => { }) expect(reportsCypherQueryResponse.records).toHaveLength(1) const [reportProperties] = reportsCypherQueryResponse.records.map( - record => record.get('report').properties, + (record) => record.get('report').properties, ) expect(reportProperties).toMatchObject({ disable: true }) }) diff --git a/backend/src/schema/resolvers/rewards.js b/backend/src/schema/resolvers/rewards.js index 311cfd2e6..c271ca8f8 100644 --- a/backend/src/schema/resolvers/rewards.js +++ b/backend/src/schema/resolvers/rewards.js @@ -24,7 +24,7 @@ export default { const { user } = await getUserAndBadge(params) const session = context.driver.session() try { - await session.writeTransaction(transaction => { + await session.writeTransaction((transaction) => { return transaction.run( ` MATCH (badge:Badge {id: $badgeKey})-[reward:REWARDED]->(rewardedUser:User {id: $userId}) diff --git a/backend/src/schema/resolvers/searches.js b/backend/src/schema/resolvers/searches.js index 994d19fa2..58fa63f8d 100644 --- a/backend/src/schema/resolvers/searches.js +++ b/backend/src/schema/resolvers/searches.js @@ -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,14 +36,25 @@ 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 => { + const searchResultPromise = session.readTransaction(async (transaction) => { const postTransactionResponse = transaction.run(postCypher, { query: myQuery, limit, @@ -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() } diff --git a/backend/src/schema/resolvers/searches.spec.js b/backend/src/schema/resolvers/searches.spec.js new file mode 100644 index 000000000..081d71e55 --- /dev/null +++ b/backend/src/schema/resolvers/searches.spec.js @@ -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²
+2. binomische Formel: (a - b)² = a² - 2ab + b²
+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²
+2. binomische Formel: (a - b)² = a² - 2ab + b²
+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²
+2. binomische Formel: (a - b)² = a² - 2ab + b²
+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
+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.`, + }, + { + __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
+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.`, + }, + { + __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', + }, + ], + }, + }) + }) + }) + }) + }) + }) +}) diff --git a/backend/src/schema/resolvers/searches/queryString.js b/backend/src/schema/resolvers/searches/queryString.js new file mode 100644 index 000000000..064f17f48 --- /dev/null +++ b/backend/src/schema/resolvers/searches/queryString.js @@ -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') +} diff --git a/backend/src/schema/resolvers/searches/queryString.spec.js b/backend/src/schema/resolvers/searches/queryString.spec.js new file mode 100644 index 000000000..23a746be1 --- /dev/null +++ b/backend/src/schema/resolvers/searches/queryString.spec.js @@ -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*') + }) + }) +}) diff --git a/backend/src/schema/resolvers/shout.js b/backend/src/schema/resolvers/shout.js index 70ebdf7ae..8c330cd67 100644 --- a/backend/src/schema/resolvers/shout.js +++ b/backend/src/schema/resolvers/shout.js @@ -7,7 +7,7 @@ export default { const session = context.driver.session() try { - const shoutWriteTxResultPromise = session.writeTransaction(async transaction => { + const shoutWriteTxResultPromise = session.writeTransaction(async (transaction) => { const shoutTransactionResponse = await transaction.run( ` MATCH (node {id: $id})<-[:WROTE]-(userWritten:User), (user:User {id: $userId}) @@ -22,7 +22,7 @@ export default { }, ) log(shoutTransactionResponse) - return shoutTransactionResponse.records.map(record => record.get('isShouted')) + return shoutTransactionResponse.records.map((record) => record.get('isShouted')) }) const [isShouted] = await shoutWriteTxResultPromise return isShouted @@ -35,7 +35,7 @@ export default { const { id, type } = params const session = context.driver.session() try { - const unshoutWriteTxResultPromise = session.writeTransaction(async transaction => { + const unshoutWriteTxResultPromise = session.writeTransaction(async (transaction) => { const unshoutTransactionResponse = await transaction.run( ` MATCH (user:User {id: $userId})-[relation:SHOUTED]->(node {id: $id}) @@ -50,7 +50,7 @@ export default { }, ) log(unshoutTransactionResponse) - return unshoutTransactionResponse.records.map(record => record.get('isShouted')) + return unshoutTransactionResponse.records.map((record) => record.get('isShouted')) }) const [isShouted] = await unshoutWriteTxResultPromise return isShouted diff --git a/backend/src/schema/resolvers/shout.spec.js b/backend/src/schema/resolvers/shout.spec.js index f5ec8f5fd..574907180 100644 --- a/backend/src/schema/resolvers/shout.spec.js +++ b/backend/src/schema/resolvers/shout.spec.js @@ -132,7 +132,7 @@ describe('shout and unshout posts', () => { }, ) const relationshipProperties = relation.records.map( - record => record.get('relationship').properties.createdAt, + (record) => record.get('relationship').properties.createdAt, ) expect(relationshipProperties[0]).toEqual(expect.any(String)) }) diff --git a/backend/src/schema/resolvers/statistics.js b/backend/src/schema/resolvers/statistics.js index 7ca9239f3..d9b7c153e 100644 --- a/backend/src/schema/resolvers/statistics.js +++ b/backend/src/schema/resolvers/statistics.js @@ -15,7 +15,7 @@ export default { countFollows: 'FOLLOWS', countShouts: 'SHOUTED', } - const statisticsReadTxResultPromise = session.readTransaction(async transaction => { + const statisticsReadTxResultPromise = session.readTransaction(async (transaction) => { const statisticsTransactionResponse = await transaction.run( ` CALL apoc.meta.stats() YIELD labels, relTypesCount @@ -23,7 +23,7 @@ export default { `, ) log(statisticsTransactionResponse) - return statisticsTransactionResponse.records.map(record => { + return statisticsTransactionResponse.records.map((record) => { return { ...record.get('labels'), ...record.get('relTypesCount'), @@ -31,7 +31,7 @@ export default { }) }) const [statistics] = await statisticsReadTxResultPromise - Object.keys(mapping).forEach(key => { + Object.keys(mapping).forEach((key) => { const stat = statistics[mapping[key]] counts[key] = stat ? stat.toNumber() : 0 }) diff --git a/backend/src/schema/resolvers/user_management.js b/backend/src/schema/resolvers/user_management.js index 2014b01b8..beb2cddb3 100644 --- a/backend/src/schema/resolvers/user_management.js +++ b/backend/src/schema/resolvers/user_management.js @@ -16,7 +16,7 @@ export default { const { user, driver } = context if (!user) return null const session = driver.session() - const currentUserTransactionPromise = session.readTransaction(async transaction => { + const currentUserTransactionPromise = session.readTransaction(async (transaction) => { const result = await transaction.run( ` MATCH (user:User {id: $id}) @@ -26,7 +26,7 @@ export default { { id: user.id }, ) log(result) - return result.records.map(record => record.get('user')) + return result.records.map((record) => record.get('user')) }) try { const [currentUser] = await currentUserTransactionPromise @@ -44,16 +44,16 @@ export default { email = normalizeEmail(email) const session = driver.session() try { - const loginReadTxResultPromise = session.readTransaction(async transaction => { + const loginReadTxResultPromise = session.readTransaction(async (transaction) => { 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 }, ) log(loginTransactionResponse) - return loginTransactionResponse.records.map(record => record.get('user')) + return loginTransactionResponse.records.map((record) => record.get('user')) }) const [currentUser] = await loginReadTxResultPromise if ( diff --git a/backend/src/schema/resolvers/user_management.spec.js b/backend/src/schema/resolvers/user_management.spec.js index 1e295638d..b434ea628 100644 --- a/backend/src/schema/resolvers/user_management.spec.js +++ b/backend/src/schema/resolvers/user_management.spec.js @@ -10,7 +10,7 @@ import { getNeode } from '../../db/neo4j' const neode = getNeode() let query, mutate, variables, req, user -const disable = async id => { +const disable = async (id) => { const moderator = await Factory.build('user', { id: 'u2', role: 'moderator' }) const user = await neode.find('User', id) const reportAgainstUser = await Factory.build('report') @@ -56,7 +56,7 @@ describe('isLoggedIn', () => { isLoggedIn } ` - const respondsWith = async expected => { + const respondsWith = async (expected) => { await expect(query({ query: isLoggedInQuery })).resolves.toMatchObject(expected) } @@ -106,14 +106,16 @@ describe('currentUser', () => { id slug name - avatar + avatar { + url + } email role } } ` - const respondsWith = async expected => { + const respondsWith = async (expected) => { await expect(query({ query: currentUserQuery, variables })).resolves.toMatchObject(expected) } @@ -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', @@ -170,7 +176,7 @@ describe('login', () => { } ` - const respondsWith = async expected => { + const respondsWith = async (expected) => { await expect(mutate({ mutation: loginMutation, variables })).resolves.toMatchObject(expected) } @@ -187,7 +193,7 @@ describe('login', () => { describe('ask for a `token`', () => { describe('with a valid email/password combination', () => { - it('responds with a JWT bearer token', async done => { + it('responds with a JWT bearer token', async (done) => { const { data: { login: token }, } = await mutate({ mutation: loginMutation, variables }) @@ -286,7 +292,7 @@ describe('change password', () => { } ` - const respondsWith = async expected => { + const respondsWith = async (expected) => { await expect(mutate({ mutation: changePasswordMutation, variables })).resolves.toMatchObject( expected, ) diff --git a/backend/src/schema/resolvers/users.js b/backend/src/schema/resolvers/users.js index c29b021be..e276968e5 100644 --- a/backend/src/schema/resolvers/users.js +++ b/backend/src/schema/resolvers/users.js @@ -1,14 +1,14 @@ 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' const neode = getNeode() -export const getMutedUsers = async context => { +export const getMutedUsers = async (context) => { const { neode } = context const userModel = neode.model('User') let mutedUsers = neode @@ -19,11 +19,11 @@ export const getMutedUsers = async context => { .to('muted', userModel) .return('muted') mutedUsers = await mutedUsers.execute() - mutedUsers = mutedUsers.records.map(r => r.get('muted').properties) + mutedUsers = mutedUsers.records.map((r) => r.get('muted').properties) return mutedUsers } -export const getBlockedUsers = async context => { +export const getBlockedUsers = async (context) => { const { neode } = context const userModel = neode.model('User') let blockedUsers = neode @@ -34,7 +34,7 @@ export const getBlockedUsers = async context => { .to('blocked', userModel) .return('blocked') blockedUsers = await blockedUsers.execute() - blockedUsers = blockedUsers.records.map(r => r.get('blocked').properties) + blockedUsers = blockedUsers.records.map((r) => r.get('blocked').properties) return blockedUsers } @@ -60,7 +60,7 @@ export default { let session try { session = context.driver.session() - const readTxResult = await session.readTransaction(txc => { + const readTxResult = await session.readTransaction((txc) => { const result = txc.run( ` MATCH (user:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email: $args.email}) @@ -69,7 +69,7 @@ export default { ) return result }) - return readTxResult.records.map(r => r.get('user').properties) + return readTxResult.records.map((r) => r.get('user').properties) } finally { session.close() } @@ -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,23 +149,26 @@ 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 => { + const writeTxResultPromise = session.writeTransaction(async (transaction) => { const updateUserTransactionResponse = await transaction.run( ` MATCH (user:User {id: $params.id}) SET user += $params SET user.updatedAt = toString(datetime()) - RETURN user + 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,52 +178,67 @@ export default { } }, DeleteUser: async (object, params, context, resolveInfo) => { - const { resource } = params + const { resource, id: userId } = params const session = context.driver.session() - const { id: userId } = params - 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, }, ) - }) - }) + 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 }, - ) - 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() @@ -231,14 +251,12 @@ export default { const { id } = parent const statement = `MATCH(u:User {id: {id}})-[:PRIMARY_EMAIL]->(e:EmailAddress) RETURN e` const result = await neode.cypher(statement, { id }) - const [{ email }] = result.records.map(r => r.get('e').properties) + const [{ email }] = result.records.map((r) => r.get('e').properties) return email }, ...Resolver('User', { undefinedToNull: [ 'actorId', - 'avatar', - 'coverImg', 'deleted', 'disabled', 'locationName', @@ -272,6 +290,7 @@ export default { badgesCount: '<-[:REWARDED]-(related:Badge)', }, hasOne: { + avatar: '-[:AVATAR_IMAGE]->(related:Image)', invitedBy: '<-[:INVITED]-(related:User)', location: '-[:IS_IN]->(related:Location)', }, diff --git a/backend/src/schema/resolvers/users.spec.js b/backend/src/schema/resolvers/users.spec.js index 892d2b4b4..cb9012133 100644 --- a/backend/src/schema/resolvers/users.spec.js +++ b/backend/src/schema/resolvers/users.spec.js @@ -29,7 +29,7 @@ beforeAll(() => { mutate = createTestClient(server).mutate }) -afterEach(async () => { +beforeEach(async () => { await cleanDatabase() }) @@ -495,6 +495,12 @@ describe('DeleteUser', () => { 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) + }) }) }) @@ -785,6 +791,12 @@ describe('DeleteUser', () => { 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) + }) }) }) @@ -834,7 +846,6 @@ describe('DeleteUser', () => { ).resolves.toMatchObject(expectedResponse) }) }) - describe('deletion of all post and comments requested', () => { beforeEach(() => { variables = { ...variables, resource: ['Post', 'Comment'] } @@ -882,27 +893,27 @@ describe('DeleteUser', () => { }) }) }) + }) + }) - 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) }) }) }) diff --git a/backend/src/schema/resolvers/users/location.js b/backend/src/schema/resolvers/users/location.js index cc00d9e0a..b58d8d1aa 100644 --- a/backend/src/schema/resolvers/users/location.js +++ b/backend/src/schema/resolvers/users/location.js @@ -7,9 +7,9 @@ import CONFIG from '../../../config' const debug = Debug('human-connection:location') -const fetch = url => { +const fetch = (url) => { return new Promise((resolve, reject) => { - request(url, function(error, response, body) { + request(url, function (error, response, body) { if (error) { reject(error) } else { @@ -57,7 +57,7 @@ const createLocation = async (session, mapboxData) => { } mutation += ' RETURN l.id' - await session.writeTransaction(transaction => { + await session.writeTransaction((transaction) => { return transaction.run(mutation, data) }) } @@ -82,7 +82,7 @@ const createOrUpdateLocations = async (userId, locationName, session) => { let data - res.features.forEach(item => { + res.features.forEach((item) => { if (item.matching_place_name === locationName) { data = item } @@ -103,9 +103,9 @@ const createOrUpdateLocations = async (userId, locationName, session) => { let parent = data if (data.context) { - await asyncForEach(data.context, async ctx => { + await asyncForEach(data.context, async (ctx) => { await createLocation(session, ctx) - await session.writeTransaction(transaction => { + await session.writeTransaction((transaction) => { return transaction.run( ` MATCH (parent:Location {id: $parentId}), (child:Location {id: $childId}) @@ -122,7 +122,7 @@ const createOrUpdateLocations = async (userId, locationName, session) => { }) } // delete all current locations from user and add new location - await session.writeTransaction(transaction => { + await session.writeTransaction((transaction) => { return transaction.run( ` MATCH (user:User {id: $userId})-[relationship:IS_IN]->(location:Location) diff --git a/backend/src/schema/resolvers/users/location.spec.js b/backend/src/schema/resolvers/users/location.spec.js index 04216dcb5..59442a9ca 100644 --- a/backend/src/schema/resolvers/users/location.spec.js +++ b/backend/src/schema/resolvers/users/location.spec.js @@ -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 => { + 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) diff --git a/backend/src/schema/resolvers/users/mutedUsers.spec.js b/backend/src/schema/resolvers/users/mutedUsers.spec.js index cdc7c81b3..345b435f5 100644 --- a/backend/src/schema/resolvers/users/mutedUsers.spec.js +++ b/backend/src/schema/resolvers/users/mutedUsers.spec.js @@ -90,7 +90,7 @@ describe('muteUser', () => { beforeEach(() => { currentUser = undefined - muteAction = variables => { + muteAction = (variables) => { const { mutate } = createTestClient(server) const muteUserMutation = gql` mutation($id: ID!) { @@ -307,7 +307,7 @@ describe('unmuteUser', () => { beforeEach(() => { currentUser = undefined - unmuteAction = variables => { + unmuteAction = (variables) => { const { mutate } = createTestClient(server) const unmuteUserMutation = gql` mutation($id: ID!) { diff --git a/backend/src/schema/types/type/EmailAddress.gql b/backend/src/schema/types/type/EmailAddress.gql index 99e309602..e09ec9e63 100644 --- a/backend/src/schema/types/type/EmailAddress.gql +++ b/backend/src/schema/types/type/EmailAddress.gql @@ -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 diff --git a/backend/src/schema/types/type/Image.gql b/backend/src/schema/types/type/Image.gql new file mode 100644 index 000000000..41cc11eef --- /dev/null +++ b/backend/src/schema/types/type/Image.gql @@ -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, +} diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql index 71fcb9605..dc6a00a41 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/schema/types/type/Post.gql @@ -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 diff --git a/backend/src/schema/types/type/Search.gql b/backend/src/schema/types/type/Search.gql index 2c22fa61f..1ce38001d 100644 --- a/backend/src/schema/types/type/Search.gql +++ b/backend/src/schema/types/type/Search.gql @@ -1,4 +1,4 @@ -union SearchResult = Post | User +union SearchResult = Post | User | Tag type Query { findResources(query: String!, limit: Int = 5): [SearchResult]! diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index 4c3555049..af525396b 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -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 @@ -198,9 +191,7 @@ type Mutation { name: String email: String slug: String - avatar: String - coverImg: String - avatarUpload: Upload + avatar: ImageInput locationName: String about: String termsAndConditionsAgreedVersion: String diff --git a/backend/src/server.js b/backend/src/server.js index 4df73559d..5a71150f0 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -20,7 +20,7 @@ const options = { host: REDIS_DOMAIN, port: REDIS_PORT, password: REDIS_PASSWORD, - retryStrategy: times => { + retryStrategy: (times) => { return Math.min(times * 50, 2000) }, } @@ -36,7 +36,7 @@ export const pubsub = prodPubsub || devPubsub const driver = getDriver() const neode = getNeode() -const getContext = async req => { +const getContext = async (req) => { const user = await decode(driver, req.headers.authorization) return { driver, @@ -48,7 +48,7 @@ const getContext = async req => { }, } } -export const context = async options => { +export const context = async (options) => { const { connection, req } = options if (connection) { return connection.context @@ -57,7 +57,7 @@ export const context = async options => { } } -const createServer = options => { +const createServer = (options) => { const defaults = { context, schema: middleware(schema), @@ -68,9 +68,9 @@ const createServer = options => { }, debug: !!CONFIG.DEBUG, tracing: !!CONFIG.DEBUG, - formatError: error => { + formatError: (error) => { if (error.message === 'ERROR_VALIDATION') { - return new Error(error.originalError.details.map(d => d.message)) + return new Error(error.originalError.details.map((d) => d.message)) } return error }, diff --git a/backend/yarn.lock b/backend/yarn.lock index 3abbd461d..26543a002 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -56,42 +56,43 @@ dependencies: "@babel/highlight" "^7.8.3" -"@babel/compat-data@^7.8.6": - version "7.8.6" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.8.6.tgz#7eeaa0dfa17e50c7d9c0832515eee09b56f04e35" - integrity sha512-CurCIKPTkS25Mb8mz267vU95vy+TyUpnctEX2lV33xWNmHAfjruztgiPBbXZRh3xZZy1CYvGx6XfxyTVS+sk7Q== +"@babel/compat-data@^7.8.6", "@babel/compat-data@^7.9.0": + version "7.9.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.9.0.tgz#04815556fc90b0c174abd2c0c1bb966faa036a6c" + integrity sha512-zeFQrr+284Ekvd9e7KAX954LkapWiOmQtsfHirhxqfdlX6MEC32iRE+pqUGlYIBchdevaCwvzxWGSy/YBNI85g== dependencies: - browserslist "^4.8.5" + browserslist "^4.9.1" invariant "^2.2.4" semver "^5.5.0" -"@babel/core@^7.1.0", "@babel/core@^7.7.5", "@babel/core@~7.8.7": - version "7.8.7" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.8.7.tgz#b69017d221ccdeb203145ae9da269d72cf102f3b" - integrity sha512-rBlqF3Yko9cynC5CCFy6+K/w2N+Sq/ff2BPy+Krp7rHlABIr5epbA7OxVeKoMHB39LZOp1UY5SuLjy6uWi35yA== +"@babel/core@^7.1.0", "@babel/core@^7.7.5", "@babel/core@~7.9.0": + version "7.9.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.9.0.tgz#ac977b538b77e132ff706f3b8a4dbad09c03c56e" + integrity sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w== dependencies: "@babel/code-frame" "^7.8.3" - "@babel/generator" "^7.8.7" - "@babel/helpers" "^7.8.4" - "@babel/parser" "^7.8.7" + "@babel/generator" "^7.9.0" + "@babel/helper-module-transforms" "^7.9.0" + "@babel/helpers" "^7.9.0" + "@babel/parser" "^7.9.0" "@babel/template" "^7.8.6" - "@babel/traverse" "^7.8.6" - "@babel/types" "^7.8.7" + "@babel/traverse" "^7.9.0" + "@babel/types" "^7.9.0" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.1" - json5 "^2.1.0" + json5 "^2.1.2" lodash "^4.17.13" resolve "^1.3.2" semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.8.6", "@babel/generator@^7.8.7": - version "7.8.7" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.8.7.tgz#870b3cf7984f5297998152af625c4f3e341400f7" - integrity sha512-DQwjiKJqH4C3qGiyQCAExJHoZssn49JTMJgZ8SANGgVFdkupcUhLOdkAeoC6kmHZCPfoDG5M0b6cFlSN5wW7Ew== +"@babel/generator@^7.9.0": + version "7.9.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.4.tgz#12441e90c3b3c4159cdecf312075bf1a8ce2dbce" + integrity sha512-rjP8ahaDy/ouhrvCoU1E5mqaitWrxwuNGU+dy1EpaoK48jZay4MdkskKGIMHLZNewg8sAsqpGSREJwP0zH3YQA== dependencies: - "@babel/types" "^7.8.7" + "@babel/types" "^7.9.0" jsesc "^2.5.1" lodash "^4.17.13" source-map "^0.5.0" @@ -139,6 +140,15 @@ "@babel/helper-regex" "^7.8.3" regexpu-core "^4.6.0" +"@babel/helper-create-regexp-features-plugin@^7.8.8": + version "7.8.8" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.8.tgz#5d84180b588f560b7864efaeea89243e58312087" + integrity sha512-LYVPdwkrQEiX9+1R29Ld/wTrmQu1SSKYnuOk3g0CkcZMA1p0gsNxJFj/3gBdaJ7Cg0Fnek5z0DsMULePP7Lrqg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.8.3" + "@babel/helper-regex" "^7.8.3" + regexpu-core "^4.7.0" + "@babel/helper-define-map@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.8.3.tgz#a0655cad5451c3760b726eba875f1cd8faa02c15" @@ -193,16 +203,17 @@ dependencies: "@babel/types" "^7.8.3" -"@babel/helper-module-transforms@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.8.3.tgz#d305e35d02bee720fbc2c3c3623aa0c316c01590" - integrity sha512-C7NG6B7vfBa/pwCOshpMbOYUmrYQDfCpVL/JCRu0ek8B5p8kue1+BCXpg2vOYs7w5ACB9GTOBYQ5U6NwrMg+3Q== +"@babel/helper-module-transforms@^7.9.0": + version "7.9.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.9.0.tgz#43b34dfe15961918707d247327431388e9fe96e5" + integrity sha512-0FvKyu0gpPfIQ8EkxlrAydOWROdHpBmiCiRwLkUiBGhCUPRRbVD2/tm3sFr/c/GWFrQ/ffutGUAnx7V0FzT2wA== dependencies: "@babel/helper-module-imports" "^7.8.3" + "@babel/helper-replace-supers" "^7.8.6" "@babel/helper-simple-access" "^7.8.3" "@babel/helper-split-export-declaration" "^7.8.3" - "@babel/template" "^7.8.3" - "@babel/types" "^7.8.3" + "@babel/template" "^7.8.6" + "@babel/types" "^7.9.0" lodash "^4.17.13" "@babel/helper-optimise-call-expression@^7.8.3": @@ -270,6 +281,11 @@ dependencies: "@babel/types" "^7.8.3" +"@babel/helper-validator-identifier@^7.9.0": + version "7.9.0" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz#ad53562a7fc29b3b9a91bbf7d10397fd146346ed" + integrity sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw== + "@babel/helper-wrap-function@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.8.3.tgz#9dbdb2bb55ef14aaa01fe8c99b629bd5352d8610" @@ -280,14 +296,14 @@ "@babel/traverse" "^7.8.3" "@babel/types" "^7.8.3" -"@babel/helpers@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.8.4.tgz#754eb3ee727c165e0a240d6c207de7c455f36f73" - integrity sha512-VPbe7wcQ4chu4TDQjimHv/5tj73qz88o12EPkO2ValS2QiQS/1F2SsjyIGNnAD0vF/nZS6Cf9i+vW6HIlnaR8w== +"@babel/helpers@^7.9.0": + version "7.9.2" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.9.2.tgz#b42a81a811f1e7313b88cba8adc66b3d9ae6c09f" + integrity sha512-JwLvzlXVPjO8eU9c/wF9/zOIN7X6h8DYf7mG4CiFRZRvZNKEF5dQ3H3V+ASkHoIB3mWhatgl5ONhyqHRI6MppA== dependencies: "@babel/template" "^7.8.3" - "@babel/traverse" "^7.8.4" - "@babel/types" "^7.8.3" + "@babel/traverse" "^7.9.0" + "@babel/types" "^7.9.0" "@babel/highlight@^7.8.3": version "7.8.3" @@ -312,10 +328,10 @@ resolve "^1.13.1" v8flags "^3.1.1" -"@babel/parser@^7.1.0", "@babel/parser@^7.7.0", "@babel/parser@^7.7.5", "@babel/parser@^7.8.6", "@babel/parser@^7.8.7": - version "7.8.7" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.8.7.tgz#7b8facf95d25fef9534aad51c4ffecde1a61e26a" - integrity sha512-9JWls8WilDXFGxs0phaXAZgpxTZhSk/yOYH2hTHC0X1yC7Z78IJfvR1vJ+rmJKq3I35td2XzXzN6ZLYlna+r/A== +"@babel/parser@^7.1.0", "@babel/parser@^7.7.0", "@babel/parser@^7.7.5", "@babel/parser@^7.8.6", "@babel/parser@^7.9.0": + version "7.9.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8" + integrity sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA== "@babel/plugin-proposal-async-generator-functions@^7.8.3": version "7.8.3" @@ -350,10 +366,18 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" -"@babel/plugin-proposal-object-rest-spread@^7.8.3": +"@babel/plugin-proposal-numeric-separator@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.8.3.tgz#eb5ae366118ddca67bed583b53d7554cad9951bb" - integrity sha512-8qvuPwU/xxUCt78HocNlv0mXXo0wdh9VT1R04WU8HGOfaOob26pF+9P5/lYjN/q7DHOX1bvX60hnhOvuQUJdbA== + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.8.3.tgz#5d6769409699ec9b3b68684cd8116cedff93bad8" + integrity sha512-jWioO1s6R/R+wEHizfaScNsAx+xKgwTLNXSh7tTC4Usj3ItsPEhYkEpU4h+lpnBwq7NBVOJXfO6cRFYcX69JUQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.8.3" + +"@babel/plugin-proposal-object-rest-spread@^7.9.0": + version "7.9.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.9.0.tgz#a28993699fc13df165995362693962ba6b061d6f" + integrity sha512-UgqBv6bjq4fDb8uku9f+wcm1J7YxJ5nT7WO/jBr0cl0PLKb7t1O6RNR1kZbjgx2LQtsDI9hwoQVmn0yhXeQyow== dependencies: "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-object-rest-spread" "^7.8.0" @@ -366,10 +390,10 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" -"@babel/plugin-proposal-optional-chaining@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.8.3.tgz#ae10b3214cb25f7adb1f3bc87ba42ca10b7e2543" - integrity sha512-QIoIR9abkVn+seDE3OjA08jWcs3eZ9+wJCKSRgo3WdEU2csFYgdScb+8qHB3+WXsGJD55u+5hWCISI7ejXS+kg== +"@babel/plugin-proposal-optional-chaining@^7.9.0": + version "7.9.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.9.0.tgz#31db16b154c39d6b8a645292472b98394c292a58" + integrity sha512-NDn5tu3tcv4W30jNhmc2hyD5c56G6cXx4TesJubhxrJeCvuuMpttxr0OnNCqbZGhFjLrg+NIhxxC+BK5F6yS3w== dependencies: "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-optional-chaining" "^7.8.0" @@ -382,12 +406,12 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-throw-expressions" "^7.8.3" -"@babel/plugin-proposal-unicode-property-regex@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.8.3.tgz#b646c3adea5f98800c9ab45105ac34d06cd4a47f" - integrity sha512-1/1/rEZv2XGweRwwSkLpY+s60za9OZ1hJs4YDqFHCw0kYWYwL5IFljVY1MYBL+weT1l9pokDO2uhSTLVxzoHkQ== +"@babel/plugin-proposal-unicode-property-regex@^7.4.4", "@babel/plugin-proposal-unicode-property-regex@^7.8.3": + version "7.8.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.8.8.tgz#ee3a95e90cdc04fe8cd92ec3279fa017d68a0d1d" + integrity sha512-EVhjVsMpbhLw9ZfHWSx2iy13Q8Z/eg8e8ccVWt23sWQK5l1UdkoLJPN5w69UA4uITGBnEZD2JOe4QOHycYKv8A== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.8.3" + "@babel/helper-create-regexp-features-plugin" "^7.8.8" "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-async-generators@^7.8.0": @@ -425,6 +449,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" +"@babel/plugin-syntax-numeric-separator@^7.8.0", "@babel/plugin-syntax-numeric-separator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.8.3.tgz#0e3fb63e09bea1b11e96467271c8308007e7c41f" + integrity sha512-H7dCMAdN83PcCmqmkHB5dtp+Xa9a6LKSvA2hiFBC/5alSHxM5VgWZXFqDi0YFe8XNGT6iCa+z4V4zSt/PdZ7Dw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-syntax-object-rest-spread@^7.0.0", "@babel/plugin-syntax-object-rest-spread@^7.8.0": version "7.8.0" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.0.tgz#9b37d580d459682364d8602494c69145b394fd4c" @@ -491,10 +522,10 @@ "@babel/helper-plugin-utils" "^7.8.3" lodash "^4.17.13" -"@babel/plugin-transform-classes@^7.8.6": - version "7.8.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.8.6.tgz#77534447a477cbe5995ae4aee3e39fbc8090c46d" - integrity sha512-k9r8qRay/R6v5aWZkrEclEhKO6mc1CCQr2dLsVHBmOQiMpN6I2bpjX3vgnldUWeEI1GHVNByULVxZ4BdP4Hmdg== +"@babel/plugin-transform-classes@^7.9.0": + version "7.9.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.9.2.tgz#8603fc3cc449e31fdbdbc257f67717536a11af8d" + integrity sha512-TC2p3bPzsfvSsqBZo0kJnuelnoK9O3welkUpqSqBQuBF6R5MN2rysopri8kNvtlGIb2jmUO7i15IooAZJjZuMQ== dependencies: "@babel/helper-annotate-as-pure" "^7.8.3" "@babel/helper-define-map" "^7.8.3" @@ -519,7 +550,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-dotall-regex@^7.8.3": +"@babel/plugin-transform-dotall-regex@^7.4.4", "@babel/plugin-transform-dotall-regex@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.8.3.tgz#c3c6ec5ee6125c6993c5cbca20dc8621a9ea7a6e" integrity sha512-kLs1j9Nn4MQoBYdRXH6AeaXMbEJFaFu/v1nQkvib6QzTj8MZI5OQzqmD83/2jEM1z0DLilra5aWO5YpyC0ALIw== @@ -542,10 +573,10 @@ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.8.3" "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-for-of@^7.8.6": - version "7.8.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.8.6.tgz#a051bd1b402c61af97a27ff51b468321c7c2a085" - integrity sha512-M0pw4/1/KI5WAxPsdcUL/w2LJ7o89YHN3yLkzNjg7Yl15GlVGgzHyCU+FMeAxevHGsLVmUqbirlUIKTafPmzdw== +"@babel/plugin-transform-for-of@^7.9.0": + version "7.9.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.9.0.tgz#0f260e27d3e29cd1bb3128da5e76c761aa6c108e" + integrity sha512-lTAnWOpMwOXpyDx06N+ywmF3jNbafZEqZ96CGYabxHrxNX8l5ny7dt4bK/rGwAh9utyP2b2Hv7PlZh1AAS54FQ== dependencies: "@babel/helper-plugin-utils" "^7.8.3" @@ -571,41 +602,41 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-modules-amd@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.8.3.tgz#65606d44616b50225e76f5578f33c568a0b876a5" - integrity sha512-MadJiU3rLKclzT5kBH4yxdry96odTUwuqrZM+GllFI/VhxfPz+k9MshJM+MwhfkCdxxclSbSBbUGciBngR+kEQ== +"@babel/plugin-transform-modules-amd@^7.9.0": + version "7.9.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.9.0.tgz#19755ee721912cf5bb04c07d50280af3484efef4" + integrity sha512-vZgDDF003B14O8zJy0XXLnPH4sg+9X5hFBBGN1V+B2rgrB+J2xIypSN6Rk9imB2hSTHQi5OHLrFWsZab1GMk+Q== dependencies: - "@babel/helper-module-transforms" "^7.8.3" + "@babel/helper-module-transforms" "^7.9.0" "@babel/helper-plugin-utils" "^7.8.3" babel-plugin-dynamic-import-node "^2.3.0" -"@babel/plugin-transform-modules-commonjs@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.8.3.tgz#df251706ec331bd058a34bdd72613915f82928a5" - integrity sha512-JpdMEfA15HZ/1gNuB9XEDlZM1h/gF/YOH7zaZzQu2xCFRfwc01NXBMHHSTT6hRjlXJJs5x/bfODM3LiCk94Sxg== +"@babel/plugin-transform-modules-commonjs@^7.9.0": + version "7.9.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.9.0.tgz#e3e72f4cbc9b4a260e30be0ea59bdf5a39748940" + integrity sha512-qzlCrLnKqio4SlgJ6FMMLBe4bySNis8DFn1VkGmOcxG9gqEyPIOzeQrA//u0HAKrWpJlpZbZMPB1n/OPa4+n8g== dependencies: - "@babel/helper-module-transforms" "^7.8.3" + "@babel/helper-module-transforms" "^7.9.0" "@babel/helper-plugin-utils" "^7.8.3" "@babel/helper-simple-access" "^7.8.3" babel-plugin-dynamic-import-node "^2.3.0" -"@babel/plugin-transform-modules-systemjs@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.8.3.tgz#d8bbf222c1dbe3661f440f2f00c16e9bb7d0d420" - integrity sha512-8cESMCJjmArMYqa9AO5YuMEkE4ds28tMpZcGZB/jl3n0ZzlsxOAi3mC+SKypTfT8gjMupCnd3YiXCkMjj2jfOg== +"@babel/plugin-transform-modules-systemjs@^7.9.0": + version "7.9.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.9.0.tgz#e9fd46a296fc91e009b64e07ddaa86d6f0edeb90" + integrity sha512-FsiAv/nao/ud2ZWy4wFacoLOm5uxl0ExSQ7ErvP7jpoihLR6Cq90ilOFyX9UXct3rbtKsAiZ9kFt5XGfPe/5SQ== dependencies: "@babel/helper-hoist-variables" "^7.8.3" - "@babel/helper-module-transforms" "^7.8.3" + "@babel/helper-module-transforms" "^7.9.0" "@babel/helper-plugin-utils" "^7.8.3" babel-plugin-dynamic-import-node "^2.3.0" -"@babel/plugin-transform-modules-umd@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.8.3.tgz#592d578ce06c52f5b98b02f913d653ffe972661a" - integrity sha512-evhTyWhbwbI3/U6dZAnx/ePoV7H6OUG+OjiJFHmhr9FPn0VShjwC2kdxqIuQ/+1P50TMrneGzMeyMTFOjKSnAw== +"@babel/plugin-transform-modules-umd@^7.9.0": + version "7.9.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.9.0.tgz#e909acae276fec280f9b821a5f38e1f08b480697" + integrity sha512-uTWkXkIVtg/JGRSIABdBoMsoIeoHQHPTL0Y2E7xf5Oj7sLqwVsNXOkNk0VJc7vF0IMBsPeikHxFjGe+qmwPtTQ== dependencies: - "@babel/helper-module-transforms" "^7.8.3" + "@babel/helper-module-transforms" "^7.9.0" "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-transform-named-capturing-groups-regex@^7.8.3": @@ -705,12 +736,12 @@ "@babel/helper-create-regexp-features-plugin" "^7.8.3" "@babel/helper-plugin-utils" "^7.8.3" -"@babel/preset-env@~7.8.7": - version "7.8.7" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.8.7.tgz#1fc7d89c7f75d2d70c2b6768de6c2e049b3cb9db" - integrity sha512-BYftCVOdAYJk5ASsznKAUl53EMhfBbr8CJ1X+AJLfGPscQkwJFiaV/Wn9DPH/7fzm2v6iRYJKYHSqyynTGw0nw== +"@babel/preset-env@~7.9.0": + version "7.9.0" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.9.0.tgz#a5fc42480e950ae8f5d9f8f2bbc03f52722df3a8" + integrity sha512-712DeRXT6dyKAM/FMbQTV/FvRCms2hPCx+3weRjZ8iQVQWZejWWk1wwG6ViWMyqb/ouBbGOl5b6aCk0+j1NmsQ== dependencies: - "@babel/compat-data" "^7.8.6" + "@babel/compat-data" "^7.9.0" "@babel/helper-compilation-targets" "^7.8.7" "@babel/helper-module-imports" "^7.8.3" "@babel/helper-plugin-utils" "^7.8.3" @@ -718,14 +749,16 @@ "@babel/plugin-proposal-dynamic-import" "^7.8.3" "@babel/plugin-proposal-json-strings" "^7.8.3" "@babel/plugin-proposal-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-proposal-object-rest-spread" "^7.8.3" + "@babel/plugin-proposal-numeric-separator" "^7.8.3" + "@babel/plugin-proposal-object-rest-spread" "^7.9.0" "@babel/plugin-proposal-optional-catch-binding" "^7.8.3" - "@babel/plugin-proposal-optional-chaining" "^7.8.3" + "@babel/plugin-proposal-optional-chaining" "^7.9.0" "@babel/plugin-proposal-unicode-property-regex" "^7.8.3" "@babel/plugin-syntax-async-generators" "^7.8.0" "@babel/plugin-syntax-dynamic-import" "^7.8.0" "@babel/plugin-syntax-json-strings" "^7.8.0" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" + "@babel/plugin-syntax-numeric-separator" "^7.8.0" "@babel/plugin-syntax-object-rest-spread" "^7.8.0" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" "@babel/plugin-syntax-optional-chaining" "^7.8.0" @@ -734,20 +767,20 @@ "@babel/plugin-transform-async-to-generator" "^7.8.3" "@babel/plugin-transform-block-scoped-functions" "^7.8.3" "@babel/plugin-transform-block-scoping" "^7.8.3" - "@babel/plugin-transform-classes" "^7.8.6" + "@babel/plugin-transform-classes" "^7.9.0" "@babel/plugin-transform-computed-properties" "^7.8.3" "@babel/plugin-transform-destructuring" "^7.8.3" "@babel/plugin-transform-dotall-regex" "^7.8.3" "@babel/plugin-transform-duplicate-keys" "^7.8.3" "@babel/plugin-transform-exponentiation-operator" "^7.8.3" - "@babel/plugin-transform-for-of" "^7.8.6" + "@babel/plugin-transform-for-of" "^7.9.0" "@babel/plugin-transform-function-name" "^7.8.3" "@babel/plugin-transform-literals" "^7.8.3" "@babel/plugin-transform-member-expression-literals" "^7.8.3" - "@babel/plugin-transform-modules-amd" "^7.8.3" - "@babel/plugin-transform-modules-commonjs" "^7.8.3" - "@babel/plugin-transform-modules-systemjs" "^7.8.3" - "@babel/plugin-transform-modules-umd" "^7.8.3" + "@babel/plugin-transform-modules-amd" "^7.9.0" + "@babel/plugin-transform-modules-commonjs" "^7.9.0" + "@babel/plugin-transform-modules-systemjs" "^7.9.0" + "@babel/plugin-transform-modules-umd" "^7.9.0" "@babel/plugin-transform-named-capturing-groups-regex" "^7.8.3" "@babel/plugin-transform-new-target" "^7.8.3" "@babel/plugin-transform-object-super" "^7.8.3" @@ -761,17 +794,29 @@ "@babel/plugin-transform-template-literals" "^7.8.3" "@babel/plugin-transform-typeof-symbol" "^7.8.4" "@babel/plugin-transform-unicode-regex" "^7.8.3" - "@babel/types" "^7.8.7" - browserslist "^4.8.5" + "@babel/preset-modules" "^0.1.3" + "@babel/types" "^7.9.0" + browserslist "^4.9.1" core-js-compat "^3.6.2" invariant "^2.2.2" levenary "^1.1.1" semver "^5.5.0" -"@babel/register@^7.8.3", "@babel/register@^7.8.6": - version "7.8.6" - resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.8.6.tgz#a1066aa6168a73a70c35ef28cc5865ccc087ea69" - integrity sha512-7IDO93fuRsbyml7bAafBQb3RcBGlCpU4hh5wADA2LJEEcYk92WkwFZ0pHyIi2fb5Auoz1714abETdZKCOxN0CQ== +"@babel/preset-modules@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.3.tgz#13242b53b5ef8c883c3cf7dddd55b36ce80fbc72" + integrity sha512-Ra3JXOHBq2xd56xSF7lMKXdjBn3T772Y1Wet3yWnkDly9zHvJki029tAFzvAAK5cf4YV3yoxuP61crYRol6SVg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" + "@babel/plugin-transform-dotall-regex" "^7.4.4" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + +"@babel/register@^7.8.3", "@babel/register@^7.9.0": + version "7.9.0" + resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.9.0.tgz#02464ede57548bddbb5e9f705d263b7c3f43d48b" + integrity sha512-Tv8Zyi2J2VRR8g7pC5gTeIN8Ihultbmk0ocyNz8H2nEZbmhp1N6q0A1UGsQbDvGP/sNinQKUHf3SqXwqjtFv4Q== dependencies: find-cache-dir "^2.0.0" lodash "^4.17.13" @@ -795,14 +840,7 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.2" -"@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.8.7": +"@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== @@ -818,27 +856,27 @@ "@babel/parser" "^7.8.6" "@babel/types" "^7.8.6" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.7.0", "@babel/traverse@^7.7.4", "@babel/traverse@^7.8.3", "@babel/traverse@^7.8.4", "@babel/traverse@^7.8.6": - version "7.8.6" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.8.6.tgz#acfe0c64e1cd991b3e32eae813a6eb564954b5ff" - integrity sha512-2B8l0db/DPi8iinITKuo7cbPznLCEk0kCxDoB9/N6gGNg/gxOXiR/IcymAFPiBwk5w6TtQ27w4wpElgp9btR9A== +"@babel/traverse@^7.1.0", "@babel/traverse@^7.7.0", "@babel/traverse@^7.7.4", "@babel/traverse@^7.8.3", "@babel/traverse@^7.8.6", "@babel/traverse@^7.9.0": + version "7.9.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.9.0.tgz#d3882c2830e513f4fe4cec9fe76ea1cc78747892" + integrity sha512-jAZQj0+kn4WTHO5dUZkZKhbFrqZE7K5LAQ5JysMnmvGij+wOdr+8lWqPeW0BcF4wFwrEXXtdGO7wcV6YPJcf3w== dependencies: "@babel/code-frame" "^7.8.3" - "@babel/generator" "^7.8.6" + "@babel/generator" "^7.9.0" "@babel/helper-function-name" "^7.8.3" "@babel/helper-split-export-declaration" "^7.8.3" - "@babel/parser" "^7.8.6" - "@babel/types" "^7.8.6" + "@babel/parser" "^7.9.0" + "@babel/types" "^7.9.0" debug "^4.1.0" globals "^11.1.0" lodash "^4.17.13" -"@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.7.0", "@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.8.7": - version "7.8.7" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.7.tgz#1fc9729e1acbb2337d5b6977a63979b4819f5d1d" - integrity sha512-k2TreEHxFA4CjGkL+GYjRyx35W0Mr7DP5+9q6WMkyKXB+904bYmG40syjMFV0oLlhhFCwWl0vA0DyzTDkwAiJw== +"@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.4.4", "@babel/types@^7.7.0", "@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.8.7", "@babel/types@^7.9.0": + version "7.9.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.0.tgz#00b064c3df83ad32b2dbf5ff07312b15c7f1efb5" + integrity sha512-BS9JKfXkzzJl8RluW4JGknzpiUV7ZrvTayM6yfqLTVBEnFtyowVIOu6rqxRd5cVO6yGoWf4T8u8dgK9oB+GCng== dependencies: - esutils "^2.0.2" + "@babel/helper-validator-identifier" "^7.9.0" lodash "^4.17.13" to-fast-properties "^2.0.0" @@ -887,10 +925,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 +962,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" @@ -969,81 +1007,80 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== -"@jest/console@^25.1.0": - version "25.1.0" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-25.1.0.tgz#1fc765d44a1e11aec5029c08e798246bd37075ab" - integrity sha512-3P1DpqAMK/L07ag/Y9/Jup5iDEG9P4pRAuZiMQnU0JB3UOvCyYCjCoxr7sIA80SeyUCUKrr24fKAxVpmBgQonA== +"@jest/console@^25.2.3": + version "25.2.3" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-25.2.3.tgz#38ac19b916ff61457173799239472659e1a67c39" + integrity sha512-k+37B1aSvOt9tKHWbZZSOy1jdgzesB0bj96igCVUG1nAH1W5EoUfgc5EXbBVU08KSLvkVdWopLXaO3xfVGlxtQ== dependencies: - "@jest/source-map" "^25.1.0" + "@jest/source-map" "^25.2.1" chalk "^3.0.0" - jest-util "^25.1.0" + jest-util "^25.2.3" slash "^3.0.0" -"@jest/core@^25.1.0": - version "25.1.0" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-25.1.0.tgz#3d4634fc3348bb2d7532915d67781cdac0869e47" - integrity sha512-iz05+NmwCmZRzMXvMo6KFipW7nzhbpEawrKrkkdJzgytavPse0biEnCNr2wRlyCsp3SmKaEY+SGv7YWYQnIdig== +"@jest/core@^25.2.4": + version "25.2.4" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-25.2.4.tgz#382ef80369d3311f1df79db1ee19e958ae95cdad" + integrity sha512-WcWYShl0Bqfcb32oXtjwbiR78D/djhMdJW+ulp4/bmHgeODcsieqUJfUH+kEv8M7VNV77E6jds5aA+WuGh1nmg== dependencies: - "@jest/console" "^25.1.0" - "@jest/reporters" "^25.1.0" - "@jest/test-result" "^25.1.0" - "@jest/transform" "^25.1.0" - "@jest/types" "^25.1.0" + "@jest/console" "^25.2.3" + "@jest/reporters" "^25.2.4" + "@jest/test-result" "^25.2.4" + "@jest/transform" "^25.2.4" + "@jest/types" "^25.2.3" ansi-escapes "^4.2.1" chalk "^3.0.0" exit "^0.1.2" graceful-fs "^4.2.3" - jest-changed-files "^25.1.0" - jest-config "^25.1.0" - jest-haste-map "^25.1.0" - jest-message-util "^25.1.0" - jest-regex-util "^25.1.0" - jest-resolve "^25.1.0" - jest-resolve-dependencies "^25.1.0" - jest-runner "^25.1.0" - jest-runtime "^25.1.0" - jest-snapshot "^25.1.0" - jest-util "^25.1.0" - jest-validate "^25.1.0" - jest-watcher "^25.1.0" + jest-changed-files "^25.2.3" + jest-config "^25.2.4" + jest-haste-map "^25.2.3" + jest-message-util "^25.2.4" + jest-regex-util "^25.2.1" + jest-resolve "^25.2.3" + jest-resolve-dependencies "^25.2.4" + jest-runner "^25.2.4" + jest-runtime "^25.2.4" + jest-snapshot "^25.2.4" + jest-util "^25.2.3" + jest-validate "^25.2.3" + jest-watcher "^25.2.4" micromatch "^4.0.2" p-each-series "^2.1.0" - realpath-native "^1.1.0" + realpath-native "^2.0.0" rimraf "^3.0.0" slash "^3.0.0" strip-ansi "^6.0.0" -"@jest/environment@^25.1.0": - version "25.1.0" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-25.1.0.tgz#4a97f64770c9d075f5d2b662b5169207f0a3f787" - integrity sha512-cTpUtsjU4cum53VqBDlcW0E4KbQF03Cn0jckGPW/5rrE9tb+porD3+hhLtHAwhthsqfyF+bizyodTlsRA++sHg== +"@jest/environment@^25.2.4": + version "25.2.4" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-25.2.4.tgz#74f4d8dd87b427434d0b822cde37bc0e78f3e28b" + integrity sha512-wA4xlhD19/gukkDpJ5HQsTle0pgnzI5qMFEjw267lpTDC8d9N7Ihqr5pI+l0p8Qn1SQhai+glSqxrGdzKy4jxw== dependencies: - "@jest/fake-timers" "^25.1.0" - "@jest/types" "^25.1.0" - jest-mock "^25.1.0" + "@jest/fake-timers" "^25.2.4" + "@jest/types" "^25.2.3" + jest-mock "^25.2.3" -"@jest/fake-timers@^25.1.0": - version "25.1.0" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-25.1.0.tgz#a1e0eff51ffdbb13ee81f35b52e0c1c11a350ce8" - integrity sha512-Eu3dysBzSAO1lD7cylZd/CVKdZZ1/43SF35iYBNV1Lvvn2Undp3Grwsv8PrzvbLhqwRzDd4zxrY4gsiHc+wygQ== +"@jest/fake-timers@^25.2.4": + version "25.2.4" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-25.2.4.tgz#6821b6edde74fda2a42467ae92cc93095d4c9527" + integrity sha512-oC1TJiwfMcBttVN7Wz+VZnqEAgYTiEMu0QLOXpypR89nab0uCB31zm/QeBZddhSstn20qe3yqOXygp6OwvKT/Q== dependencies: - "@jest/types" "^25.1.0" - jest-message-util "^25.1.0" - jest-mock "^25.1.0" - jest-util "^25.1.0" + "@jest/types" "^25.2.3" + jest-message-util "^25.2.4" + jest-mock "^25.2.3" + jest-util "^25.2.3" lolex "^5.0.0" -"@jest/reporters@^25.1.0": - version "25.1.0" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-25.1.0.tgz#9178ecf136c48f125674ac328f82ddea46e482b0" - integrity sha512-ORLT7hq2acJQa8N+NKfs68ZtHFnJPxsGqmofxW7v7urVhzJvpKZG9M7FAcgh9Ee1ZbCteMrirHA3m5JfBtAaDg== +"@jest/reporters@^25.2.4": + version "25.2.4" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-25.2.4.tgz#aa01c20aab217150d3a6080d5c98ce0bf34b17ed" + integrity sha512-VHbLxM03jCc+bTLOluW/IqHR2G0Cl0iATwIQbuZtIUast8IXO4fD0oy4jpVGpG5b20S6REA8U3BaQoCW/CeVNQ== dependencies: "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^25.1.0" - "@jest/environment" "^25.1.0" - "@jest/test-result" "^25.1.0" - "@jest/transform" "^25.1.0" - "@jest/types" "^25.1.0" + "@jest/console" "^25.2.3" + "@jest/test-result" "^25.2.4" + "@jest/transform" "^25.2.4" + "@jest/types" "^25.2.3" chalk "^3.0.0" collect-v8-coverage "^1.0.0" exit "^0.1.2" @@ -1053,11 +1090,10 @@ istanbul-lib-report "^3.0.0" istanbul-lib-source-maps "^4.0.0" istanbul-reports "^3.0.0" - jest-haste-map "^25.1.0" - jest-resolve "^25.1.0" - jest-runtime "^25.1.0" - jest-util "^25.1.0" - jest-worker "^25.1.0" + jest-haste-map "^25.2.3" + jest-resolve "^25.2.3" + jest-util "^25.2.3" + jest-worker "^25.2.1" slash "^3.0.0" source-map "^0.6.0" string-length "^3.1.0" @@ -1066,62 +1102,62 @@ optionalDependencies: node-notifier "^6.0.0" -"@jest/source-map@^25.1.0": - version "25.1.0" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-25.1.0.tgz#b012e6c469ccdbc379413f5c1b1ffb7ba7034fb0" - integrity sha512-ohf2iKT0xnLWcIUhL6U6QN+CwFWf9XnrM2a6ybL9NXxJjgYijjLSitkYHIdzkd8wFliH73qj/+epIpTiWjRtAA== +"@jest/source-map@^25.2.1": + version "25.2.1" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-25.2.1.tgz#b62ecf8ae76170b08eff8859b56eb7576df34ab8" + integrity sha512-PgScGJm1U27+9Te/cxP4oUFqJ2PX6NhBL2a6unQ7yafCgs8k02c0LSyjSIx/ao0AwcAdCczfAPDf5lJ7zoB/7A== dependencies: callsites "^3.0.0" graceful-fs "^4.2.3" source-map "^0.6.0" -"@jest/test-result@^25.1.0": - version "25.1.0" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-25.1.0.tgz#847af2972c1df9822a8200457e64be4ff62821f7" - integrity sha512-FZzSo36h++U93vNWZ0KgvlNuZ9pnDnztvaM7P/UcTx87aPDotG18bXifkf1Ji44B7k/eIatmMzkBapnAzjkJkg== +"@jest/test-result@^25.2.4": + version "25.2.4" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-25.2.4.tgz#8fc9eac58e82eb2a82e4058e68c3814f98f59cf5" + integrity sha512-AI7eUy+q2lVhFnaibDFg68NGkrxVWZdD6KBr9Hm6EvN0oAe7GxpEwEavgPfNHQjU2mi6g+NsFn/6QPgTUwM1qg== dependencies: - "@jest/console" "^25.1.0" - "@jest/transform" "^25.1.0" - "@jest/types" "^25.1.0" + "@jest/console" "^25.2.3" + "@jest/transform" "^25.2.4" + "@jest/types" "^25.2.3" "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-sequencer@^25.1.0": - version "25.1.0" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-25.1.0.tgz#4df47208542f0065f356fcdb80026e3c042851ab" - integrity sha512-WgZLRgVr2b4l/7ED1J1RJQBOharxS11EFhmwDqknpknE0Pm87HLZVS2Asuuw+HQdfQvm2aXL2FvvBLxOD1D0iw== +"@jest/test-sequencer@^25.2.4": + version "25.2.4" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-25.2.4.tgz#28364aeddec140c696324114f63570f3de536c87" + integrity sha512-TEZm/Rkd6YgskdpTJdYLBtu6Gc11tfWPuSpatq0duH77ekjU8dpqX2zkPdY/ayuHxztV5LTJoV5BLtI9mZfXew== dependencies: - "@jest/test-result" "^25.1.0" - jest-haste-map "^25.1.0" - jest-runner "^25.1.0" - jest-runtime "^25.1.0" + "@jest/test-result" "^25.2.4" + jest-haste-map "^25.2.3" + jest-runner "^25.2.4" + jest-runtime "^25.2.4" -"@jest/transform@^25.1.0": - version "25.1.0" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-25.1.0.tgz#221f354f512b4628d88ce776d5b9e601028ea9da" - integrity sha512-4ktrQ2TPREVeM+KxB4zskAT84SnmG1vaz4S+51aTefyqn3zocZUnliLLm5Fsl85I3p/kFPN4CRp1RElIfXGegQ== +"@jest/transform@^25.2.4": + version "25.2.4" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-25.2.4.tgz#34336f37f13f62f7d1f5b93d5d150ba9eb3e11b9" + integrity sha512-6eRigvb+G6bs4kW5j1/y8wu4nCrmVuIe0epPBbiWaYlwawJ8yi1EIyK3d/btDqmBpN5GpN4YhR6iPPnDmkYdTA== dependencies: "@babel/core" "^7.1.0" - "@jest/types" "^25.1.0" + "@jest/types" "^25.2.3" babel-plugin-istanbul "^6.0.0" chalk "^3.0.0" convert-source-map "^1.4.0" fast-json-stable-stringify "^2.0.0" graceful-fs "^4.2.3" - jest-haste-map "^25.1.0" - jest-regex-util "^25.1.0" - jest-util "^25.1.0" + jest-haste-map "^25.2.3" + jest-regex-util "^25.2.1" + jest-util "^25.2.3" micromatch "^4.0.2" pirates "^4.0.1" - realpath-native "^1.1.0" + realpath-native "^2.0.0" slash "^3.0.0" source-map "^0.6.1" write-file-atomic "^3.0.0" -"@jest/types@^25.1.0": - version "25.1.0" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.1.0.tgz#b26831916f0d7c381e11dbb5e103a72aed1b4395" - integrity sha512-VpOtt7tCrgvamWZh1reVsGADujKigBUFTi19mlRjqEGsE8qH4r3s+skY33dNdXOwyZIvuftZ5tqdF1IgsMejMA== +"@jest/types@^25.2.3": + version "25.2.3" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.2.3.tgz#035c4fb94e2da472f359ff9a211915d59987f6b6" + integrity sha512-6oLQwO9mKif3Uph3RX5J1i3S7X7xtDHWBaaaoeKw8hOzV6YUd0qDcYcHZ6QXMHDIzSr7zzrEa51o2Ovlj6AtKQ== dependencies: "@types/istanbul-lib-coverage" "^2.0.0" "@types/istanbul-reports" "^1.1.1" @@ -1262,83 +1298,83 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= -"@sentry/apm@5.14.1": - version "5.14.1" - resolved "https://registry.yarnpkg.com/@sentry/apm/-/apm-5.14.1.tgz#99605c4cf933962aeda4a1b03e99256213e51d7d" - integrity sha512-vm5Ee0VPAZ5TmO3iKwLFwTAtDj0ZVJvawJCfBjUe5sn58GL60XapN5AsXICdsh4fbgtNJVZEtsawcL2JNVJvuQ== +"@sentry/apm@5.15.4": + version "5.15.4" + resolved "https://registry.yarnpkg.com/@sentry/apm/-/apm-5.15.4.tgz#59af766d2bb4c9d98eda5ddba7a32a79ecc807a2" + integrity sha512-gcW225Jls1ShyBXMWN6zZyuVJwBOIQ63sI+URI2NSFsdpBpdpZ8yennIm+oMlSfb25Nzt9SId7TRSjPhlSbTZQ== dependencies: - "@sentry/browser" "5.14.1" - "@sentry/hub" "5.14.1" - "@sentry/minimal" "5.14.1" - "@sentry/types" "5.14.1" - "@sentry/utils" "5.14.1" + "@sentry/browser" "5.15.4" + "@sentry/hub" "5.15.4" + "@sentry/minimal" "5.15.4" + "@sentry/types" "5.15.4" + "@sentry/utils" "5.15.4" tslib "^1.9.3" -"@sentry/browser@5.14.1": - version "5.14.1" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.14.1.tgz#ccd806d77b4eff1ae6ca1ec3a0839b9bbb5dd241" - integrity sha512-PiadrPcsPcCjHrfT8nxzVFi0WiekLYh2unGnkSFoIvS8dGv+fjIX0gqYPy7UHhCZZUsLCCiuWpoaSDWymkZGTA== +"@sentry/browser@5.15.4": + version "5.15.4" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.15.4.tgz#5a7e7bad088556665ed8e69bceb0e18784e4f6c7" + integrity sha512-l/auT1HtZM3KxjCGQHYO/K51ygnlcuOrM+7Ga8gUUbU9ZXDYw6jRi0+Af9aqXKmdDw1naNxr7OCSy6NBrLWVZw== dependencies: - "@sentry/core" "5.14.1" - "@sentry/types" "5.14.1" - "@sentry/utils" "5.14.1" + "@sentry/core" "5.15.4" + "@sentry/types" "5.15.4" + "@sentry/utils" "5.15.4" tslib "^1.9.3" -"@sentry/core@5.14.1": - version "5.14.1" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.14.1.tgz#21a7c14ca08b0f228023f9c6f399db1e35cd6438" - integrity sha512-UPwnS7P4xdjlC60ybzKZ/o1kYkVCublD0OUW9JfLJpIyiu3QEA3QUHjbambzvU47WHOSy9b4RYtQll44WMtdaw== +"@sentry/core@5.15.4": + version "5.15.4" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.15.4.tgz#08b617e093a636168be5aebad141d1f744217085" + integrity sha512-9KP4NM4SqfV5NixpvAymC7Nvp36Zj4dU2fowmxiq7OIbzTxGXDhwuN/t0Uh8xiqlkpkQqSECZ1OjSFXrBldetQ== dependencies: - "@sentry/hub" "5.14.1" - "@sentry/minimal" "5.14.1" - "@sentry/types" "5.14.1" - "@sentry/utils" "5.14.1" + "@sentry/hub" "5.15.4" + "@sentry/minimal" "5.15.4" + "@sentry/types" "5.15.4" + "@sentry/utils" "5.15.4" tslib "^1.9.3" -"@sentry/hub@5.14.1": - version "5.14.1" - resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.14.1.tgz#1a4515558705b2680a6e9f3cb8092555ed31324a" - integrity sha512-FTqeLcOWbww18WRQ8F3Z0DryQwD5DeuapXlinez7Y+HDZZAOm7HdPDEY5xjrUzi4V0n9BtgfJDp27UKJENWtYA== +"@sentry/hub@5.15.4": + version "5.15.4" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.15.4.tgz#cb64473725a60eec63b0be58ed1143eaaf894bee" + integrity sha512-1XJ1SVqadkbUT4zLS0TVIVl99si7oHizLmghR8LMFl5wOkGEgehHSoOydQkIAX2C7sJmaF5TZ47ORBHgkqclUg== dependencies: - "@sentry/types" "5.14.1" - "@sentry/utils" "5.14.1" + "@sentry/types" "5.15.4" + "@sentry/utils" "5.15.4" tslib "^1.9.3" -"@sentry/minimal@5.14.1": - version "5.14.1" - resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.14.1.tgz#3ec74503cd72772de56188f010a35d9bc956dc94" - integrity sha512-TC+i37rGBomu74xcSdPA4xeqSeWEvTHP5dx+LIJunntk4/mDi5/aC3HSMxRy3ifoJiUwZO+gyJJ7I/UMzvOMEw== +"@sentry/minimal@5.15.4": + version "5.15.4" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.15.4.tgz#113f01fefb86b7830994c3dfa7ad4889ba7b2003" + integrity sha512-GL4GZ3drS9ge+wmxkHBAMEwulaE7DMvAEfKQPDAjg2p3MfcCMhAYfuY4jJByAC9rg9OwBGGehz7UmhWMFjE0tw== dependencies: - "@sentry/hub" "5.14.1" - "@sentry/types" "5.14.1" + "@sentry/hub" "5.15.4" + "@sentry/types" "5.15.4" tslib "^1.9.3" -"@sentry/node@^5.14.1": - version "5.14.1" - resolved "https://registry.yarnpkg.com/@sentry/node/-/node-5.14.1.tgz#ebac38bd5036d7feefa0b44569e731b59441ce27" - integrity sha512-bkOp6RRynt0C8J2yRF6RII9/dn03WFo5Yk/di7753VW1CUgjUOjtozirs0PYewlfXfaVHveixjN61Mcy4XwJ+g== +"@sentry/node@^5.15.4": + version "5.15.4" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-5.15.4.tgz#e7bc3962d321a12b633743200165ca5f1757cb68" + integrity sha512-OfdhNEvOJZ55ZkCUcVgctjaZkOw7rmLzO5VyDTSgevA4uLsPaTNXSAeK2GSQBXc5J0KdRpNz4sSIyuxOS4Z7Vg== dependencies: - "@sentry/apm" "5.14.1" - "@sentry/core" "5.14.1" - "@sentry/hub" "5.14.1" - "@sentry/types" "5.14.1" - "@sentry/utils" "5.14.1" + "@sentry/apm" "5.15.4" + "@sentry/core" "5.15.4" + "@sentry/hub" "5.15.4" + "@sentry/types" "5.15.4" + "@sentry/utils" "5.15.4" cookie "^0.3.1" https-proxy-agent "^4.0.0" lru_map "^0.3.3" tslib "^1.9.3" -"@sentry/types@5.14.1": - version "5.14.1" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.14.1.tgz#564f9b703c06c2a97e756f60a3c873b977b2bbd2" - integrity sha512-Eht+qI68BxELKQRnaV6q1PxS9/E+YatUHjG80ov9/azLbIJElQhWUDzyyJCefDD/fEI37VQ2SrsQsVqS9W7ZOQ== +"@sentry/types@5.15.4": + version "5.15.4" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.15.4.tgz#37f30e35b06e8e12ad1101f1beec3e9b88ca1aab" + integrity sha512-quPHPpeAuwID48HLPmqBiyXE3xEiZLZ5D3CEbU3c3YuvvAg8qmfOOTI6z4Z3Eedi7flvYpnx3n7N3dXIEz30Eg== -"@sentry/utils@5.14.1": - version "5.14.1" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.14.1.tgz#577a9dd7b5f4b3436e8e2847d058547eab2ed5c4" - integrity sha512-iCN5p+ArPVMkt09bRfYuldtKSTQEqRfNuoCxPFJKDfMyLO9YetIwRCwHluIRoJoEIFE4JTM2F4HfmTsxoTqw4g== +"@sentry/utils@5.15.4": + version "5.15.4" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.15.4.tgz#02865ab3c9b745656cea0ab183767ec26c96f6e6" + integrity sha512-lO8SLBjrUDGADl0LOkd55R5oL510d/1SaI08/IBHZCxCUwI4TiYo5EPECq8mrj3XGfgCyq9osw33bymRlIDuSQ== dependencies: - "@sentry/types" "5.14.1" + "@sentry/types" "5.15.4" tslib "^1.9.3" "@sindresorhus/is@^0.14.0": @@ -1557,6 +1593,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.16.tgz#4d690c96cbb7b2728afea0e260d680501b3da5cf" integrity sha512-/opXIbfn0P+VLt+N8DE4l8Mn8rbhiJgabU96ZJ0p9mxOkIks5gh6RUnpHak7Yh0SFkyjO/ODbxsQQPV2bpMmyA== +"@types/prettier@^1.19.0": + version "1.19.1" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-1.19.1.tgz#33509849f8e679e4add158959fdb086440e9553f" + integrity sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ== + "@types/range-parser@*": version "1.2.3" resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" @@ -1594,10 +1635,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.34": + version "0.26.34" + resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.34.tgz#199329f9fee5074a7385b4a4a25d1559db628aef" + integrity sha512-/zH/Yuwl2vC5fgPE+Qtv67iI/o50qLJJsR0KVc86cpDlY2IsRv3yJop1v/9hfNrgy7J8J5BpJM4BMhyFE3QE4w== "@types/zen-observable@^0.8.0": version "0.8.0" @@ -1676,9 +1717,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" @@ -2217,6 +2258,21 @@ audio-extensions@0.0.0: resolved "https://registry.yarnpkg.com/audio-extensions/-/audio-extensions-0.0.0.tgz#d0eefe077fb9eb625898eed9985890548cf1f8d2" integrity sha1-0O7+B3+562JYmO7ZmFiQVIzx+NI= +aws-sdk@^2.650.0: + version "2.650.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.650.0.tgz#edf995cf2805c918d7470a652f1316ae902c5aa4" + integrity sha512-MlTKXeRSe4IXXqnulAiXZccpTgDafs3ofYIQv/7ApR+oQUFsq96RHwe8MdW9N1cXn7fz302jLXUAykj4boR3DA== + dependencies: + buffer "4.9.1" + events "1.1.1" + ieee754 "1.1.13" + jmespath "0.15.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.3.2" + xml2js "0.4.19" + aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -2244,16 +2300,16 @@ babel-eslint@~10.1.0: eslint-visitor-keys "^1.0.0" resolve "^1.12.0" -babel-jest@^25.1.0, babel-jest@~25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-25.1.0.tgz#206093ac380a4b78c4404a05b3277391278f80fb" - integrity sha512-tz0VxUhhOE2y+g8R2oFrO/2VtVjA1lkJeavlhExuRBg3LdNJY9gwQ+Vcvqt9+cqy71MCTJhewvTB7Qtnnr9SWg== +babel-jest@^25.2.4, babel-jest@~25.2.4: + version "25.2.4" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-25.2.4.tgz#b21b68d3af8f161c3e6e501e91f0dea8e652e344" + integrity sha512-+yDzlyJVWrqih9i2Cvjpt7COaN8vUwCsKGtxJLzg6I0xhxD54K8mvDUCliPKLufyzHh/c5C4MRj4Vk7VMjOjIg== dependencies: - "@jest/transform" "^25.1.0" - "@jest/types" "^25.1.0" + "@jest/transform" "^25.2.4" + "@jest/types" "^25.2.3" "@types/babel__core" "^7.1.0" babel-plugin-istanbul "^6.0.0" - babel-preset-jest "^25.1.0" + babel-preset-jest "^25.2.1" chalk "^3.0.0" slash "^3.0.0" @@ -2275,10 +2331,10 @@ babel-plugin-istanbul@^6.0.0: istanbul-lib-instrument "^4.0.0" test-exclude "^6.0.0" -babel-plugin-jest-hoist@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-25.1.0.tgz#fb62d7b3b53eb36c97d1bc7fec2072f9bd115981" - integrity sha512-oIsopO41vW4YFZ9yNYoLQATnnN46lp+MZ6H4VvPKFkcc2/fkl3CfE/NZZSmnEIEsJRmJAgkVEK0R7Zbl50CpTw== +babel-plugin-jest-hoist@^25.2.1: + version "25.2.1" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-25.2.1.tgz#d0003a1f3d5caa281e1107fe03bbf16b799f9955" + integrity sha512-HysbCQfJhxLlyxDbKcB2ucGYV0LjqK4h6dBoI3RtFuOxTiTWK6XGZMsHb0tGh8iJdV4hC6Z2GCHzVvDeh9i0lQ== dependencies: "@types/babel__traverse" "^7.0.6" @@ -2289,14 +2345,14 @@ babel-plugin-transform-runtime@^6.23.0: dependencies: babel-runtime "^6.22.0" -babel-preset-jest@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-25.1.0.tgz#d0aebfebb2177a21cde710996fce8486d34f1d33" - integrity sha512-eCGn64olaqwUMaugXsTtGAM2I0QTahjEtnRu0ql8Ie+gDWAc1N6wqN0k2NilnyTunM69Pad7gJY7LOtwLimoFQ== +babel-preset-jest@^25.2.1: + version "25.2.1" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-25.2.1.tgz#4ccd0e577f69aa11b71806edfe8b25a5c3ac93a2" + integrity sha512-zXHJBM5iR8oEO4cvdF83AQqqJf3tJrXy3x8nfu2Nlqvn4cneg4Ca8M7cQvC5S9BzDDy1O0tZ9iXru9J6E3ym+A== dependencies: "@babel/plugin-syntax-bigint" "^7.0.0" "@babel/plugin-syntax-object-rest-spread" "^7.0.0" - babel-plugin-jest-hoist "^25.1.0" + babel-plugin-jest-hoist "^25.2.1" babel-runtime@^6.22.0: version "6.26.0" @@ -2316,6 +2372,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +base64-js@^1.0.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" + integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== + base@^0.11.1: version "0.11.2" resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" @@ -2443,7 +2504,7 @@ browser-resolve@^1.11.3: dependencies: resolve "1.1.7" -browserslist@^4.8.3, browserslist@^4.8.5, browserslist@^4.9.1: +browserslist@^4.8.3, browserslist@^4.9.1: version "4.9.1" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.9.1.tgz#01ffb9ca31a1aef7678128fc6a2253316aa7287c" integrity sha512-Q0DnKq20End3raFulq6Vfp1ecB9fh8yUNV55s8sekaDDeqBaCtWlRHCUdaWyUeSSBJM7IbM6HcsyaeYqgeDhnw== @@ -2469,6 +2530,15 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== +buffer@4.9.1: + version "4.9.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" + integrity sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg= + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + busboy@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.3.1.tgz#170899274c5bf38aae27d5c62b71268cd585fd1b" @@ -3127,10 +3197,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.1: + version "2.11.1" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.11.1.tgz#197b8be1bbf5c5e6fe8bea817f0fe111820e7a12" + integrity sha512-3RdUoinZ43URd2MJcquzBbDQo+J87cSzB8NkXdZiN5ia1UNyep0oCyitfiL88+R7clGTeq/RniXAc16gWyAu1w== dateformat@^2.0.0: version "2.2.0" @@ -3197,7 +3267,7 @@ deep-is@~0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= -deepmerge@4.2.2: +deepmerge@4.2.2, deepmerge@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== @@ -3288,10 +3358,10 @@ dicer@0.3.0: dependencies: streamsearch "0.1.2" -diff-sequences@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.1.0.tgz#fd29a46f1c913fd66c22645dc75bffbe43051f32" - integrity sha512-nFIfVk5B/NStCsJ+zaPO4vYuLjlzQ6uFvPxzYyHlejNZ/UGa7G/n7peOXVrVNvRuyfstt+mZQYGpjxg9Z6N8Kw== +diff-sequences@^25.2.1: + version "25.2.1" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.2.1.tgz#fcfe8aa07dd9b0c648396a478dabca8e76c6ab27" + integrity sha512-foe7dXnGlSh3jR1ovJmdv+77VQj98eKCHHwJPbZ2eEf0fHwKbkZicpPxEch9smZ+n2dnF6QFwkOQdLq9hpeJUg== diff@^4.0.1: version "4.0.1" @@ -3610,17 +3680,17 @@ escodegen@^1.11.1: optionalDependencies: source-map "~0.6.1" -eslint-config-prettier@~6.10.0: - version "6.10.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.10.0.tgz#7b15e303bf9c956875c948f6b21500e48ded6a7f" - integrity sha512-AtndijGte1rPILInUdHjvKEGbIV06NuvPrqlIEaEaWtbtvJh464mDeyGMdZEQMsGvC0ZVkiex1fSNcC4HAbRGg== +eslint-config-prettier@~6.10.1: + version "6.10.1" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.10.1.tgz#129ef9ec575d5ddc0e269667bf09defcd898642a" + integrity sha512-svTy6zh1ecQojvpbJSgH3aei/Rt7C6i090l5f2WQ4aB05lYHeZIR1qL4wZyyILTbtmnbHP5Yn8MrsOJMGa8RkQ== dependencies: get-stdin "^6.0.0" -eslint-config-standard@~14.1.0: - version "14.1.0" - resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-14.1.0.tgz#b23da2b76fe5a2eba668374f246454e7058f15d4" - integrity sha512-EF6XkrrGVbvv8hL/kYa/m6vnvmUT+K82pJJc4JJVMM6+Qgqh0pnwprSxdduDLB9p/7bIxD+YV5O0wfb8lmcPbA== +eslint-config-standard@~14.1.1: + version "14.1.1" + resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-14.1.1.tgz#830a8e44e7aef7de67464979ad06b406026c56ea" + integrity sha512-Z9B+VR+JIXRxz21udPTL9HpFMyoMUEeX1G251EQ6e05WD9aPVtVBn09XUmZ259wCMlCDmYDSZG62Hhm+ZTJcUg== eslint-import-resolver-node@^0.3.2: version "0.3.2" @@ -3646,10 +3716,10 @@ eslint-plugin-es@^3.0.0: eslint-utils "^2.0.0" regexpp "^3.0.0" -eslint-plugin-import@~2.20.1: - version "2.20.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.20.1.tgz#802423196dcb11d9ce8435a5fc02a6d3b46939b3" - integrity sha512-qQHgFOTjguR+LnYRoToeZWT62XM55MBVXObHM6SKFd1VzDcX/vqT1kAz8ssqigh5eMj8qXcRoXXGZpPP6RfdCw== +eslint-plugin-import@~2.20.2: + version "2.20.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.20.2.tgz#91fc3807ce08be4837141272c8b99073906e588d" + integrity sha512-FObidqpXrR8OnCh4iNsxy+WACztJLXAHBO5hK79T1Hc77PgQZkyDGA5Ag9xAvRpglvLNxhH/zSmZ70/pZ31dHg== dependencies: array-includes "^3.0.3" array.prototype.flat "^1.2.1" @@ -3671,10 +3741,10 @@ eslint-plugin-jest@~23.8.2: dependencies: "@typescript-eslint/experimental-utils" "^2.5.0" -eslint-plugin-node@~11.0.0: - version "11.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-11.0.0.tgz#365944bb0804c5d1d501182a9bc41a0ffefed726" - integrity sha512-chUs/NVID+sknFiJzxoN9lM7uKSOEta8GC8365hw1nDfwIPIjjpRSwwPvQanWv8dt/pDe9EV4anmVSwdiSndNg== +eslint-plugin-node@~11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz#c95544416ee4ada26740a30474eefc5402dc671d" + integrity sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g== dependencies: eslint-plugin-es "^3.0.0" eslint-utils "^2.0.0" @@ -3818,6 +3888,11 @@ eventemitter3@^3.1.0: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== +events@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= + exec-sh@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.2.tgz#6738de2eb7c8e671d0366aea0b0db8c6f7d7391b" @@ -3901,17 +3976,17 @@ expect-ct@0.2.0: resolved "https://registry.yarnpkg.com/expect-ct/-/expect-ct-0.2.0.tgz#3a54741b6ed34cc7a93305c605f63cd268a54a62" integrity sha512-6SK3MG/Bbhm8MsgyJAylg+ucIOU71/FzyFalcfu5nY19dH8y/z0tBJU0wrNBXD4B27EoQtqPF/9wqH0iYAd04g== -expect@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/expect/-/expect-25.1.0.tgz#7e8d7b06a53f7d66ec927278db3304254ee683ee" - integrity sha512-wqHzuoapQkhc3OKPlrpetsfueuEiMf3iWh0R8+duCu9PIjXoP7HgD5aeypwTnXUAjC8aMsiVDaWwlbJ1RlQ38g== +expect@^25.2.4: + version "25.2.4" + resolved "https://registry.yarnpkg.com/expect/-/expect-25.2.4.tgz#b66e0777c861034ebc21730bb34e1839d5d46806" + integrity sha512-hfuPhPds4yOsZtIw4kwAg70r0hqGmpqekgA+VX7pf/3wZ6FY+xIOXZhNsPMMMsspYG/YIsbAiwqsdnD4Ht+bCA== dependencies: - "@jest/types" "^25.1.0" + "@jest/types" "^25.2.3" ansi-styles "^4.0.0" - jest-get-type "^25.1.0" - jest-matcher-utils "^25.1.0" - jest-message-util "^25.1.0" - jest-regex-util "^25.1.0" + jest-get-type "^25.2.1" + jest-matcher-utils "^25.2.3" + jest-message-util "^25.2.4" + jest-regex-util "^25.2.1" express@^4.0.0, express@^4.17.1: version "4.17.1" @@ -4505,12 +4580,12 @@ graphql-redis-subscriptions@^2.2.1: optionalDependencies: ioredis "^4.6.3" -graphql-shield@~7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-7.2.0.tgz#81b26794370608ad78dfe3833473789fb471fbd8" - integrity sha512-eLdD+gUIKYu77XRcuHs5ewZhiBuRFeWFGxPnJa+g9AkxB7Yi5RSEjEJEx0Drg9GuNvDYpHeW7nPff4v35AT2aQ== +graphql-shield@~7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-7.2.2.tgz#daf9a91c6b8487b5f00dea12c8ab3bb85947471a" + integrity sha512-Zab6O+xHHfyxVoyEROnkjw3Ep23C7oYtqYKbuXEyenaPgif9ncUp+JKA3GA2HEzVjkxw/psfSeaQWWz3VFtXeA== dependencies: - "@types/yup" "0.26.32" + "@types/yup" "0.26.34" object-hash "^2.0.3" yup "^0.28.3" @@ -4669,20 +4744,20 @@ helmet-crossdomain@0.4.0: resolved "https://registry.yarnpkg.com/helmet-crossdomain/-/helmet-crossdomain-0.4.0.tgz#5f1fe5a836d0325f1da0a78eaa5fd8429078894e" integrity sha512-AB4DTykRw3HCOxovD1nPR16hllrVImeFp5VBV9/twj66lJ2nU75DP8FPL0/Jp4jj79JhTfG+pFI2MD02kWJ+fA== -helmet-csp@2.9.5: - version "2.9.5" - resolved "https://registry.yarnpkg.com/helmet-csp/-/helmet-csp-2.9.5.tgz#ea1ebec6d481e8f9aa5f48cc4ca2714e031f627d" - integrity sha512-w9nps5adqFQwgktVPDbXkARmZot/nr8aegzQas9AXdBSwBFBBefPpDSTV0wtgHlAUdDwY6MZo7qAl9yts3ppJg== +helmet-csp@2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/helmet-csp/-/helmet-csp-2.10.0.tgz#685dde1747bc16c5e28ad9d91e229a69f0a85e84" + integrity sha512-Rz953ZNEFk8sT2XvewXkYN0Ho4GEZdjAZy4stjiEQV3eN7GDxg1QKmYggH7otDyIA7uGA6XnUMVSgeJwbR5X+w== dependencies: bowser "2.9.0" camelize "1.0.0" content-security-policy-builder "2.1.0" dasherize "2.0.0" -helmet@~3.21.3: - version "3.21.3" - resolved "https://registry.yarnpkg.com/helmet/-/helmet-3.21.3.tgz#15777aae82a4d2678c104fd18195a4012f429b67" - integrity sha512-8OjGNdpG3WQhPO71fSy2fT4X3FSNutU1LDeAf+YS+Vil6r+fE7w8per5mNed6egGYbZl3QhKXgFzMYSwys+YQw== +helmet@~3.22.0: + version "3.22.0" + resolved "https://registry.yarnpkg.com/helmet/-/helmet-3.22.0.tgz#3a6f11d931799145f0aff15dbc563cff9e13131f" + integrity sha512-Xrqicn2nm1ZIUxP3YGuTBmbDL04neKsIT583Sjh0FkiwKDXYCMUqGqC88w3NUvVXtA75JyR2Jn6jw6ZEMOD+ZA== dependencies: depd "2.0.0" dns-prefetch-control "0.2.0" @@ -4691,7 +4766,7 @@ helmet@~3.21.3: feature-policy "0.3.0" frameguard "3.1.0" helmet-crossdomain "0.4.0" - helmet-csp "2.9.5" + helmet-csp "2.10.0" hide-powered-by "1.1.0" hpkp "2.0.0" hsts "2.2.0" @@ -4829,6 +4904,11 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: dependencies: safer-buffer ">= 2.1.2 < 3" +ieee754@1.1.13, ieee754@^1.1.4: + version "1.1.13" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== + ienoopen@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/ienoopen/-/ienoopen-1.1.0.tgz#411e5d530c982287dbdc3bb31e7a9c9e32630974" @@ -5386,353 +5466,363 @@ iterall@^1.1.3, iterall@^1.2.1, iterall@^1.2.2, iterall@^1.3.0: resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea" integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg== -jest-changed-files@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-25.1.0.tgz#73dae9a7d9949fdfa5c278438ce8f2ff3ec78131" - integrity sha512-bdL1aHjIVy3HaBO3eEQeemGttsq1BDlHgWcOjEOIAcga7OOEGWHD2WSu8HhL7I1F0mFFyci8VKU4tRNk+qtwDA== +jest-changed-files@^25.2.3: + version "25.2.3" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-25.2.3.tgz#ad19deef9e47ba37efb432d2c9a67dfd97cc78af" + integrity sha512-EFxy94dvvbqRB36ezIPLKJ4fDIC+jAdNs8i8uTwFpaXd6H3LVc3ova1lNS4ZPWk09OCR2vq5kSdSQgar7zMORg== dependencies: - "@jest/types" "^25.1.0" + "@jest/types" "^25.2.3" execa "^3.2.0" throat "^5.0.0" -jest-cli@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-25.1.0.tgz#75f0b09cf6c4f39360906bf78d580be1048e4372" - integrity sha512-p+aOfczzzKdo3AsLJlhs8J5EW6ffVidfSZZxXedJ0mHPBOln1DccqFmGCoO8JWd4xRycfmwy1eoQkMsF8oekPg== +jest-cli@^25.2.4: + version "25.2.4" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-25.2.4.tgz#021c2383904696597abc060dcb133c82ebd8bfcc" + integrity sha512-zeY2pRDWKj2LZudIncvvguwLMEdcnJqc2jJbwza1beqi80qqLvkPF/BjbFkK2sIV3r+mfTJS+7ITrvK6pCdRjg== dependencies: - "@jest/core" "^25.1.0" - "@jest/test-result" "^25.1.0" - "@jest/types" "^25.1.0" + "@jest/core" "^25.2.4" + "@jest/test-result" "^25.2.4" + "@jest/types" "^25.2.3" chalk "^3.0.0" exit "^0.1.2" import-local "^3.0.2" is-ci "^2.0.0" - jest-config "^25.1.0" - jest-util "^25.1.0" - jest-validate "^25.1.0" + jest-config "^25.2.4" + jest-util "^25.2.3" + jest-validate "^25.2.3" prompts "^2.0.1" - realpath-native "^1.1.0" - yargs "^15.0.0" + realpath-native "^2.0.0" + yargs "^15.3.1" -jest-config@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-25.1.0.tgz#d114e4778c045d3ef239452213b7ad3ec1cbea90" - integrity sha512-tLmsg4SZ5H7tuhBC5bOja0HEblM0coS3Wy5LTCb2C8ZV6eWLewHyK+3qSq9Bi29zmWQ7ojdCd3pxpx4l4d2uGw== +jest-config@^25.2.4: + version "25.2.4" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-25.2.4.tgz#f4f33238979f225683179c89d1e402893008975d" + integrity sha512-fxy3nIpwJqOUQJRVF/q+pNQb6dv5b9YufOeCbpPZJ/md1zXpiupbhfehpfODhnKOfqbzSiigtSLzlWWmbRxnqQ== dependencies: "@babel/core" "^7.1.0" - "@jest/test-sequencer" "^25.1.0" - "@jest/types" "^25.1.0" - babel-jest "^25.1.0" + "@jest/test-sequencer" "^25.2.4" + "@jest/types" "^25.2.3" + babel-jest "^25.2.4" chalk "^3.0.0" + deepmerge "^4.2.2" glob "^7.1.1" - jest-environment-jsdom "^25.1.0" - jest-environment-node "^25.1.0" - jest-get-type "^25.1.0" - jest-jasmine2 "^25.1.0" - jest-regex-util "^25.1.0" - jest-resolve "^25.1.0" - jest-util "^25.1.0" - jest-validate "^25.1.0" + jest-environment-jsdom "^25.2.4" + jest-environment-node "^25.2.4" + jest-get-type "^25.2.1" + jest-jasmine2 "^25.2.4" + jest-regex-util "^25.2.1" + jest-resolve "^25.2.3" + jest-util "^25.2.3" + jest-validate "^25.2.3" micromatch "^4.0.2" - pretty-format "^25.1.0" - realpath-native "^1.1.0" + pretty-format "^25.2.3" + realpath-native "^2.0.0" -jest-diff@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.1.0.tgz#58b827e63edea1bc80c1de952b80cec9ac50e1ad" - integrity sha512-nepXgajT+h017APJTreSieh4zCqnSHEJ1iT8HDlewu630lSJ4Kjjr9KNzm+kzGwwcpsDE6Snx1GJGzzsefaEHw== +jest-diff@^25.2.3: + version "25.2.3" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.2.3.tgz#54d601a0a754ef26e808a8c8dbadd278c215aa3f" + integrity sha512-VtZ6LAQtaQpFsmEzps15dQc5ELbJxy4L2DOSo2Ev411TUEtnJPkAMD7JneVypeMJQ1y3hgxN9Ao13n15FAnavg== dependencies: chalk "^3.0.0" - diff-sequences "^25.1.0" - jest-get-type "^25.1.0" - pretty-format "^25.1.0" + diff-sequences "^25.2.1" + jest-get-type "^25.2.1" + pretty-format "^25.2.3" -jest-docblock@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-25.1.0.tgz#0f44bea3d6ca6dfc38373d465b347c8818eccb64" - integrity sha512-370P/mh1wzoef6hUKiaMcsPtIapY25suP6JqM70V9RJvdKLrV4GaGbfUseUVk4FZJw4oTZ1qSCJNdrClKt5JQA== +jest-docblock@^25.2.3: + version "25.2.3" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-25.2.3.tgz#ac45280c43d59e7139f9fbe5896c6e0320c01ebb" + integrity sha512-d3/tmjLLrH5fpRGmIm3oFa3vOaD/IjPxtXVOrfujpfJ9y1tCDB1x/tvunmdOVAyF03/xeMwburl6ITbiQT1mVA== dependencies: detect-newline "^3.0.0" -jest-each@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-25.1.0.tgz#a6b260992bdf451c2d64a0ccbb3ac25e9b44c26a" - integrity sha512-R9EL8xWzoPySJ5wa0DXFTj7NrzKpRD40Jy+zQDp3Qr/2QmevJgkN9GqioCGtAJ2bW9P/MQRznQHQQhoeAyra7A== +jest-each@^25.2.3: + version "25.2.3" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-25.2.3.tgz#64067ba1508ebbd07e9b126c173ab371e8e6309d" + integrity sha512-RTlmCjsBDK2c9T5oO4MqccA3/5Y8BUtiEy7OOQik1iyCgdnNdHbI0pNEpyapZPBG0nlvZ4mIu7aY6zNUvLraAQ== dependencies: - "@jest/types" "^25.1.0" + "@jest/types" "^25.2.3" chalk "^3.0.0" - jest-get-type "^25.1.0" - jest-util "^25.1.0" - pretty-format "^25.1.0" + jest-get-type "^25.2.1" + jest-util "^25.2.3" + pretty-format "^25.2.3" -jest-environment-jsdom@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-25.1.0.tgz#6777ab8b3e90fd076801efd3bff8e98694ab43c3" - integrity sha512-ILb4wdrwPAOHX6W82GGDUiaXSSOE274ciuov0lztOIymTChKFtC02ddyicRRCdZlB5YSrv3vzr1Z5xjpEe1OHQ== +jest-environment-jsdom@^25.2.4: + version "25.2.4" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-25.2.4.tgz#f2783541d0538b1bc43641703372cea6a2e83611" + integrity sha512-5dm+tNwrLmhELdjAwiQnVGf/U9iFMWdTL4/wyrMg2HU6RQnCiuxpWbIigLHUhuP1P2Ak0F4k3xhjrikboKyShA== dependencies: - "@jest/environment" "^25.1.0" - "@jest/fake-timers" "^25.1.0" - "@jest/types" "^25.1.0" - jest-mock "^25.1.0" - jest-util "^25.1.0" - jsdom "^15.1.1" + "@jest/environment" "^25.2.4" + "@jest/fake-timers" "^25.2.4" + "@jest/types" "^25.2.3" + jest-mock "^25.2.3" + jest-util "^25.2.3" + jsdom "^15.2.1" -jest-environment-node@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-25.1.0.tgz#797bd89b378cf0bd794dc8e3dca6ef21126776db" - integrity sha512-U9kFWTtAPvhgYY5upnH9rq8qZkj6mYLup5l1caAjjx9uNnkLHN2xgZy5mo4SyLdmrh/EtB9UPpKFShvfQHD0Iw== +jest-environment-node@^25.2.4: + version "25.2.4" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-25.2.4.tgz#dc211dfb0d8b66dfc1965a8f846e72e54ff0c430" + integrity sha512-Jkc5Y8goyXPrLRHnrUlqC7P4o5zn2m4zw6qWoRJ59kxV1f2a5wK+TTGhrhCwnhW/Ckpdl/pm+LufdvhJkvJbiw== dependencies: - "@jest/environment" "^25.1.0" - "@jest/fake-timers" "^25.1.0" - "@jest/types" "^25.1.0" - jest-mock "^25.1.0" - jest-util "^25.1.0" + "@jest/environment" "^25.2.4" + "@jest/fake-timers" "^25.2.4" + "@jest/types" "^25.2.3" + jest-mock "^25.2.3" + jest-util "^25.2.3" + semver "^6.3.0" -jest-get-type@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-25.1.0.tgz#1cfe5fc34f148dc3a8a3b7275f6b9ce9e2e8a876" - integrity sha512-yWkBnT+5tMr8ANB6V+OjmrIJufHtCAqI5ic2H40v+tRqxDmE0PGnIiTyvRWFOMtmVHYpwRqyazDbTnhpjsGvLw== +jest-get-type@^25.2.1: + version "25.2.1" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-25.2.1.tgz#6c83de603c41b1627e6964da2f5454e6aa3c13a6" + integrity sha512-EYjTiqcDTCRJDcSNKbLTwn/LcDPEE7ITk8yRMNAOjEsN6yp+Uu+V1gx4djwnuj/DvWg0YGmqaBqPVGsPxlvE7w== -jest-haste-map@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-25.1.0.tgz#ae12163d284f19906260aa51fd405b5b2e5a4ad3" - integrity sha512-/2oYINIdnQZAqyWSn1GTku571aAfs8NxzSErGek65Iu5o8JYb+113bZysRMcC/pjE5v9w0Yz+ldbj9NxrFyPyw== +jest-haste-map@^25.2.3: + version "25.2.3" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-25.2.3.tgz#2649392b5af191f0167a27bfb62e5d96d7eaaade" + integrity sha512-pAP22OHtPr4qgZlJJFks2LLgoQUr4XtM1a+F5UaPIZNiCRnePA0hM3L7aiJ0gzwiNIYwMTfKRwG/S1L28J3A3A== dependencies: - "@jest/types" "^25.1.0" + "@jest/types" "^25.2.3" anymatch "^3.0.3" fb-watchman "^2.0.0" graceful-fs "^4.2.3" - jest-serializer "^25.1.0" - jest-util "^25.1.0" - jest-worker "^25.1.0" + jest-serializer "^25.2.1" + jest-util "^25.2.3" + jest-worker "^25.2.1" micromatch "^4.0.2" sane "^4.0.3" walker "^1.0.7" + which "^2.0.2" optionalDependencies: fsevents "^2.1.2" -jest-jasmine2@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-25.1.0.tgz#681b59158a430f08d5d0c1cce4f01353e4b48137" - integrity sha512-GdncRq7jJ7sNIQ+dnXvpKO2MyP6j3naNK41DTTjEAhLEdpImaDA9zSAZwDhijjSF/D7cf4O5fdyUApGBZleaEg== +jest-jasmine2@^25.2.4: + version "25.2.4" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-25.2.4.tgz#5f77de83e1027f0c7588137055a80da773872374" + integrity sha512-juoKrmNmLwaheNbAg71SuUF9ovwUZCFNTpKVhvCXWk+SSeORcIUMptKdPCoLXV3D16htzhTSKmNxnxSk4SrTjA== dependencies: "@babel/traverse" "^7.1.0" - "@jest/environment" "^25.1.0" - "@jest/source-map" "^25.1.0" - "@jest/test-result" "^25.1.0" - "@jest/types" "^25.1.0" + "@jest/environment" "^25.2.4" + "@jest/source-map" "^25.2.1" + "@jest/test-result" "^25.2.4" + "@jest/types" "^25.2.3" chalk "^3.0.0" co "^4.6.0" - expect "^25.1.0" + expect "^25.2.4" is-generator-fn "^2.0.0" - jest-each "^25.1.0" - jest-matcher-utils "^25.1.0" - jest-message-util "^25.1.0" - jest-runtime "^25.1.0" - jest-snapshot "^25.1.0" - jest-util "^25.1.0" - pretty-format "^25.1.0" + jest-each "^25.2.3" + jest-matcher-utils "^25.2.3" + jest-message-util "^25.2.4" + jest-runtime "^25.2.4" + jest-snapshot "^25.2.4" + jest-util "^25.2.3" + pretty-format "^25.2.3" throat "^5.0.0" -jest-leak-detector@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-25.1.0.tgz#ed6872d15aa1c72c0732d01bd073dacc7c38b5c6" - integrity sha512-3xRI264dnhGaMHRvkFyEKpDeaRzcEBhyNrOG5oT8xPxOyUAblIAQnpiR3QXu4wDor47MDTiHbiFcbypdLcLW5w== +jest-leak-detector@^25.2.3: + version "25.2.3" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-25.2.3.tgz#4cf39f137925e0061c04c24ca65cae36465f0238" + integrity sha512-yblCMPE7NJKl7778Cf/73yyFWAas5St0iiEBwq7RDyaz6Xd4WPFnPz2j7yDb/Qce71A1IbDoLADlcwD8zT74Aw== dependencies: - jest-get-type "^25.1.0" - pretty-format "^25.1.0" + jest-get-type "^25.2.1" + pretty-format "^25.2.3" -jest-matcher-utils@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-25.1.0.tgz#fa5996c45c7193a3c24e73066fc14acdee020220" - integrity sha512-KGOAFcSFbclXIFE7bS4C53iYobKI20ZWleAdAFun4W1Wz1Kkej8Ng6RRbhL8leaEvIOjGXhGf/a1JjO8bkxIWQ== +jest-matcher-utils@^25.2.3: + version "25.2.3" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-25.2.3.tgz#59285bd6d6c810debc9caa585ed985e46a3f28fd" + integrity sha512-ZmiXiwQRVM9MoKjGMP5YsGGk2Th5ncyRxfXKz5AKsmU8m43kgNZirckVzaP61MlSa9LKmXbevdYqVp1ZKAw2Rw== dependencies: chalk "^3.0.0" - jest-diff "^25.1.0" - jest-get-type "^25.1.0" - pretty-format "^25.1.0" + jest-diff "^25.2.3" + jest-get-type "^25.2.1" + pretty-format "^25.2.3" -jest-message-util@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-25.1.0.tgz#702a9a5cb05c144b9aa73f06e17faa219389845e" - integrity sha512-Nr/Iwar2COfN22aCqX0kCVbXgn8IBm9nWf4xwGr5Olv/KZh0CZ32RKgZWMVDXGdOahicM10/fgjdimGNX/ttCQ== +jest-message-util@^25.2.4: + version "25.2.4" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-25.2.4.tgz#b1441b9c82f5c11fc661303cbf200a2f136a7762" + integrity sha512-9wWMH3Bf+GVTv0GcQLmH/FRr0x0toptKw9TA8U5YFLVXx7Tq9pvcNzTyJrcTJ+wLqNbMPPJlJNft4MnlcrtF5Q== dependencies: "@babel/code-frame" "^7.0.0" - "@jest/test-result" "^25.1.0" - "@jest/types" "^25.1.0" + "@jest/test-result" "^25.2.4" + "@jest/types" "^25.2.3" "@types/stack-utils" "^1.0.1" chalk "^3.0.0" micromatch "^4.0.2" slash "^3.0.0" stack-utils "^1.0.1" -jest-mock@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-25.1.0.tgz#411d549e1b326b7350b2e97303a64715c28615fd" - integrity sha512-28/u0sqS+42vIfcd1mlcg4ZVDmSUYuNvImP4X2lX5hRMLW+CN0BeiKVD4p+ujKKbSPKd3rg/zuhCF+QBLJ4vag== +jest-mock@^25.2.3: + version "25.2.3" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-25.2.3.tgz#b37a581f59d61bd91db27a99bf7eb8b3e5e993d5" + integrity sha512-xlf+pyY0j47zoCs8zGGOGfWyxxLximE8YFOfEK8s4FruR8DtM/UjNj61um+iDuMAFEBDe1bhCXkqiKoCmWjJzg== dependencies: - "@jest/types" "^25.1.0" + "@jest/types" "^25.2.3" jest-pnp-resolver@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz#ecdae604c077a7fbc70defb6d517c3c1c898923a" integrity sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ== -jest-regex-util@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-25.1.0.tgz#efaf75914267741838e01de24da07b2192d16d87" - integrity sha512-9lShaDmDpqwg+xAd73zHydKrBbbrIi08Kk9YryBEBybQFg/lBWR/2BDjjiSE7KIppM9C5+c03XiDaZ+m4Pgs1w== +jest-regex-util@^25.2.1: + version "25.2.1" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-25.2.1.tgz#db64b0d15cd3642c93b7b9627801d7c518600584" + integrity sha512-wroFVJw62LdqTdkL508ZLV82FrJJWVJMIuYG7q4Uunl1WAPTf4ftPKrqqfec4SvOIlvRZUdEX2TFpWR356YG/w== -jest-resolve-dependencies@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-25.1.0.tgz#8a1789ec64eb6aaa77fd579a1066a783437e70d2" - integrity sha512-Cu/Je38GSsccNy4I2vL12ZnBlD170x2Oh1devzuM9TLH5rrnLW1x51lN8kpZLYTvzx9j+77Y5pqBaTqfdzVzrw== +jest-resolve-dependencies@^25.2.4: + version "25.2.4" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-25.2.4.tgz#2d904400387d74a366dff54badb40a2b3210e733" + integrity sha512-qhUnK4PfNHzNdca7Ub1mbAqE0j5WNyMTwxBZZJjQlUrdqsiYho/QGK65FuBkZuSoYtKIIqriR9TpGrPEc3P5Gg== dependencies: - "@jest/types" "^25.1.0" - jest-regex-util "^25.1.0" - jest-snapshot "^25.1.0" + "@jest/types" "^25.2.3" + jest-regex-util "^25.2.1" + jest-snapshot "^25.2.4" -jest-resolve@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-25.1.0.tgz#23d8b6a4892362baf2662877c66aa241fa2eaea3" - integrity sha512-XkBQaU1SRCHj2Evz2Lu4Czs+uIgJXWypfO57L7JYccmAXv4slXA6hzNblmcRmf7P3cQ1mE7fL3ABV6jAwk4foQ== +jest-resolve@^25.2.3: + version "25.2.3" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-25.2.3.tgz#ababeaf2bb948cb6d2dea8453759116da0fb7842" + integrity sha512-1vZMsvM/DBH258PnpUNSXIgtzpYz+vCVCj9+fcy4akZl4oKbD+9hZSlfe9RIDpU0Fc28ozHQrmwX3EqFRRIHGg== dependencies: - "@jest/types" "^25.1.0" + "@jest/types" "^25.2.3" browser-resolve "^1.11.3" chalk "^3.0.0" jest-pnp-resolver "^1.2.1" - realpath-native "^1.1.0" + realpath-native "^2.0.0" + resolve "^1.15.1" -jest-runner@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-25.1.0.tgz#fef433a4d42c89ab0a6b6b268e4a4fbe6b26e812" - integrity sha512-su3O5fy0ehwgt+e8Wy7A8CaxxAOCMzL4gUBftSs0Ip32S0epxyZPDov9Znvkl1nhVOJNf4UwAsnqfc3plfQH9w== +jest-runner@^25.2.4: + version "25.2.4" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-25.2.4.tgz#d0daf7c56b4a83b6b675863d5cdcd502c960f9a1" + integrity sha512-5xaIfqqxck9Wg2CV4b9KmJtf/sWO7zWQx7O+34GCLGPzoPcVmB3mZtdrQI1/jS3Reqjru9ycLjgLHSf6XoxRqA== dependencies: - "@jest/console" "^25.1.0" - "@jest/environment" "^25.1.0" - "@jest/test-result" "^25.1.0" - "@jest/types" "^25.1.0" + "@jest/console" "^25.2.3" + "@jest/environment" "^25.2.4" + "@jest/test-result" "^25.2.4" + "@jest/types" "^25.2.3" chalk "^3.0.0" exit "^0.1.2" graceful-fs "^4.2.3" - jest-config "^25.1.0" - jest-docblock "^25.1.0" - jest-haste-map "^25.1.0" - jest-jasmine2 "^25.1.0" - jest-leak-detector "^25.1.0" - jest-message-util "^25.1.0" - jest-resolve "^25.1.0" - jest-runtime "^25.1.0" - jest-util "^25.1.0" - jest-worker "^25.1.0" + jest-config "^25.2.4" + jest-docblock "^25.2.3" + jest-haste-map "^25.2.3" + jest-jasmine2 "^25.2.4" + jest-leak-detector "^25.2.3" + jest-message-util "^25.2.4" + jest-resolve "^25.2.3" + jest-runtime "^25.2.4" + jest-util "^25.2.3" + jest-worker "^25.2.1" source-map-support "^0.5.6" throat "^5.0.0" -jest-runtime@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-25.1.0.tgz#02683218f2f95aad0f2ec1c9cdb28c1dc0ec0314" - integrity sha512-mpPYYEdbExKBIBB16ryF6FLZTc1Rbk9Nx0ryIpIMiDDkOeGa0jQOKVI/QeGvVGlunKKm62ywcioeFVzIbK03bA== +jest-runtime@^25.2.4: + version "25.2.4" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-25.2.4.tgz#c66a421e115944426b377a7fd331f6c0902cfa56" + integrity sha512-6ehOUizgIghN+aV5YSrDzTZ+zJ9omgEjJbTHj3Jqes5D52XHfhzT7cSfdREwkNjRytrR7mNwZ7pRauoyNLyJ8Q== dependencies: - "@jest/console" "^25.1.0" - "@jest/environment" "^25.1.0" - "@jest/source-map" "^25.1.0" - "@jest/test-result" "^25.1.0" - "@jest/transform" "^25.1.0" - "@jest/types" "^25.1.0" + "@jest/console" "^25.2.3" + "@jest/environment" "^25.2.4" + "@jest/source-map" "^25.2.1" + "@jest/test-result" "^25.2.4" + "@jest/transform" "^25.2.4" + "@jest/types" "^25.2.3" "@types/yargs" "^15.0.0" chalk "^3.0.0" collect-v8-coverage "^1.0.0" exit "^0.1.2" glob "^7.1.3" graceful-fs "^4.2.3" - jest-config "^25.1.0" - jest-haste-map "^25.1.0" - jest-message-util "^25.1.0" - jest-mock "^25.1.0" - jest-regex-util "^25.1.0" - jest-resolve "^25.1.0" - jest-snapshot "^25.1.0" - jest-util "^25.1.0" - jest-validate "^25.1.0" - realpath-native "^1.1.0" + jest-config "^25.2.4" + jest-haste-map "^25.2.3" + jest-message-util "^25.2.4" + jest-mock "^25.2.3" + jest-regex-util "^25.2.1" + jest-resolve "^25.2.3" + jest-snapshot "^25.2.4" + jest-util "^25.2.3" + jest-validate "^25.2.3" + realpath-native "^2.0.0" slash "^3.0.0" strip-bom "^4.0.0" - yargs "^15.0.0" + yargs "^15.3.1" -jest-serializer@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-25.1.0.tgz#73096ba90e07d19dec4a0c1dd89c355e2f129e5d" - integrity sha512-20Wkq5j7o84kssBwvyuJ7Xhn7hdPeTXndnwIblKDR2/sy1SUm6rWWiG9kSCgJPIfkDScJCIsTtOKdlzfIHOfKA== +jest-serializer@^25.2.1: + version "25.2.1" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-25.2.1.tgz#51727a5fc04256f461abe0fa024a022ba165877a" + integrity sha512-fibDi7M5ffx6c/P66IkvR4FKkjG5ldePAK1WlbNoaU4GZmIAkS9Le/frAwRUFEX0KdnisSPWf+b1RC5jU7EYJQ== -jest-snapshot@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-25.1.0.tgz#d5880bd4b31faea100454608e15f8d77b9d221d9" - integrity sha512-xZ73dFYN8b/+X2hKLXz4VpBZGIAn7muD/DAg+pXtDzDGw3iIV10jM7WiHqhCcpDZfGiKEj7/2HXAEPtHTj0P2A== +jest-snapshot@^25.2.4: + version "25.2.4" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-25.2.4.tgz#08d4517579c864df4280bcc948ceea34327a4ded" + integrity sha512-nIwpW7FZCq5p0AE3Oyqyb6jL0ENJixXzJ5/CD/XRuOqp3gS5OM3O/k+NnTrniCXxPFV4ry6s9HNfiPQBi0wcoA== dependencies: "@babel/types" "^7.0.0" - "@jest/types" "^25.1.0" + "@jest/types" "^25.2.3" + "@types/prettier" "^1.19.0" chalk "^3.0.0" - expect "^25.1.0" - jest-diff "^25.1.0" - jest-get-type "^25.1.0" - jest-matcher-utils "^25.1.0" - jest-message-util "^25.1.0" - jest-resolve "^25.1.0" - mkdirp "^0.5.1" + expect "^25.2.4" + jest-diff "^25.2.3" + jest-get-type "^25.2.1" + jest-matcher-utils "^25.2.3" + jest-message-util "^25.2.4" + jest-resolve "^25.2.3" + make-dir "^3.0.0" natural-compare "^1.4.0" - pretty-format "^25.1.0" - semver "^7.1.1" + pretty-format "^25.2.3" + semver "^6.3.0" -jest-util@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-25.1.0.tgz#7bc56f7b2abd534910e9fa252692f50624c897d9" - integrity sha512-7did6pLQ++87Qsj26Fs/TIwZMUFBXQ+4XXSodRNy3luch2DnRXsSnmpVtxxQ0Yd6WTipGpbhh2IFP1mq6/fQGw== +jest-util@^25.2.3: + version "25.2.3" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-25.2.3.tgz#0abf95a1d6b96f2de5a3ecd61b36c40a182dc256" + integrity sha512-7tWiMICVSo9lNoObFtqLt9Ezt5exdFlWs5fLe1G4XLY2lEbZc814cw9t4YHScqBkWMfzth8ASHKlYBxiX2rdCw== dependencies: - "@jest/types" "^25.1.0" + "@jest/types" "^25.2.3" chalk "^3.0.0" is-ci "^2.0.0" - mkdirp "^0.5.1" + make-dir "^3.0.0" -jest-validate@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-25.1.0.tgz#1469fa19f627bb0a9a98e289f3e9ab6a668c732a" - integrity sha512-kGbZq1f02/zVO2+t1KQGSVoCTERc5XeObLwITqC6BTRH3Adv7NZdYqCpKIZLUgpLXf2yISzQ465qOZpul8abXA== +jest-validate@^25.2.3: + version "25.2.3" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-25.2.3.tgz#ecb0f093cf8ae71d15075fb48439b6f78f1fcb5a" + integrity sha512-GObn91jzU0B0Bv4cusAwjP6vnWy78hJUM8MOSz7keRfnac/ZhQWIsUjvk01IfeXNTemCwgR57EtdjQMzFZGREg== dependencies: - "@jest/types" "^25.1.0" + "@jest/types" "^25.2.3" camelcase "^5.3.1" chalk "^3.0.0" - jest-get-type "^25.1.0" + jest-get-type "^25.2.1" leven "^3.1.0" - pretty-format "^25.1.0" + pretty-format "^25.2.3" -jest-watcher@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-25.1.0.tgz#97cb4a937f676f64c9fad2d07b824c56808e9806" - integrity sha512-Q9eZ7pyaIr6xfU24OeTg4z1fUqBF/4MP6J801lyQfg7CsnZ/TCzAPvCfckKdL5dlBBEKBeHV0AdyjFZ5eWj4ig== +jest-watcher@^25.2.4: + version "25.2.4" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-25.2.4.tgz#dda85b914d470fa4145164a8f70bda4f208bafb6" + integrity sha512-p7g7s3zqcy69slVzQYcphyzkB2FBmJwMbv6k6KjI5mqd6KnUnQPfQVKuVj2l+34EeuxnbXqnrjtUFmxhcL87rg== dependencies: - "@jest/test-result" "^25.1.0" - "@jest/types" "^25.1.0" + "@jest/test-result" "^25.2.4" + "@jest/types" "^25.2.3" ansi-escapes "^4.2.1" chalk "^3.0.0" - jest-util "^25.1.0" + jest-util "^25.2.3" string-length "^3.1.0" -jest-worker@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-25.1.0.tgz#75d038bad6fdf58eba0d2ec1835856c497e3907a" - integrity sha512-ZHhHtlxOWSxCoNOKHGbiLzXnl42ga9CxDr27H36Qn+15pQZd3R/F24jrmjDelw9j/iHUIWMWs08/u2QN50HHOg== +jest-worker@^25.2.1: + version "25.2.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-25.2.1.tgz#209617015c768652646aa33a7828cc2ab472a18a" + integrity sha512-IHnpekk8H/hCUbBlfeaPZzU6v75bqwJp3n4dUrQuQOAgOneI4tx3jV2o8pvlXnDfcRsfkFIUD//HWXpCmR+evQ== dependencies: merge-stream "^2.0.0" supports-color "^7.0.0" -jest@~25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest/-/jest-25.1.0.tgz#b85ef1ddba2fdb00d295deebbd13567106d35be9" - integrity sha512-FV6jEruneBhokkt9MQk0WUFoNTwnF76CLXtwNMfsc0um0TlB/LG2yxUd0KqaFjEJ9laQmVWQWS0sG/t2GsuI0w== +jest@~25.2.4: + version "25.2.4" + resolved "https://registry.yarnpkg.com/jest/-/jest-25.2.4.tgz#d10941948a2b57eb7accc2e7ae78af4a0e11b40a" + integrity sha512-Lu4LXxf4+durzN/IFilcAoQSisOwgHIXgl9vffopePpSSwFqfj1Pj4y+k3nL8oTbnvjxgDIsEcepy6he4bWqnQ== dependencies: - "@jest/core" "^25.1.0" + "@jest/core" "^25.2.4" import-local "^3.0.2" - jest-cli "^25.1.0" + jest-cli "^25.2.4" + +jmespath@0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" + integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc= jquery@^3.3.1: version "3.4.1" @@ -5757,7 +5847,7 @@ jsbn@~0.1.0: resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= -jsdom@^15.1.1: +jsdom@^15.2.1: version "15.2.1" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-15.2.1.tgz#d2feb1aef7183f86be521b8c6833ff5296d07ec5" integrity sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g== @@ -5829,12 +5919,12 @@ json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= -json5@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.0.tgz#e7a0c62c48285c628d20a10b85c89bb807c32850" - integrity sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ== +json5@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.2.tgz#43ef1f0af9835dd624751a6b7fa48874fb2d608e" + integrity sha512-MoUOQ4WdiN3yxhm7NEVJSJrieAo5hNSLQ5sj05OTRHPL9HOBy8u4Bu88jsC1jvqAdN+E1bJmsUcZH+1HQxliqQ== dependencies: - minimist "^1.2.0" + minimist "^1.2.5" jsonwebtoken@^8.3.0, jsonwebtoken@~8.5.1: version "8.5.1" @@ -6296,13 +6386,13 @@ metascraper-publisher@^5.11.6: dependencies: "@metascraper/helpers" "^5.11.6" -metascraper-soundcloud@^5.11.6: - version "5.11.6" - resolved "https://registry.yarnpkg.com/metascraper-soundcloud/-/metascraper-soundcloud-5.11.6.tgz#01699553a37d5e02f73cd5b15792986f761e94e9" - integrity sha512-KR/KNK5pWthgwuixqyfL13uSwr+mUanzhC6LEYSL7kHYTtYl0mwG7P64Ab+OdloLMwRkekITK6EaD3T8Omj2tw== +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.6" - tldts "~5.6.10" + tldts "~5.6.12" metascraper-title@^5.11.6: version "5.11.6" @@ -6399,7 +6489,7 @@ mime-db@1.43.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== -mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.22, mime-types@~2.1.24, mime-types@~2.1.26: +mime-types@^2.1.12, mime-types@^2.1.26, mime-types@~2.1.19, mime-types@~2.1.22, mime-types@~2.1.24, mime-types@~2.1.26: version "2.1.26" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ== @@ -6443,6 +6533,11 @@ minimist@^1.1.1, minimist@^1.2.0: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= +minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + minipass@^2.2.1, minipass@^2.3.5: version "2.3.5" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848" @@ -6498,10 +6593,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 +6667,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" @@ -6691,7 +6786,7 @@ nodemailer-html-to-text@^3.1.0: dependencies: html-to-text "^5.1.1" -nodemailer@^6.4.5: +nodemailer@^6.4.4: version "6.4.5" resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.4.5.tgz#45614c6454d1a947242105eeddae03df87e29916" integrity sha512-NH7aNVQyZLAvGr2+EOto7znvz+qJ02Cb/xpou98ApUt5tEAUSVUxhvHvgV/8I5dhjKTYqUw0nasoKzLNBJKrDQ== @@ -7320,17 +7415,17 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" -prettier@~1.19.1: - version "1.19.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" - integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== +prettier@~2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.2.tgz#1ba8f3eb92231e769b7fcd7cb73ae1b6b74ade08" + integrity sha512-5xJQIPT8BraI7ZnaDwSbu5zLrB6vvi8hVV58yHQ+QK64qrY40dULy0HSRlQ2/2IdzeBpjhDkqdcFBnFeDEMVdg== -pretty-format@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.1.0.tgz#ed869bdaec1356fc5ae45de045e2c8ec7b07b0c8" - integrity sha512-46zLRSGLd02Rp+Lhad9zzuNZ+swunitn8zIpfD2B4OPCRLXbM87RJT2aBLBWYOznNUML/2l/ReMyWNC80PJBUQ== +pretty-format@^25.2.3: + version "25.2.3" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.2.3.tgz#ba6e9603a0d80fa2e470b1fed55de1f9bfd81421" + integrity sha512-IP4+5UOAVGoyqC/DiomOeHBUKN6q00gfyT2qpAsRH64tgOKB2yF7FHJXC18OCiU0/YFierACup/zdCOWw0F/0w== dependencies: - "@jest/types" "^25.1.0" + "@jest/types" "^25.2.3" ansi-regex "^5.0.0" ansi-styles "^4.0.0" react-is "^16.12.0" @@ -7408,6 +7503,11 @@ punycode2@~1.0.0: resolved "https://registry.yarnpkg.com/punycode2/-/punycode2-1.0.0.tgz#e2b4b9a9a8ff157d0b84438e203181ee7892dfd8" integrity sha1-4rS5qaj/FX0LhEOOIDGB7niS39g= +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= + punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -7428,6 +7528,11 @@ qs@~6.5.2: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= + querystringify@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" @@ -7551,12 +7656,10 @@ readdirp@~3.2.0: dependencies: picomatch "^2.0.4" -realpath-native@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" - integrity sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA== - dependencies: - util.promisify "^1.0.0" +realpath-native@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-2.0.0.tgz#7377ac429b6e1fd599dc38d08ed942d0d7beb866" + integrity sha512-v1SEYUOXXdbBZK8ZuNgO4TBjamPsiSgcFr0aP+tEKpQZK8vooEUqV6nm6Cv502mX4NF2EfsnVqtNAHG+/6Ur1Q== redis-commands@1.5.0: version "1.5.0" @@ -7587,6 +7690,13 @@ regenerate-unicode-properties@^8.1.0: dependencies: regenerate "^1.4.0" +regenerate-unicode-properties@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec" + integrity sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA== + dependencies: + regenerate "^1.4.0" + regenerate@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" @@ -7640,6 +7750,18 @@ regexpu-core@^4.6.0: unicode-match-property-ecmascript "^1.0.4" unicode-match-property-value-ecmascript "^1.1.0" +regexpu-core@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.0.tgz#fcbf458c50431b0bb7b45d6967b8192d91f3d938" + integrity sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ== + dependencies: + regenerate "^1.4.0" + regenerate-unicode-properties "^8.2.0" + regjsgen "^0.5.1" + regjsparser "^0.6.4" + unicode-match-property-ecmascript "^1.0.4" + unicode-match-property-value-ecmascript "^1.2.0" + registry-auth-token@^3.0.1: version "3.4.0" resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.4.0.tgz#d7446815433f5d5ed6431cd5dca21048f66b397e" @@ -7660,6 +7782,11 @@ regjsgen@^0.5.0: resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.0.tgz#a7634dc08f89209c2049adda3525711fb97265dd" integrity sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA== +regjsgen@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.1.tgz#48f0bf1a5ea205196929c0d9798b42d1ed98443c" + integrity sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg== + regjsparser@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.0.tgz#f1e6ae8b7da2bae96c99399b868cd6c933a2ba9c" @@ -7667,6 +7794,13 @@ regjsparser@^0.6.0: dependencies: jsesc "~0.5.0" +regjsparser@^0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.4.tgz#a769f8684308401a66e9b529d2436ff4d0666272" + integrity sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw== + dependencies: + jsesc "~0.5.0" + remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" @@ -7766,10 +7900,10 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.3.2, resolve@^1.3.3, resolve@^1.5.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.14.1.tgz#9e018c540fcf0c427d678b9931cbf45e984bcaff" - integrity sha512-fn5Wobh4cxbLzuHaE+nphztHy43/b++4M6SsGFC2gB8uYwf0C8LcarfCz1un7UTW8OFQg9iNjZ4xpcFVGebDPg== +resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.15.1, resolve@^1.3.2, resolve@^1.3.3, resolve@^1.5.0: + version "1.15.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8" + integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w== dependencies: path-parse "^1.0.6" @@ -7895,7 +8029,7 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" -sanitize-html@~1.22.1: +sanitize-html@~1.22.0: version "1.22.1" resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.22.1.tgz#5b36c92ab27917ddd2775396815c2bc1a6268310" integrity sha512-++IMC00KfMQc45UWZJlhWOlS9eMrME38sFG9GXfR+k6oBo9JXSYQgTOZCl9j3v/smFTRNT9XNwz5DseFdMY+2Q== @@ -7911,6 +8045,11 @@ sanitize-html@~1.22.1: srcset "^2.0.1" xtend "^4.0.1" +sax@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" + integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= + sax@>=0.6.0, sax@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -7958,11 +8097,6 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.1.1.tgz#29104598a197d6cbe4733eeecbe968f7b43a9667" - integrity sha512-WfuG+fl6eh3eZ2qAf6goB7nhiCd7NPXhmyFxigB/TOkQyeLP8w8GsVehvtGNtnNmyboz4TgeK40B1Kbql/8c5A== - send@0.17.1: version "0.17.1" resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" @@ -8667,17 +8801,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.10: - version "5.6.10" - resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-5.6.10.tgz#509a93c2bfd79da5a456f06ef099a19fa00bc2c9" - integrity sha512-bbz6I/200adIZgrUcnCD2RiOWkOeyDSuRSyueYN6XFs/Jwoser30nQwEqFaXeKCRsUl4IobnOGWbvUC5ihcxCA== +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.10: - version "5.6.10" - resolved "https://registry.yarnpkg.com/tldts/-/tldts-5.6.10.tgz#b9a07d7ed14c7a2b7bcf48ac1f6908d5b06d28e1" - integrity sha512-YM/3+cMeulrAqGQE8EhU7Ugmw5PAqTUJJq51XTVzsh0S9VxEwFZRkJSn7B4OY/tgQIMu8GXzPMd9fIoiOxsRYA== +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.10" + tldts-core "^5.6.12" tmp@^0.0.33: version "0.0.33" @@ -8908,6 +9042,11 @@ unicode-match-property-value-ecmascript@^1.1.0: resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz#5b4b426e08d13a80365e0d657ac7a6c1ec46a277" integrity sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g== +unicode-match-property-value-ecmascript@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz#0d91f600eeeb3096aa962b1d6fc88876e64ea531" + integrity sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ== + unicode-property-aliases-ecmascript@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz#a9cc6cc7ce63a0a3023fc99e341b94431d405a57" @@ -9036,6 +9175,14 @@ url-regex@~5.0.0: ip-regex "^4.1.0" tlds "^1.203.0" +url@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" + integrity sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ= + dependencies: + punycode "1.3.2" + querystring "0.2.0" + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" @@ -9071,15 +9218,20 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= +uuid@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + uuid@^3.1.0, uuid@^3.3.2: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@~7.0.2: - version "7.0.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.2.tgz#7ff5c203467e91f5e0d85cfcbaaf7d2ebbca9be6" - integrity sha512-vy9V/+pKG+5ZTYKf+VcphF5Oc6EFiu3W8Nv3P3zIh0EqVI80ZxOzuPfe9EHjkFNvf8+xuTHVeei4Drydlx4zjw== +uuid@~7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" + integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== v8-compile-cache@^2.0.3: version "2.1.0" @@ -9110,10 +9262,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" @@ -9207,7 +9359,7 @@ which@^1.2.9, which@^1.3.1: dependencies: isexe "^2.0.0" -which@^2.0.1: +which@^2.0.1, which@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== @@ -9315,7 +9467,7 @@ xml-name-validator@^3.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== -xml2js@^0.4.17: +xml2js@0.4.19, xml2js@^0.4.17: version "0.4.19" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== @@ -9360,18 +9512,18 @@ yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A== -yargs-parser@^16.1.0: - version "16.1.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-16.1.0.tgz#73747d53ae187e7b8dbe333f95714c76ea00ecf1" - integrity sha512-H/V41UNZQPkUMIT5h5hiwg4QKIY1RPvoBV4XcjUbRM8Bk2oKqqyZ0DIEbTFZB0XjbtSPG8SAa/0DxCQmiRgzKg== +yargs-parser@^18.1.1: + version "18.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.1.tgz#bf7407b915427fc760fcbbccc6c82b4f0ffcbd37" + integrity sha512-KRHEsOM16IX7XuLnMOqImcPNbLVXMNHYAoFc3BKR8Ortl5gzDbtXvvEoGx9imk5E+X1VeNKNlcHr8B8vi+7ipA== dependencies: camelcase "^5.0.0" decamelize "^1.2.0" -yargs@^15.0.0: - version "15.1.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.1.0.tgz#e111381f5830e863a89550bd4b136bb6a5f37219" - integrity sha512-T39FNN1b6hCW4SOIk1XyTOWxtXdcen0t+XYrysQmChzSipvhBO8Bj0nK1ozAasdk24dNWuMZvr4k24nz+8HHLg== +yargs@^15.3.1: + version "15.3.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.3.1.tgz#9505b472763963e54afe60148ad27a330818e98b" + integrity sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA== dependencies: cliui "^6.0.0" decamelize "^1.2.0" @@ -9383,7 +9535,7 @@ yargs@^15.0.0: string-width "^4.2.0" which-module "^2.0.0" y18n "^4.0.0" - yargs-parser "^16.1.0" + yargs-parser "^18.1.1" yup@^0.28.3: version "0.28.3" diff --git a/cypress/integration/common/post.js b/cypress/integration/common/post.js index d0298c5a3..cba238a63 100644 --- a/cypress/integration/common/post.js +++ b/cypress/integration/common/post.js @@ -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"); }); diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js index 9d0c4b03a..36536e302 100644 --- a/cypress/integration/common/steps.js +++ b/cypress/integration/common/steps.js @@ -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, }; diff --git a/cypress/integration/post/DeleteImage.feature b/cypress/integration/post/DeleteImage.feature index a3fa6f9b6..07bfe43b1 100644 --- a/cypress/integration/post/DeleteImage.feature +++ b/cypress/integration/post/DeleteImage.feature @@ -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 diff --git a/cypress/integration/search/Search.feature b/cypress/integration/search/Search.feature index e83f58477..b77b45d8e 100644 --- a/cypress/integration/search/Search.feature +++ b/cypress/integration/search/Search.feature @@ -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 | diff --git a/package.json b/package.json index 8695f5fc2..f0b83b3c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "human-connection", - "version": "0.4.2", + "version": "0.6.0", "description": "Fullstack and API tests with cypress and cucumber for Human Connection", "author": "Human Connection gGmbh", "license": "MIT", @@ -26,27 +26,27 @@ "generate:changelog": "yarn version && auto-changelog" }, "devDependencies": { - "@babel/core": "^7.8.7", - "@babel/preset-env": "^7.8.7", - "@babel/register": "^7.8.6", - "auto-changelog": "^1.16.2", + "@babel/core": "^7.9.0", + "@babel/preset-env": "^7.9.0", + "@babel/register": "^7.9.0", + "auto-changelog": "^1.16.3", "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-cucumber-preprocessor": "^2.0.1", + "cypress": "^4.2.0", + "cypress-cucumber-preprocessor": "^2.1.0", "cypress-file-upload": "^3.5.3", "cypress-plugin-retries": "^1.5.2", - "date-fns": "^2.10.0", + "date-fns": "^2.11.1", "dotenv": "^8.2.0", - "expect": "^25.1.0", + "expect": "^25.2.4", "faker": "Marak/faker.js#master", "graphql-request": "^1.8.2", "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", diff --git a/scripts/translations/sort.sh b/scripts/translations/sort.sh index aa5737de8..35496f27e 100755 --- a/scripts/translations/sort.sh +++ b/scripts/translations/sort.sh @@ -3,12 +3,6 @@ ROOT_DIR=$(dirname "$0")/../.. tmp=$(mktemp) exit_code=0 -errors=0 - -TEXT_RED="\e[31m" -TEXT_BLUE="\e[34m" -TEXT_RESET="\e[0m" -TEXT_BOLD="\e[1m" for locale_file in $ROOT_DIR/webapp/locales/*.json do @@ -22,13 +16,9 @@ do : # all good else exit_code=$? - echo -e "${TEXT_BOLD}${TEXT_RED}>>> $(basename -- $locale_file) is not sorted by keys <<<${TEXT_RESET}" - errors=1 + echo "$(basename -- $locale_file) is not sorted by keys" fi fi done -[ "$errors" = 1 ] && echo -e "${TEXT_BOLD}${TEXT_BLUE}Please run $0 --fix to sort your locale definitions!${TEXT_RESET}"; - - exit $exit_code diff --git a/webapp/app/router.scrollBehavior.js b/webapp/app/router.scrollBehavior.js index d34c4c38a..a87054048 100644 --- a/webapp/app/router.scrollBehavior.js +++ b/webapp/app/router.scrollBehavior.js @@ -1,4 +1,4 @@ -export default function(to, from, savedPosition) { +export default function (to, from, savedPosition) { if (savedPosition) return savedPosition // Edge case: If you click on a notification from a comment and then on the diff --git a/webapp/assets/_new/icons/index.js b/webapp/assets/_new/icons/index.js index daa0714f2..bc9ef6638 100644 --- a/webapp/assets/_new/icons/index.js +++ b/webapp/assets/_new/icons/index.js @@ -2,7 +2,7 @@ const svgFileList = require.context('./svgs', true, /\.svg/) const icons = {} const iconNames = [] -svgFileList.keys().forEach(fileName => { +svgFileList.keys().forEach((fileName) => { const svgCode = svgFileList(fileName) const iconName = fileName.replace('./', '').replace('.svg', '') icons[iconName] = svgCode diff --git a/webapp/assets/_new/styles/resets.scss b/webapp/assets/_new/styles/resets.scss index 144f22d10..72a6184b3 100644 --- a/webapp/assets/_new/styles/resets.scss +++ b/webapp/assets/_new/styles/resets.scss @@ -19,3 +19,10 @@ h6, p { margin: 0; } + +ul, +ol { + list-style-type: none; + padding: 0; + margin: 0; +} diff --git a/webapp/assets/_new/styles/tokens.scss b/webapp/assets/_new/styles/tokens.scss index 9e86b3c70..54a8b4042 100644 --- a/webapp/assets/_new/styles/tokens.scss +++ b/webapp/assets/_new/styles/tokens.scss @@ -248,7 +248,9 @@ $size-ribbon: 6px; * @presenter Spacing */ +$size-width-filter-sidebar: 85px; $size-width-paginate: 100px; +$size-max-width-filter-menu: 1026px; /** * @tokens Size Avatar diff --git a/webapp/components/AvatarMenu/AvatarMenu.spec.js b/webapp/components/AvatarMenu/AvatarMenu.spec.js index c432a5ad8..85f5c32a8 100644 --- a/webapp/components/AvatarMenu/AvatarMenu.spec.js +++ b/webapp/components/AvatarMenu/AvatarMenu.spec.js @@ -21,7 +21,7 @@ describe('AvatarMenu.vue', () => { return { href: '/profile/u343/matt' } }), }, - $t: jest.fn(a => a), + $t: jest.fn((a) => a), } getters = { 'auth/user': () => { @@ -86,21 +86,21 @@ describe('AvatarMenu.vue', () => { it('displays a link to user profile', () => { const profileLink = wrapper .findAll('.ds-menu-item span') - .at(wrapper.vm.routes.findIndex(route => route.path === '/profile/u343/matt')) + .at(wrapper.vm.routes.findIndex((route) => route.path === '/profile/u343/matt')) expect(profileLink.exists()).toBe(true) }) it('displays a link to the notifications page', () => { const notificationsLink = wrapper .findAll('.ds-menu-item span') - .at(wrapper.vm.routes.findIndex(route => route.path === '/notifications')) + .at(wrapper.vm.routes.findIndex((route) => route.path === '/notifications')) expect(notificationsLink.exists()).toBe(true) }) it('displays a link to the settings page', () => { const settingsLink = wrapper .findAll('.ds-menu-item span') - .at(wrapper.vm.routes.findIndex(route => route.path === '/settings')) + .at(wrapper.vm.routes.findIndex((route) => route.path === '/settings')) expect(settingsLink.exists()).toBe(true) }) }) @@ -121,7 +121,7 @@ describe('AvatarMenu.vue', () => { it('displays a link to moderation page', () => { const moderationLink = wrapper .findAll('.ds-menu-item span') - .at(wrapper.vm.routes.findIndex(route => route.path === '/moderation')) + .at(wrapper.vm.routes.findIndex((route) => route.path === '/moderation')) expect(moderationLink.exists()).toBe(true) }) @@ -147,7 +147,7 @@ describe('AvatarMenu.vue', () => { it('displays a link to admin page', () => { const adminLink = wrapper .findAll('.ds-menu-item span') - .at(wrapper.vm.routes.findIndex(route => route.path === '/admin')) + .at(wrapper.vm.routes.findIndex((route) => route.path === '/admin')) expect(adminLink.exists()).toBe(true) }) diff --git a/webapp/components/AvatarMenu/AvatarMenu.vue b/webapp/components/AvatarMenu/AvatarMenu.vue index f65c6f6cf..63c550fd3 100644 --- a/webapp/components/AvatarMenu/AvatarMenu.vue +++ b/webapp/components/AvatarMenu/AvatarMenu.vue @@ -20,7 +20,7 @@ {{ $t('login.hello') }} {{ userName }} diff --git a/webapp/components/Badges.spec.js b/webapp/components/Badges.spec.js index 5273fca21..f81eaafb1 100644 --- a/webapp/components/Badges.spec.js +++ b/webapp/components/Badges.spec.js @@ -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) + }) + }) + }) }) diff --git a/webapp/components/CommentCard/CommentCard.spec.js b/webapp/components/CommentCard/CommentCard.spec.js index b18ab67c0..0ebcb2649 100644 --- a/webapp/components/CommentCard/CommentCard.spec.js +++ b/webapp/components/CommentCard/CommentCard.spec.js @@ -29,8 +29,8 @@ describe('CommentCard.vue', () => { locale: () => 'en', }, $filters: { - truncate: a => a, - removeHtml: a => a, + truncate: (a) => a, + removeHtml: (a) => a, }, $route: { hash: '' }, $scrollTo: jest.fn(), diff --git a/webapp/components/CommentCard/CommentCard.story.js b/webapp/components/CommentCard/CommentCard.story.js index 1749999f3..467a125d5 100644 --- a/webapp/components/CommentCard/CommentCard.story.js +++ b/webapp/components/CommentCard/CommentCard.story.js @@ -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, diff --git a/webapp/components/CommentForm/CommentForm.spec.js b/webapp/components/CommentForm/CommentForm.spec.js index b940c561d..996e6ecfc 100644 --- a/webapp/components/CommentForm/CommentForm.spec.js +++ b/webapp/components/CommentForm/CommentForm.spec.js @@ -25,7 +25,7 @@ describe('CommentForm.vue', () => { success: jest.fn(), }, $filters: { - removeHtml: a => a, + removeHtml: (a) => a, }, } }) diff --git a/webapp/components/CommentForm/CommentForm.vue b/webapp/components/CommentForm/CommentForm.vue index 422530259..4bdb95f90 100644 --- a/webapp/components/CommentForm/CommentForm.vue +++ b/webapp/components/CommentForm/CommentForm.vue @@ -1,6 +1,6 @@